forgejo-mcp/demos/mcp-resource-templates.md
Christoph Görn 945b54b61c
docs: 📚 demos walkthrough for forgejo:// resource templates
Adds demos/mcp-resource-templates.md covering all 7 URI templates shipped
in PR #172: owner, repo, commit, commit-status, issue, pr, comment.

Uses stdio JSON-RPC for invocation (--cli does not cover resources).
Captures are real codeberg.org payloads against goern/forgejo-mcp.
Tokens redacted; only commit SHAs in 40+ char matches; gitleaks clean.

Closes part of OpenSpec task 1.11 (deferred manual client verification).
Bead: forgejo-mcp-pkz.
2026-05-28 18:05:34 +02:00

355 lines
14 KiB
Markdown

# Demo: MCP resource templates on the forgejo:// scheme
*2026-05-28T18:00:00Z by Showboat 0.6.1*
<!-- showboat-id: a3f7e2b1-mcp-resource-templates-demo-2026 -->
## Background
PR [#172](https://codeberg.org/goern/forgejo-mcp/pulls/172) (merged 2026-05-28,
commit `872d4c559868128fedd794327c14e0f74d257a84`) added 7 MCP resource templates
on the `forgejo://` URI scheme. The normative spec lives in the unarchived
change directory at
[`openspec/changes/mcp-resource-templates/specs/mcp-resources-core/spec.md`](../openspec/changes/mcp-resource-templates/specs/mcp-resources-core/spec.md);
on archive it moves to `openspec/specs/mcp-resources-core/spec.md`.
**Why resource templates?**
- **Instance-portable URIs.** `forgejo://repo/goern/forgejo-mcp` resolves against
whatever Forgejo instance the server is configured with. Same URI works on
codeberg.org and a self-hosted instance.
- **Additive, not replacing.** All existing tools remain. Clients that do not
support `resources/templates/list` fall back to tools transparently.
- **JSON primary + markdown sidecar.** Commit, issue, PR, and comment resources
return two content blocks: `application/json` for structured data and
`text/markdown` for the human-readable body/message.
- **Embedded-list cap = 30 + truncation sentinel.** Resources that embed lists
(comments, reviews, CI statuses) cap at 30 items. When truncated, a
`*_truncated: true` field and a `*_list_tool` escape-hatch name the
corresponding `list_*` tool to fetch more.
- **v1 scope.** `subscribe=false`, `listChanged=false`. Cache by URI where the
resource is pinned to an immutable SHA.
**Resource templates shipped:**
| # | URI template | MIME | Sidecar |
|---|-------------|------|---------|
| 1 | `forgejo://owner/{owner}` | json | — |
| 2 | `forgejo://repo/{owner}/{repo}` | json | — |
| 3 | `forgejo://repo/{owner}/{repo}/commit/{sha}` | json | markdown |
| 4 | `forgejo://repo/{owner}/{repo}/commit/{sha}/status` | json | — |
| 5 | `forgejo://repo/{owner}/{repo}/issue/{index}` | json | markdown |
| 6 | `forgejo://repo/{owner}/{repo}/pr/{index}` | json | markdown |
| 7 | `forgejo://repo/{owner}/{repo}/{kind}/{index}/comment/{id}` | json | markdown |
## Setup
```bash
export FORGEJO_URL=https://codeberg.org
export FORGEJO_ACCESS_TOKEN=<your-token>
make build
```
## Invocation via stdio JSON-RPC
`--cli` mode covers tools only. Resources require the MCP stdio transport.
Every section below uses `printf` to pipe JSON-RPC messages into the binary,
then `jq` to select the response by `id`.
Handshake used in every example below:
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'<YOUR REQUEST>' \
| ./forgejo-mcp -t stdio -url "$FORGEJO_URL" -token "$FORGEJO_ACCESS_TOKEN" 2>/dev/null \
| jq 'select(.id==<YOUR ID>)'
```
## 1. Discover all templates — resources/templates/list
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"resources/templates/list"}' \
| ./forgejo-mcp -t stdio -url "$FORGEJO_URL" -token "$FORGEJO_ACCESS_TOKEN" 2>/dev/null \
| jq 'select(.id==2) | .result.resourceTemplates | map({uriTemplate,name})'
```
```output
[
{
"uriTemplate": "forgejo://repo/{owner}/{repo}/{kind}/{index}/comment/{id}",
"name": "Forgejo Comment"
},
{
"uriTemplate": "forgejo://repo/{owner}/{repo}/commit/{sha}",
"name": "Forgejo Commit"
},
{
"uriTemplate": "forgejo://repo/{owner}/{repo}/commit/{sha}/status",
"name": "Forgejo Commit Status"
},
{
"uriTemplate": "forgejo://repo/{owner}/{repo}/issue/{index}",
"name": "Forgejo Issue"
},
{
"uriTemplate": "forgejo://owner/{owner}",
"name": "Forgejo Owner"
},
{
"uriTemplate": "forgejo://repo/{owner}/{repo}/pr/{index}",
"name": "Forgejo Pull Request"
},
{
"uriTemplate": "forgejo://repo/{owner}/{repo}",
"name": "Forgejo Repository"
}
]
```
All 7 templates registered.
## 2. Owner — forgejo://owner/{owner}
Resolves user or org by login. Tries user first; falls back to org on 404.
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":3,"method":"resources/read","params":{"uri":"forgejo://owner/goern"}}' \
| ./forgejo-mcp -t stdio -url "$FORGEJO_URL" -token "$FORGEJO_ACCESS_TOKEN" 2>/dev/null \
| jq 'select(.id==3) | .result.contents[0].text | fromjson'
```
```output
{
"login": "goern",
"full_name": "Christoph Görn",
"html_url": "https://codeberg.org/goern",
"kind": "user",
"website": "https://görn.name/",
"created_at": "2022-07-04T06:28:06+02:00",
"followers_count": 7,
"following_count": 2
}
```
`kind` is `"user"` or `"org"`. No embedded lists; single JSON block only.
## 3. Repository — forgejo://repo/{owner}/{repo}
Returns identity + mutable counts. No embedded lists.
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"forgejo://repo/goern/forgejo-mcp"}}' \
| ./forgejo-mcp -t stdio -url "$FORGEJO_URL" -token "$FORGEJO_ACCESS_TOKEN" 2>/dev/null \
| jq 'select(.id==4) | .result.contents[0].text | fromjson'
```
```output
{
"owner": "goern",
"name": "forgejo-mcp",
"full_name": "goern/forgejo-mcp",
"description": "This Model Context Protocol (MCP) server and Command Line Interface (CLI) tool provides tools and resources for interacting with the Forgejo (specifically Codeberg.org) REST API.",
"html_url": "https://codeberg.org/goern/forgejo-mcp",
"default_branch": "main",
"fork": false,
"archived": false,
"private": false,
"stars_count": 86,
"forks_count": 27,
"watchers_count": 9,
"open_issues_count": 9,
"open_pr_count": 1,
"size": 9129,
"has_issues": true,
"has_wiki": false,
"has_pull_requests": true
}
```
## 4. Commit — forgejo://repo/{owner}/{repo}/commit/{sha}
SHA must be the full 40-character hex SHA. Returns JSON + markdown sidecar.
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":5,"method":"resources/read","params":{"uri":"forgejo://repo/goern/forgejo-mcp/commit/872d4c559868128fedd794327c14e0f74d257a84"}}' \
| ./forgejo-mcp -t stdio -url "$FORGEJO_URL" -token "$FORGEJO_ACCESS_TOKEN" 2>/dev/null \
| jq 'select(.id==5) | .result.contents | map({mimeType, text: .text[:120]})'
```
```output
[
{
"mimeType": "application/json",
"text": "{\"url\":\"https://codeberg.org/api/v1/repos/goern/forgejo-mcp/git/commits/872d4c559868128fedd794327c14e0f74d257a84\",\"sha\":"
},
{
"mimeType": "text/markdown",
"text": "Merge pull request 'feat: MCP resource templates — 7 entities on forgejo:// URI scheme' (#172) from "
}
]
```
Two content blocks: `application/json` (full commit object from the SDK) and
`text/markdown` (commit message body). Clients that only want the message read
index 1; structured clients parse index 0.
## 5. Commit status — forgejo://repo/{owner}/{repo}/commit/{sha}/status
Aggregates CI contexts into a combined state. Safe to cache (pinned SHA).
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":6,"method":"resources/read","params":{"uri":"forgejo://repo/goern/forgejo-mcp/commit/872d4c559868128fedd794327c14e0f74d257a84/status"}}' \
| ./forgejo-mcp -t stdio -url "$FORGEJO_URL" -token "$FORGEJO_ACCESS_TOKEN" 2>/dev/null \
| jq 'select(.id==6) | .result.contents[0].text | fromjson | {sha,state,total_count}'
```
```output
{
"sha": "872d4c559868128fedd794327c14e0f74d257a84",
"state": "pending",
"total_count": 6
}
```
`state` is one of `success`, `failure`, `pending`, `unknown`. `total_count`
reflects all statuses returned. The `truncated` field is omitted when false
(`omitempty`); when over the embedded-list cap it appears as
`"truncated": true` alongside `"list_tool": "get_commit_statuses"` —
use the named tool for paginated enumeration.
## 6. Issue — forgejo://repo/{owner}/{repo}/issue/{index}
Returns JSON metadata + markdown sidecar. Embeds up to 30 recent comments.
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":7,"method":"resources/read","params":{"uri":"forgejo://repo/goern/forgejo-mcp/issue/148"}}' \
| ./forgejo-mcp -t stdio -url "$FORGEJO_URL" -token "$FORGEJO_ACCESS_TOKEN" 2>/dev/null \
| jq 'select(.id==7) | .result.contents | map({mimeType, snippet: .text[:200]})'
```
```output
[
{
"mimeType": "application/json",
"snippet": "{\"owner\":\"goern\",\"repo\":\"forgejo-mcp\",\"index\":148,\"title\":\"docs: OpenSpec proposal for MCP resource templates\",\"state\":\"closed\",\"author\":\"goern\",\"created_at\":\"2026-05-25T10:11:56+02:00\",\"updated_at\""
},
{
"mimeType": "text/markdown",
"snippet": "# docs: OpenSpec proposal for MCP resource templates\nState: closed · #148 · goern · 2026-05-25\n\n## Summary\r\n\r\nAdds the OpenSpec change `mcp-resource-templates` — a design-only proposal for exposin"
}
]
```
The markdown sidecar contains the full issue body rendered in place — useful
for agents that display or summarise issue content without parsing JSON.
## 7. Pull request — forgejo://repo/{owner}/{repo}/pr/{index}
Returns JSON + markdown sidecar. Embeds up to 30 recent comments and 30
recent reviews with truncation sentinels.
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":8,"method":"resources/read","params":{"uri":"forgejo://repo/goern/forgejo-mcp/pr/172"}}' \
| ./forgejo-mcp -t stdio -url "$FORGEJO_URL" -token "$FORGEJO_ACCESS_TOKEN" 2>/dev/null \
| jq 'select(.id==8) | .result.contents[0].text | fromjson | {title,state,comment_count,review_count,comments_truncated,comments_list_tool}'
```
```output
{
"title": "feat: MCP resource templates — 7 entities on forgejo:// URI scheme",
"state": "merged",
"comment_count": 107,
"review_count": 1,
"comments_truncated": true,
"comments_list_tool": "list_issue_comments"
}
```
PR #172 has 107 comments — well over the cap of 30. The sentinel fires:
`comments_truncated: true` + `comments_list_tool: "list_issue_comments"`.
An agent reads the 30 embedded excerpts, then calls `list_issue_comments`
with `page=2` for the rest.
The markdown sidecar provides `title · state · #index · author · created_at ·
head · base` followed by the PR description body — enough for a standalone
summary without further API calls.
## 8. Comment — forgejo://repo/{owner}/{repo}/{kind}/{index}/comment/{id}
`kind` is `issue` or `pr`. PR comments share the Forgejo issue-comment API;
`kind` is display context only, not a different fetch path. `id` is the
global comment ID (returned in `recent_comments[].id` above).
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":9,"method":"resources/read","params":{"uri":"forgejo://repo/goern/forgejo-mcp/pr/172/comment/16020554"}}' \
| ./forgejo-mcp -t stdio -url "$FORGEJO_URL" -token "$FORGEJO_ACCESS_TOKEN" 2>/dev/null \
| jq 'select(.id==9) | .result.contents | map({mimeType, snippet: .text[:200]})'
```
```output
[
{
"mimeType": "application/json",
"snippet": "{\"owner\":\"goern\",\"repo\":\"forgejo-mcp\",\"kind\":\"pr\",\"index\":172,\"id\":16020554,\"author\":\"op1st-gitops\",\"created_at\":\"2026-05-28T15:20:29+02:00\",\"updated_at\":\"2026-05-28T15:20:29+02:00\",\"body\":\""
},
{
"mimeType": "text/markdown",
"snippet": "op1st-gitops commented on pr#172:\nop1st Pipelines as Code/openspec-validate-pr-pwkpp is running.\n\nStarting Pipelinerun <b>[openspec-validate-pr-pwkpp](https://console-openshift-console.apps.nostromo."
}
]
```
## 9. End-to-end: autonomous read-only navigation
An agent navigating a repository without burning tool-call budget:
1. `resources/read forgejo://repo/goern/forgejo-mcp` — get counts (stars,
open issues, open PRs) at minimal cost. No list embedded; single JSON block.
2. `resources/read forgejo://repo/goern/forgejo-mcp/pr/172` — get PR metadata,
head/base refs, mergeability, and up to 30 comment excerpts. If
`comments_truncated: true`, continue with `list_issue_comments` tool.
3. For each `recent_comments[].id` the agent wants to read in full:
`resources/read forgejo://repo/goern/forgejo-mcp/pr/172/comment/{id}`.
4. `resources/read forgejo://repo/goern/forgejo-mcp/commit/{sha}/status` —
check CI. `state: "success"` → safe to merge. Response is cache-safe
because the SHA is immutable.
**Resources vs tools for read-only paths.** Resources return focused
payloads (no pagination params needed, no field projection). Tools are
better for mutations, paginated enumeration, and operations without a URI
anchor. Prefer resources for navigation; fall back to tools when the
embedded list is truncated or when writing.
## Out of scope / future
Two follow-ups filed after PR #172:
- **`forgejo-mcp-7ra`** — `subscribe=true` support: push notifications when
a resource changes (requires server-sent events on the transport side).
- **`forgejo-mcp-7de`** — cap telemetry: surface `truncated_count` and
`cap_used` metrics so clients can tune their request patterns.