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

14 KiB

Demo: MCP resource templates on the forgejo:// scheme

2026-05-28T18:00:00Z by Showboat 0.6.1

Background

PR #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; 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

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:

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

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})'
[
  {
    "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.

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'
{
  "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.

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'
{
  "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.

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]})'
[
  {
    "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).

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}'
{
  "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.

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]})'
[
  {
    "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.

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}'
{
  "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).

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]})'
[
  {
    "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.