dispatch#67 fix #2 needed a read-only CI credential and hit a hard wall: Gitea's own token-creation and admin-user endpoints reject token auth outright -- POST /users/{u}/tokens returns 'auth required', POST /admin/users returns 'token does not have at least one of required scope(s), required=[write:admin]' -- for ANY existing token, by design. An existing token cannot mint another token or create an account; the only way through either endpoint is real basic auth as an actual site admin. That is exactly what a sandboxed agent session cannot safely hold. internal/adminbootstrap: the core bootstrap, HTTP-only, fully testable against a fake transport (5 tests, no network). Given real admin username/password, it creates the target account (or resets its password if it already exists -- deliberately unconditional, gated behind a required ConfirmServiceAccount flag since that reset is only safe for a dedicated bot account), mints it a scoped token by authenticating AS that account (Gitea requires this specifically -- an admin cannot mint on another account's behalf), grants it read-only collaborator access on named repos, and stores the result via a SecretStore -- never returning the token or either password to the caller. internal/localtools: the MCP tool wrapper (mint_service_account_token) and the real op-CLI-backed SecretStore (token passed via stdin, never argv, per the estate's own secret-handling rule). Deliberately its OWN package, not internal/tools -- this capability is far too dangerous to risk it ever being wired into RegisterAll and reachable from the shared HTTP gitea-mcp server, even behind OIDC. internal/mcp/stdio.go: a stdio JSON-RPC transport (one message per line, standard MCP stdio framing) for the existing registry.Tool interface -- the only transport gitea-mcp had before this was HTTP (server.go), which a sandboxed session can reach but a real admin password never should be piped through. cmd/gitea-admin-mcp: the binary. Meant to be spawned by a LOCAL Claude Code session (.mcp.json stdio server) or run by hand, via 'op run --env-file ~/.op-env' so admin credentials arrive just-in-time and never touch a sandboxed session. 11 new tests total, all passing under -race. Nothing in cmd/gitea-mcp (the shared HTTP server) changed.
3.3 KiB
gitea-admin-mcp
A separate, local-only stdio MCP server. One tool: mint_service_account_token.
Why this exists
dispatch#67 fix #2 needed a read-only CI credential for a dedicated service
account, and hit a hard wall: Gitea's own POST /users/{u}/tokens and
POST /admin/users both reject token auth outright —
{"message":"auth required"}
{"message":"token does not have at least one of required scope(s), required=[write:admin], ..."}
— by design: an existing API token cannot mint another token, or create an
account, no matter its scope. The only way through either endpoint is real
basic auth as an actual site admin. A sandboxed agent session has no
interactive password and shouldn't be handed one — that's broader exposure
than the job needs. This binary is meant to run where real admin credentials
already exist just-in-time: your own machine, via op run.
What it does
mint_service_account_token does the whole bootstrap in one call:
- Creates the named account if it doesn't exist (admin basic auth), or
resets its password if it does. This is why
confirm_service_accountis required and checked first — the tool unconditionally (re)sets whatever password the target account has, which is correct for a dedicated bot account and would be destructive pointed at a real person's login by mistake. - Mints a scoped token for that account, authenticating as the account itself (Gitea requires this specifically — an admin cannot mint a token on another account's behalf).
- Grants the account read-only collaborator access on any named
owner/repotargets (admin action, least privilege). - Stores the token in 1Password via
op item create, value passed on stdin — never as a literal argv element, matching the estate's own secret-handling rule. Returns only theop://reference, never the token value itself, to the caller.
Setup
Deliberately not wired into cmd/gitea-mcp (the shared HTTP server) —
this capability is too dangerous to expose over the network, even behind
OIDC. internal/localtools is a separate package specifically so it can
never be wired into that server's RegisterAll by accident.
Build:
go build -o gitea-admin-mcp ./cmd/gitea-admin-mcp
Configure as a local stdio MCP server (Claude Code .mcp.json, run where
your real op session lives):
{
"mcpServers": {
"gitea-admin": {
"command": "op",
"args": ["run", "--env-file", "~/.op-env", "--", "/path/to/gitea-admin-mcp"]
}
}
}
Required env (see ~/.op-env):
| Var | Meaning |
|---|---|
GITEA_BASE_URL |
e.g. https://git.d-ma.be |
GITEA_ADMIN_USERNAME |
a real site-admin account |
GITEA_ADMIN_PASSWORD |
that account's real password |
OP_VAULT |
1Password vault for minted tokens (default AgentSecrets, per the estate's agent-governed ops-secrets policy) |
Example call
{
"username": "dispatch-bot",
"email": "dispatch@d-ma.be",
"scopes": ["read:repository", "read:issue"],
"token_name": "dispatch-ci-readonly",
"secret_name": "gitea-dispatch-bot-ci-readonly",
"grant_read_repos": ["mathias/cobalt-dingo"],
"confirm_service_account": true
}
Result carries SecretRef (e.g. op://AgentSecrets/gitea-dispatch-bot-ci-readonly/credential)
and GrantedRepos — never the token or either password.