Files
sherlock/docs/gitea-mcp.md
T
amacocian 8d159a9bcd
CI / build (push) Failing after 1s
Gitea tools improvement
2026-05-28 16:05:07 +02:00

12 KiB

gitea-mcp

The first sherlock MCP. Wraps a small slice of the Gitea REST API (whoami, list_repos) using a Gitea-issued OAuth2 bearer token.

How auth works

Gitea has two relevant features that share the word "OAuth":

  1. Gitea as OAuth client — Gitea logs users into its web UI via Authentik (your homelab SSO). This is how human accounts get provisioned the first time someone visits Gitea.
  2. Gitea as OAuth server — Gitea is itself an OAuth 2.0 / OIDC provider that third-party apps can authenticate against. The resulting access token is accepted by Gitea's own REST API.

gitea-mcp uses (2). The flow does not touch Authentik at all from sherlock's perspective — sherlock OAuths directly against Gitea using PKCE, gets a Gitea-minted bearer token, and uses it as Authorization: Bearer <token> on /api/v1/... calls. The fact that Gitea behind the scenes might bounce you through Authentik for SSO is invisible to sherlock.

This is deliberate. Going through Authentik would require Gitea to trust Authentik-issued bearer tokens at its API layer, which Gitea doesn't do out of the box. Going through Gitea's own OAuth2 server is the documented, supported path.

Known Gitea scope limitation

Gitea's OAuth2 server does not honour the full scope list sherlock requests. We send openid profile email read:user read:repository read:issue read:organization read:package, but Gitea silently grants only read:repository read:user regardless. Verified end-to-end 2026-05-28 against gitea.alexandru.macocian.me: the token's scope attribute on /login/oauth/access_token comes back narrowed, and any subsequent call into an ungranted area returns:

HTTP 403: token does not have at least one of required scope(s),
required=[read:issue|read:organization|read:package],
token scope=read:repository,read:user

Affected tools (every one returns 403 with this token, regardless of how many times you sherlock logout gitea and re-auth):

  • Issues / PRs: list_issues, get_issue, list_issue_comments (PRs themselves go through read:repository and do work).
  • Organisations: list_my_orgs, get_org, list_org_repos, list_org_members, list_org_teams, search_org_teams, list_org_activity_feed, list_org_workflow_runs, list_org_workflow_jobs, list_org_runners, get_org_runner.
  • Packages: list_packages.

This is a Gitea-side issue (the OAuth2 server doesn't surface those scopes on the consent screen, so the user can't grant them even if they wanted to). Workarounds we may consider later:

  1. Use a long-lived PAT scoped explicitly to issue/org/package read instead of an OAuth token for these tool families (would require a second wallet entry and a --use-pat knob on the MCP).
  2. Patch / upgrade Gitea once upstream wires these scopes into the OAuth2 consent flow.
  3. Drop the affected tools from the surface and document the gap.

Until one of those lands, those tools stay registered but will 403 at call time. The MCP itself does not refuse to start.

One-time setup

  1. Sign in to https://gitea.alexandru.macocian.me as the operator.
  2. Settings → Applications → Manage OAuth2 Applications → New Application:
    • Application Name: sherlock
    • Redirect URIs: http://127.0.0.1:6990/callback
    • Confidential Client: UNCHECK — sherlock uses PKCE, not a client secret. Leaving this checked makes Gitea demand a client_secret on the token request, which sherlock never sends.
  3. Click Create Application.
  4. Copy the Client ID Gitea displays.

Build & install

For Charlie (default — Client ID embedded in the binary):

cd ~/Dev/charlie/sherlock
go install ./cmd/gitea-mcp

That's it. No -ldflags, no flags at all. The Charlie Gitea OAuth2 app's Client ID lives in cmd/gitea-mcp/gitea_clientid_charlie.go and is baked in unless you build with -tags noembed.

For other deployments:

go build -tags noembed \
  -ldflags "-X main.giteaIssuer=https://gitea.example.com \
            -X main.giteaClientID=<CLIENT_ID>              \
            -X main.giteaBaseURL=https://gitea.example.com" \
  ./cmd/gitea-mcp

Modern Gitea (≥ 1.16-ish) accepts PKCE-alone for OAuth2 applications that have the "Confidential Client" checkbox unchecked, so no client secret is needed even though Gitea generates one in the UI.

Knobs

Variable / -X main.… Charlie default
giteaIssuer https://gitea.alexandru.macocian.me
giteaBaseURL https://gitea.alexandru.macocian.me
giteaClientID embedded; see gitea_clientid_charlie.go
giteaClientSecret "" (only needed for older / confidential Gitea apps)
Version 0.0.0-dev

When pointing at a different Gitea deployment, override giteaIssuer, giteaBaseURL, and giteaClientID together (with -tags noembed). Sherlock treats the wallet entry as service gitea regardless of which deployment the token came from, so don't mix.

If gitea-mcp --probe ever hits invalid_client or similar after you click Authorize in the browser, your Gitea is enforcing client auth on token exchange. Rebuild with -X main.giteaClientSecret=<the secret> to include it. The secret is baked into the local binary, never written to the source tree.

Verify (without an agent)

gitea-mcp --probe

First run: opens a browser, you click through Gitea's "Authorize sherlock" page (which itself goes through Authentik SSO if you're not already signed in), browser shows "Logged in. You may close this tab.", terminal prints:

OK: logged in to https://gitea.alexandru.macocian.me as <login> <<email>>

Subsequent runs are silent until the refresh window opens.

To force a re-login: sherlock logout gitea.

Use with an agent

sherlock copilot (or sherlock claude) automatically renders an MCP config that lists gitea-mcp. Copilot spawns it as a stdio subprocess; the first call into it triggers the OAuth flow described above (browser pops while you're in the middle of a chat — accept once, never again).

Tools exposed (all read-only; write tools land in a follow-up):

User

Tool Description
whoami Authenticated Gitea user.

Repos & content

Tool Description
list_repos Search/list repos accessible to the user.
get_file Read file contents (≤ 256 KiB, truncated tail).
list_dir List one directory level at a given ref.
get_tree Recursive listing of files/dirs (≤ 2000 entries) at a given ref.
file_history Commits touching a specific file.
list_branches Branches of a repo.
get_branch One branch's details + tip SHA.
list_tags Tags of a repo.
get_tag One tag's details.

Commits

Tool Description
list_commits Repo commit history with date / SHA filters.
get_commit Single commit by SHA (full message, parents).
get_commit_status Combined CI / status check state for a ref.

Issues

Tool Description
list_issues Issues with state/labels/type filters.
get_issue One issue or PR by index.
list_issue_comments Comments on a single issue / PR.

Pull requests

Tool Description
list_pulls PRs with state/sort/labels filters.
get_pull Full PR details incl. requested reviewers.
list_pull_files Files changed by a PR (counts; no diffs).
list_pull_reviews Reviews on a PR (state, body, reviewer).

Releases

Tool Description
list_releases Releases of a repo (drafts & pre-releases opt-in).
get_release One release by ID, incl. assets.

Actions (CI)

Tool Description
list_workflow_runs Per-repo pipeline runs.
list_workflow_jobs Jobs of a specific run.
get_job_logs Tail (100 KiB) of a job's log output.
list_org_workflow_runs All runs across an org's repos.
list_org_workflow_jobs All jobs across an org's runs.
list_org_runners Self-hosted runners registered to an org.
get_org_runner Details for a single org runner.

Packages

Tool Description
list_packages Packages owned by a user/org (type + name).

Wiki

Tool Description
list_wiki_pages Wiki page titles/slugs/last-update.
get_wiki_page One wiki page content.

Organisations

Tool Description
list_my_orgs Orgs the authenticated user is a member of.
get_org One org's details.
list_org_repos Repos owned by an org.
list_org_members Members of an org.
list_org_teams Teams of an org.
search_org_teams Search teams in an org by name substring.
list_org_activity_feed Recent activity feed of an org.

Write operations and admin-scoped tools (admin runners, admin workflow runs, admin org management) land in a separate gitea-mcp-admin MCP later so the per-tool permission surface stays small.

Troubleshooting

Symptom Cause
gitea-mcp: giteaClientID not configured. Built without -ldflags "-X main.giteaClientID=...". See above.
Browser opens, Gitea says "Client ID not registered" Either the OAuth2 application was never created in Gitea, or the Client ID was mistyped.
Browser opens, Gitea says "client_secret required" The OAuth2 application has "Confidential Client" checked. Edit it and uncheck.
gitea: 401 (token rejected ...) Token was revoked or the wallet has stale tokens for a different Gitea deployment. Run sherlock logout gitea and retry.
bind: address already in use A previous OAuth flow (or another MCP) still holds 127.0.0.1:6990. Wait a few seconds or kill the stuck process.
EOF
echo "gitea-mcp doc created"