feat!: full-access Gitea MCP server (1.0.0): full API coverage, stdio + streamable HTTP, admin and per-user OAuth #71

Merged
Latte merged 70 commits from feat/full-api-coverage into dev 2026-10-14 17:25:25 +00:00
Owner

Rebuilds AegisGitea-MCP as a complete, full-access MCP bridge to the Gitea API. Breaking change: all tool names, the HTTP adapter, the YAML policy and API keys change (version 1.0.0.dev0). Design decisions are in docs/adr/0001-0006, the audit and chunk plan in docs/audit.md.

What changes

  • 538 tools: one per operation of the pinned Gitea 28.1.0 swagger (spec/gitea-swagger.v1.json), generated into catalog/tools.json (gitea_<domain>_<action>), plus gitea_api_request for endpoints newer than the spec. Everything is enabled by default, including admin, secrets, deletes and Sudo. READ_ONLY, TOOLSETS and DISABLED_TOOLS restrict exposure.
  • Official MCP Python SDK v2 (low-level Server, since FastMCP/MCPServer cannot take raw JSON schemas): stdio and streamable HTTP from one codebase, /health readiness endpoint.
  • Auth: admin-token mode (stdio, static bearer on HTTP, constant-time compare) and per-user OAuth (our server is the OAuth AS for MCP clients with dynamic client registration and a consent page, and a Gitea OAuth client upstream; calls run with the user's own Gitea token, identity in every result; encrypted token store). A non-loopback bind refuses to start without auth.
  • Executor: path encoding verified against the live instance, pagination metadata, binary/text handling, sanitized error mapping, exact secret redaction, untrusted-content labelling on every result.
  • aegis-gitea-mcp setup: verifies reachability, admin rights and token scopes, registers the OAuth app, writes .env (0600) without echoing secrets, prints client configs for Claude, Claude Code, Mistral Le Chat and local tools plus a compose and reverse-proxy snippet.
  • Removed: FastAPI adapter, hand-rolled JSON-RPC, YAML policy engine, resource-type gate, dead API keys, automation and metrics.

Verification

  • Unit: 1553 passed, coverage 98% (gate raised to 90%).
  • Live integration against a disposable digest-pinned Gitea 28.1.0: scenario tests (org, repo, issue, PR, review, merge, release, secrets, variables, webhooks, admin, Sudo, packages, a real Actions run via act_runner), a sweep calling all 538 tools against a pinned per-tool baseline (521 with a 2xx; the rest documented in docs/integration-coverage.md), per-user mode, the setup command.
  • MCP smoke tests over stdio, streamable HTTP and the built image with real SDK clients.
  • Security phase: threat model in docs/security.md, 19 review findings fixed or waived with reasons (docs/security-findings.md), dedicated tests/security suite; pip-audit, ruff S, gitleaks and Trivy clean.
  • CI: 8 jobs (gitleaks, lint, test-unit, test-integration, test-mcp, build-and-push, docs-check, security), green on the feature branch.

Known limitations and owner follow-ups

  • gitea_user_create_token, gitea_user_get_tokens, gitea_user_delete_access_token require basic auth with a password (Gitea rule) and return 401 through this server.
  • The tool list is large (538); for clients that struggle, use TOOLSETS.
  • Reserve the aegis-gitea-mcp name on PyPI; docs install from the Gitea package index.
  • Enable "require approval for workflows from forks" in the repository settings (docs/deployment.md).
Rebuilds AegisGitea-MCP as a complete, full-access MCP bridge to the Gitea API. Breaking change: all tool names, the HTTP adapter, the YAML policy and API keys change (version 1.0.0.dev0). Design decisions are in docs/adr/0001-0006, the audit and chunk plan in docs/audit.md. ## What changes - 538 tools: one per operation of the pinned Gitea 28.1.0 swagger (`spec/gitea-swagger.v1.json`), generated into `catalog/tools.json` (`gitea_<domain>_<action>`), plus `gitea_api_request` for endpoints newer than the spec. Everything is enabled by default, including admin, secrets, deletes and Sudo. `READ_ONLY`, `TOOLSETS` and `DISABLED_TOOLS` restrict exposure. - Official MCP Python SDK v2 (low-level `Server`, since FastMCP/`MCPServer` cannot take raw JSON schemas): stdio and streamable HTTP from one codebase, `/health` readiness endpoint. - Auth: admin-token mode (stdio, static bearer on HTTP, constant-time compare) and per-user OAuth (our server is the OAuth AS for MCP clients with dynamic client registration and a consent page, and a Gitea OAuth client upstream; calls run with the user's own Gitea token, identity in every result; encrypted token store). A non-loopback bind refuses to start without auth. - Executor: path encoding verified against the live instance, pagination metadata, binary/text handling, sanitized error mapping, exact secret redaction, untrusted-content labelling on every result. - `aegis-gitea-mcp setup`: verifies reachability, admin rights and token scopes, registers the OAuth app, writes `.env` (0600) without echoing secrets, prints client configs for Claude, Claude Code, Mistral Le Chat and local tools plus a compose and reverse-proxy snippet. - Removed: FastAPI adapter, hand-rolled JSON-RPC, YAML policy engine, resource-type gate, dead API keys, automation and metrics. ## Verification - Unit: 1553 passed, coverage 98% (gate raised to 90%). - Live integration against a disposable digest-pinned Gitea 28.1.0: scenario tests (org, repo, issue, PR, review, merge, release, secrets, variables, webhooks, admin, Sudo, packages, a real Actions run via act_runner), a sweep calling all 538 tools against a pinned per-tool baseline (521 with a 2xx; the rest documented in docs/integration-coverage.md), per-user mode, the setup command. - MCP smoke tests over stdio, streamable HTTP and the built image with real SDK clients. - Security phase: threat model in docs/security.md, 19 review findings fixed or waived with reasons (docs/security-findings.md), dedicated `tests/security` suite; pip-audit, ruff S, gitleaks and Trivy clean. - CI: 8 jobs (gitleaks, lint, test-unit, test-integration, test-mcp, build-and-push, docs-check, security), green on the feature branch. ## Known limitations and owner follow-ups - `gitea_user_create_token`, `gitea_user_get_tokens`, `gitea_user_delete_access_token` require basic auth with a password (Gitea rule) and return 401 through this server. - The tool list is large (538); for clients that struggle, use `TOOLSETS`. - Reserve the `aegis-gitea-mcp` name on PyPI; docs install from the Gitea package index. - Enable "require approval for workflows from forks" in the repository settings (docs/deployment.md).
Latte added 70 commits 2026-10-14 16:25:51 +00:00
Inventory and gap analysis of the 0.2.0 codebase against the full Gitea API
and the official gitea-mcp toolset, the B1..B11 + security chunk plan, ADRs
0001-0006 for the full-access rebuild, SDK/OAuth research notes, and the
instance swagger pinned for tool generation. The audit-logging doc moves to
docs/audit-logging.md.
Removes the FastAPI server, hand-rolled MCP protocol, tool registry and
tools, YAML policy, resource-type authz, API-key and OAuth proxy modules,
automation, observability, cache, the old Gitea client and stdio adapter,
the key-management scripts and their tests (ADR 0006).
Adds the pydantic-settings Settings with validated GITEA_URL, OAuth scopes
and toolsets, the argparse CLI skeleton, catalog.model, identity and
GiteaAPIError. Test fixtures now isolate env, cwd and state paths in tmp.
Verifies the Gitea instance, token and scopes, registers or reuses the aegis-gitea-mcp OAuth app, generates missing secrets and writes a 0600 env file from the template. Prints client configs and deployment snippets without any secret value.
Adds scripts/generate_catalog.py (with --check), the naming rules, schema conversion, the committed tools.json for all 537 operations and the load_catalog() loader.
Replace lint/test/docker workflows with ci.yml (gitleaks, lint, test-unit, test-integration, test-mcp, build-and-push, docs-check, security). Rebuild the Dockerfile as a digest-pinned multi-stage uv build running as uid 1000, add .dockerignore, a hardened compose file and a gitleaks config. The integration harness now falls back to /etc/hostname and /proc/self to find its own container.
Slash safety is decided per (path template, parameter) so single-segment
routes such as releases/tags/{tag} and editorconfig/{filepath} keep %2F.
Adds authorization_header, remote_password, mirror_password, mirror_token and
the AWS key fields to the redacted request fields. Deeply nested JSON from
Gitea is treated as text instead of raising RecursionError, and base64
uploads are capped before decoding.
Require consent before sending the token over plain http to a non-loopback
host, strip control characters from server-supplied text, restrict env values
and tokens to printable ASCII, validate the URLs like other env values and
fsync the directory after the atomic replace.
Low-level SDK server built from the catalog plus gitea_api_request, with TOOLSETS/READ_ONLY/DISABLED_TOOLS filters, schema validation without value echo, one audit event per call, static bearer verifier, identity resolver hook, /health, DNS-rebinding protection, rate limiting and fail-closed startup checks.
docs: regenerate tool inventory after annotation overrides
ci / gitleaks (push) Successful in 8s
ci / docs-check (push) Failing after 35s
ci / lint (push) Successful in 47s
ci / test-mcp (push) Failing after 48s
ci / test-integration (push) Successful in 52s
ci / security (push) Failing after 29s
ci / test-unit (push) Successful in 1m57s
ci / build-and-push (push) Successful in 18s
e139ae826f
The audit logger is opened at startup (exit 2 on failure) and obtained before
every call, which is refused with audit_unavailable otherwise. Audit records only
take validated path and sudo values and use the request id as correlation id. The
rate limiter evicts idle keys, caps tracked keys, audits the first excess per key
and window and uses the settings passed to the app. FORWARDED_ALLOW_IPS is now a
setting, and startup warns about a missing host allow-list and unavailable OAuth.
test: add MCP smoke tests over stdio and streamable HTTP, from source and from the built image
ci / lint (push) Successful in 41s
ci / security (push) Failing after 28s
ci / build-and-push (push) Successful in 17s
ci / gitleaks (push) Successful in 9s
ci / docs-check (push) Failing after 30s
ci / test-integration (push) Successful in 46s
ci / test-mcp (push) Successful in 1m16s
ci / test-unit (push) Successful in 1m52s
5e144772b2
Authorization server for MCP clients (SDK provider with dynamic client
registration, PKCE, refresh rotation and revocation) that signs users in
through Gitea and runs tool calls with the user's own Gitea token.

- consent page with CSRF protection and a browser-binding cookie on the
  Gitea callback (confused-deputy protection for the shared Gitea app)
- encrypted token store (Fernet, HKDF-derived key, 0600, atomic writes,
  symlink and corrupt-file refusal); our tokens stored only as hashes
- tokens bound to the MCP endpoint resource and the gitea scope; the
  static bearer keeps working next to OAuth
- code replay and refresh token reuse revoke the grant
- automatic upstream refresh with per-session locking; invalid_grant
  ends the session and never falls back to the admin token
- OAuth-only deployments (no GITEA_TOKEN, no bearer) may start
- rate limit on the OAuth endpoints, query strings dropped from access logs
fix: redact identity secrets through a public hook and mask issued agm_ tokens
ci / gitleaks (push) Successful in 8s
ci / docs-check (push) Failing after 30s
ci / lint (push) Successful in 42s
ci / test-integration (push) Successful in 46s
ci / security (push) Failing after 27s
ci / test-mcp (push) Successful in 1m16s
ci / test-unit (push) Successful in 2m17s
ci / build-and-push (push) Successful in 17s
ce5ae6caa5
Identity gains a redact field that the executor honours, so the OAuth
resolver no longer needs an executor subclass overriding private
internals. Opaque agm_ tokens issued by our authorization server are now
masked by the secret patterns wherever they appear in content.
test: sweep checks the audit trail and keeps the unit test free of em dashes
ci / gitleaks (push) Successful in 8s
ci / docs-check (push) Successful in 32s
ci / lint (push) Successful in 44s
ci / test-mcp (push) Successful in 1m19s
ci / security (push) Successful in 1m33s
ci / test-integration (push) Successful in 2m30s
ci / test-unit (push) Successful in 3m45s
ci / build-and-push (push) Successful in 40s
8786502b8c
The --env option swallowed the server name, so the stdio command is reordered. uvx now always
names the project registry because the package name is not registered on public PyPI.
README gains a Known limitations section, the READ_ONLY wording is corrected, packaging.md
warns about --extra-index-url and the Le Chat admin requirement is mentioned.
ci: keep the publish workflow comment free of em dashes
ci / gitleaks (push) Successful in 9s
ci / docs-check (push) Successful in 32s
ci / lint (push) Successful in 42s
ci / test-mcp (push) Successful in 1m20s
ci / security (push) Successful in 1m34s
ci / test-integration (push) Successful in 2m38s
ci / test-unit (push) Successful in 3m48s
ci / build-and-push (push) Successful in 35s
ci / gitleaks (pull_request) Successful in 7s
ci / lint (pull_request) Successful in 37s
ci / test-unit (pull_request) Successful in 3m39s
ci / test-integration (pull_request) Successful in 2m47s
ci / test-mcp (pull_request) Successful in 1m5s
ci / docs-check (pull_request) Successful in 27s
ci / security (pull_request) Successful in 1m31s
ci / build-and-push (pull_request) Successful in 29s
3fead4e121
Latte merged commit a4800951ef into dev 2026-10-14 17:25:25 +00:00
Latte deleted branch feat/full-api-coverage 2026-10-14 17:25:26 +00:00
Sign in to join this conversation.