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.
19 KiB
Codebase audit and chunk plan
Date: 2026-10-14. Baseline: dev at 2b99644 (package version 0.2.0).
Target: a complete, full-access Gitea MCP server covering the entire instance
API (Gitea 28.1.0 swagger, pinned at spec/gitea-swagger.v1.json), served over
stdio and streamable HTTP, with admin-token and per-user OAuth modes.
Supporting research (SDK signatures, Gitea OAuth facts, client requirements, all with source URLs): research/sdk-oauth-gitea.md. Design decisions: adr/.
1. Inventory of the current codebase
1.1 MCP tools (44)
28 read tools, 15 write tools, 1 hybrid escape hatch. Definitions live in
mcp_protocol.py (AVAILABLE_TOOLS), handlers in registry.py and tools/*,
HTTP calls in gitea_client.py.
| Group | Tools | Gitea endpoints |
|---|---|---|
| Repository read | list_repositories, get_repository_info, get_file_tree, get_file_contents, search_code, list_commits, get_commit_diff, compare_refs, list_branches, get_branch, list_tags, get_repo_languages, list_repo_topics, get_commit_status |
/user/repos, /repos/search, R, R/git/trees, R/contents, R/search (does not exist in the swagger), R/commits, R/git/commits, R/compare, R/branches, R/tags, R/languages, R/topics, R/commits/{sha}/status |
| Issues / PRs read | list_issues, get_issue, list_issue_comments, list_labels, list_milestones, list_pull_requests, get_pull_request, list_pull_request_files, list_pull_request_commits |
R/issues*, R/labels, R/milestones, R/pulls* |
| Releases / orgs read | list_releases, get_release, get_latest_release, list_organizations, list_org_repositories |
R/releases*, /user/orgs, /orgs/{org}/repos |
| Write | create_issue, update_issue, create_issue_comment, create_pr_comment, edit_issue_comment, add_labels, remove_labels, assign_issue, create_label, update_label, create_milestone, create_pull_request, create_release, edit_release, create_branch |
R/issues*, R/labels*, R/milestones, R/pulls, R/releases*, R/branches |
| Escape hatch | gitea_request |
any known prefix, admin/credential segments denied |
(R = /repos/{owner}/{repo}.)
1.2 Transports
- stdio (
stdio_app.py) is the only part using themcpSDK, via the v1 low-level decorators (@server.list_tools()). The dependency is unbounded (mcp>=1.2.0), resolves tomcp==2.3.0, and under 2.xbuild_server()raisesAttributeError. The stdio adapter is broken today. - HTTP (
server.py, FastAPI) is hand-rolled: a minimal JSON-RPC dispatcher onPOST /mcpwith a fixedprotocolVersion: "2024-11-05", no session id, no streamed responses, errors as JSON-RPC errors rather thanisErrorresults.GET /mcpis a heartbeat-only SSE stream. The README calls this "Streamable HTTP"; it is not a conforming implementation. - Extra routes:
/,/health,/metrics(unauthenticated), OAuth metadata and proxy routes,/register(public, unthrottled, writes to disk on every call),/automation/*,/mcp/tools(public),/mcp/tool/call, plus FastAPI's/docs,/redoc,/openapi.json(public).
1.3 Configuration
config.py (pydantic-settings) has about 40 variables. Security defaults:
WRITE_MODE=false, RAW_API_ALLOW_SENSITIVE=false, MCP_HOST=127.0.0.1
(only the literal 0.0.0.0 is blocked), EXPOSE_ERROR_DETAILS=false,
SECRET_DETECTION_MODE=mask. AUTH_ENABLED, MCP_API_KEYS and OAUTH_MODE
only affect config validation; runtime auth is always Gitea-OAuth validation.
AUTOMATION_SCHEDULER_ENABLED is never read.
1.4 Authentication
auth.py(static API keys) is dead at runtime:MCP_API_KEYSis never compared against anything. The key-management scripts manage keys the server ignores.oauth.pyvalidates bearer tokens as Gitea OAuth tokens (JWT via JWKS, else/login/oauth/userinfo).oauth_flow.pyproxies the authorization code flow (PKCE S256, HMAC-signed state) and runs a small DCR registry on disk. It does not check theredirect_uriagainst the registered client's own redirect URIs.- Every call runs on a single service PAT (
GITEA_TOKEN); per-user permissions are emulated with a collaborator-permission probe (server.py) and a resource-type gate (authz.py). The user's own Gitea token is never used for API calls.
1.5 Supporting modules
| Module | Role | Fate under full access |
|---|---|---|
audit.py |
Hash-chained JSONL audit log | Keep. About 38 client methods log only a "pending" entry; the new dispatcher logs one complete event per call. |
security.py |
Regex secret detection, mask/block | Keep as output safety net. |
response_limits.py |
Item and character caps | Keep. |
request_context.py, errors.py, logging_utils.py, cache.py |
Context vars, ToolError, JSON logging, TTL cache |
Keep or fold into the new core. |
policy.py |
YAML allow/deny plus the write gate | Retire (ADR 0006). Replaced by READ_ONLY, TOOLSETS, DISABLED_TOOLS. |
authz.py |
Fail-closed emulation of per-user rights on a service PAT | Retire. Per-user mode now uses the user's own Gitea token, so Gitea enforces permissions itself. |
auth.py, oauth.py, oauth_flow.py |
Dead API keys, Gitea token validation, hand-rolled OAuth proxy | Replace with the SDK auth stack (ADR 0004). |
server.py, server_entry.py, mcp_protocol.py, registry.py, tools/* |
FastAPI adapter, custom protocol, 44 hand-written tools | Replace with the generated catalog and the SDK server (ADR 0002, 0003). |
automation.py |
Webhook ingest, placeholder jobs (45% covered) | Remove; out of scope for an API bridge. |
observability.py, rate_limit.py |
Unauthenticated /metrics, in-memory rate limits |
Rate limiting kept for HTTP; /metrics removed (not in scope). |
1.6 Tests
351 tests in 21 files, Gitea always mocked with hand-made stubs (pytest-httpx
is installed but unused). Full run on this machine: 336 passed, 15 failed,
coverage 80.10%. Failures: 12 environment-dependent (default paths under
/var/log and /var/lib, one test module shadows the reset_globals
fixture), 3 real (mcp 2.x API break in the stdio tests). No live-Gitea
integration tests, no MCP transport tests over HTTP.
1.7 CI (.gitea/workflows/)
lint.yml (ruff, black, mypy), test.yml (pytest at 80%, uv build package
check), docker.yml (lint + test again, build, import smoke test, push to the
Gitea registry on main/dev), publish.yml (lint + test again, uv publish to
the Gitea PyPI registry on push to dev/main). Python 3.12 only, unpinned
pip install -r requirements-dev.txt. No secret scan, dependency audit,
security lint, integration tests or docs drift check. docker.yml runs
docker image prune and docker builder prune on a shared host.
1.8 Dependencies and packaging
pyproject.toml core: httpx, pydantic, pydantic-settings, PyYAML,
python-dotenv, structlog (never imported), mcp (unbounded). [server] extra:
fastapi, uvicorn, PyJWT, python-multipart. Dev: pytest stack, black, ruff,
mypy, pre-commit. No lockfile. The Dockerfile pip-installs requirements.txt
without installing the package itself and sets a stray NODE_ENV. The compose
file keeps MCP_HOST=127.0.0.1 inside the container, so the published port is
unreachable unless the bind is widened.
1.9 Dead code and stale docs
- Code:
auth.py,AUTOMATION_SCHEDULER_ENABLED,observability.ToolTiming,arguments.RAW_API_METHODS,raw_method_is_write, most oftools/__init__.pyre-exports,structlog,pytest-httpx, the API-key scripts.search_codetargets a non-existent route.list_repositoriesis capped at 50 results. - Docs:
PLAN.mdphases 7 and 8 unchecked though done;CLAUDE.mdsays publish runs on av*tag (it runs on push to dev/main); API keys are documented as working in four docs;write-mode.md,roadmap.mdandtodo.mdclaim merge and content writes are impossible whilegitea_requestcan do them; the transport is described as streamable HTTP.
2. Gap analysis
2.1 Against the instance API (Gitea 28.1.0, 537 operations)
The existing tools cover 44 operations (about 8%). Every domain below is missing entirely or partially. Counts use the domain split of ADR 0003.
| Domain | Operations | Covered today | Missing highlights |
|---|---|---|---|
| repo | 120 | ~17 | CRUD, contents writes, forks, mirrors, transfer, collaborators, topics writes, tags/refs writes, archives, LFS, git notes, templates, avatars, push mirrors, branch protection |
| issue | 90 | ~15 | comments delete, reactions, pins, subscriptions, time tracking, stopwatches, dependencies, attachments, label/milestone delete, issue templates, search |
| user | 73 | 0 | profile, emails, SSH/GPG keys, follow, blocks, tokens, OAuth apps, starred/watched, settings, searches |
| actions | 70 | 0 | workflow runs/jobs/logs/artifacts, dispatch, repo/org/user secrets and variables, runners and registration tokens |
| org | 36 | 2 | CRUD, members, public members, blocks, labels, avatars, activity |
| admin | 33 | 0 | users (create/edit/delete/rename, keys, orgs, repos, badges), orgs, emails, cron list/run, system hooks, unadopted repos, runners, packages |
| pull | 26 | 4 | update, merge, reviews (create/submit/dismiss/delete), review comments, requested reviewers, diff/patch, update branch |
| hook | 20 | 0 | repo, org, user and system webhooks |
| misc | 19 | 0 | version, settings, markdown/markup, licenses, gitignore, labels templates, nodeinfo, signing keys, activitypub |
| team | 15 | 0 | team CRUD, members, repos |
| release | 13 | 4 | delete, by tag, attachments |
| package | 9 | 0 | list, get, delete, files, link/unlink |
| notification | 7 | 0 | list, mark read, threads |
| wiki | 6 | 0 | pages CRUD, revisions |
Swagger operations that the API does not offer and therefore cannot get a tool: queue management, instance configuration writes, and instance-wide OAuth2 applications (UI only). This is recorded honestly in the README rather than faked with UI scraping.
2.2 Against the official gitea/gitea-mcp (v1.8.1, 59 tools)
gitea-mcp consolidates operations into *_read/*_write tools with a method
enum. Every one of its methods maps to a swagger operation, so one tool per
operation is a strict superset. gitea-mcp features we must match or exceed:
read-only mode, toolset and tool-name filters, readOnlyHint/destructiveHint
annotations, a health endpoint for HTTP. Features it lacks that we add: Sudo,
admin domain, hooks, packages beyond delete, users/orgs administration,
per-user OAuth with dynamic client registration (needed for Mistral Le Chat).
2.3 Platform gaps
- Conforming streamable HTTP transport (current one is hand-rolled).
- Working stdio on
mcp2.x. - Static bearer auth for the MCP endpoint (the existing API keys are dead).
- Per-user OAuth where calls actually run under the user's Gitea identity.
- Setup command, live-Gitea integration tests, MCP-level smoke tests, docs drift check, security scanning, gitleaks.
uvlockfile, ruff format instead of black, coverage at 90%.
3. Chunk plan
Every chunk ends with: tests green, ruff check, ruff format --check, mypy
clean, checker verdict (acceptance criteria + API docs + security regression
review), conventional commit on feat/full-api-coverage, PR into dev.
Dependency order: B1 first; B2, B3, B6, B7 in parallel; B4; then B5, B8, B9, B10, B11 in parallel; then the security phase (S1-S3).
| Chunk | Scope | Acceptance criteria |
|---|---|---|
| B1 Foundation | uv + uv.lock, mcp>=2.3,<3, ruff format (black removed), ruff S rules, new contract in AGENTS.md/CLAUDE.md, ADRs, delete legacy modules and their tests, interface skeletons (catalog/model.py, identity.py, new config.py). |
uv run pytest green on the remaining modules, lint clean, no import of removed modules anywhere. |
| B2 Catalog | scripts/generate_catalog.py builds catalog/tools.json from the pinned swagger: names, domains, JSON input schemas (additionalProperties: false, owner/repo patterns), annotations, content types. Loader in catalog/__init__.py. --check mode. |
All 537 operations present, names unique, match ^gitea_[a-z0-9_]+$, at most 64 chars; schemas validate as Draft 2020-12; unit tests for naming, schema building, ref resolution. |
| B3 Executor | gitea/executor.py: path templating with encoding, query/JSON/multipart (base64 files), Sudo header, pagination metadata (Link, X-Total-Count), text and binary responses, error mapping with sanitized messages, response limits, secret masking, untrusted-content envelope with identity. |
pytest-httpx unit tests for each branch, tokens never present in any error or result. |
| B4 Server core + transports | Low-level SDK Server built from the catalog: list_tools with TOOLSETS/READ_ONLY/DISABLED_TOOLS, call_tool with jsonschema validation, audit, executor, is_error results. stdio entry, streamable HTTP app with static bearer verifier, /health, bind fail-closed, gitea_api_request raw tool. |
In-memory client tests for list/call/validation/errors; HTTP tests for 401 without/with bad token, refusal to start on a non-loopback bind without auth. |
| B5 Per-user OAuth | OAuthAuthorizationServerProvider proxying Gitea (DCR on, PKCE to Gitea), /oauth/callback, encrypted token store (file, 0600), refresh handling, combined verifier (static bearer = admin identity, OAuth token = user identity), identity in result metadata. |
Unit tests with mocked Gitea OAuth endpoints: full code flow, refresh, expiry, revoke, store permissions and encryption, tokens never logged. |
| B6 Setup command | aegis-gitea-mcp setup: reachability, admin and token-scope check (GET /user, GET /token), OAuth app registration via POST /user/applications/oauth2, writes .env from .env.example (0600), prints client configs and a compose snippet, never echoes secrets. |
Unit tests with mocked Gitea; integration test against the CI instance (in B8). |
| B7 Integration harness | Pytest fixtures that start gitea/gitea:28.1.0 (digest-pinned) with unique container/network names and guaranteed cleanup, seed an admin user and a full-scope token, plus fake-data seed helpers. Marker integration, skipped unless enabled. |
Harness starts, seeds, tears down on this host and in CI; no bind mounts, no prune. |
| B8 Integration suite | Scenario tests (org, repo, issue, PR, review, merge, release, secret, variable, webhook, cleanup) and a sweep that calls every tool once with fixture-derived arguments, asserting either success or a documented expected status. Per-user mode via minted user tokens. Setup command against the live instance. | Every registered tool exercised; write tools have happy and failure paths; non-admin user gets Gitea's own 403 on admin tools; identity visible. |
| B9 MCP smoke tests | Real SDK clients over stdio (subprocess) and streamable HTTP (uvicorn, and the built image) listing tools and calling a read and a write tool. | Green locally and in CI. |
| B10 CI | .gitea/workflows/: gitleaks, lint, test-unit, test-integration, test-mcp, build-and-push (SHA tags, digest output), docs-check, security; strict per-run cleanup, no prune. Dockerfile on uv, non-root, no secrets in layers. |
All eight jobs green on the PR. |
| B11 Docs | README rewrite (honest scope, setup with time estimate, stdio and HTTP client configs for Claude, Claude Code, Mistral, local tools, token scopes, untrusted-content notice), generated tool inventory docs/tools.md with endpoint mapping, scripts/check_docs.py drift check, deployment and reverse-proxy guide. |
Docs check green; no stale claims left from section 1.9. |
| S1 Threat model | docs/security.md: assets, trust boundaries, threats, mitigations. |
Reviewed by the checker. |
| S2 Fixes and negative tests | Security test suite: auth negatives, bind refusal, token redaction in logs/errors/results, secrets never echoed, malformed input rejected, base URL validation. | All green. |
| S3 Scans | pip-audit, ruff S/bandit, gitleaks over history; findings fixed or waived with reasons in docs/security.md. |
Scans clean or waived. |
4. Risks and open questions
Owner decisions taken before execution (2026-10-14):
- Security contract reversed. All capabilities on by default; the owner restricts via configuration and token choice (ADR 0001).
- Framework. Rebuild on the official SDK (ADR 0002).
- Commit signing. Disabled by the owner on
dev; commits are unsigned. - Tool count. One tool per swagger operation (537 plus the raw request tool), all on by default, with a toolset filter (ADR 0003).
Open risks:
- Tool-list size. 538 tools is a large
tools/listpayload. Claude Code handles it with tool search; claude.ai and Mistral Le Chat may degrade or truncate. Mitigation:TOOLSETS,READ_ONLYandDISABLED_TOOLS; the README gives a recommended slim profile for Le Chat. Needs owner testing against the real clients. - "FastMCP" no longer exists. In
mcp2.x FastMCP was renamedMCPServer, and it cannot register tools from raw JSON schemas. We use the SDK's low-levelServer, which is the same package and transport stack (ADR 0002). - Gitea has no dynamic client registration. Per-user OAuth therefore needs our server to be the OAuth authorization server for MCP clients and a Gitea OAuth client upstream. The server holds per-user Gitea tokens, which makes the token store a high-value asset (encrypted at rest, ADR 0004).
- Gitea scope fallback. Gitea grants full access for unknown scope strings. The proxy sends a fixed, validated scope list.
- Literal "every endpoint gets an integration test". Some operations need state that is costly to build in CI: Actions runs need a registered runner executing a job, push mirrors and migrations need a reachable remote, package endpoints need an uploaded package, unadopted repositories need files on the Gitea volume. The sweep exercises every tool; where a full happy path is not reproducible in CI, the test asserts the documented Gitea response (for example 404 for a missing artifact) and the README marks it.
- Shared CI runner. Integration and MCP jobs need Docker from the job
container. Per-run unique names and
always()cleanup; the existingdocker image prune/builder prunesteps are removed. - Breaking change. All tool names change (
gitea_<domain>_<action>), the FastAPI server, YAML policy and API keys are removed. Version goes to 1.0.0. - Admin coverage limits. The API has no queue management or instance configuration write endpoints, and no instance-wide OAuth app endpoint; setup registers the OAuth app as a user application owned by the admin.