# 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: 1. Creates the named account if it doesn't exist (admin basic auth), or resets its password if it does. **This is why `confirm_service_account` is 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. 2. 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). 3. Grants the account read-only collaborator access on any named `owner/repo` targets (admin action, least privilege). 4. 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 the `op://` 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): ```json { "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 ```json { "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.