# 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](research/sdk-oauth-gitea.md). Design decisions: [adr/](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 the `mcp` SDK, via the v1 low-level decorators (`@server.list_tools()`). The dependency is unbounded (`mcp>=1.2.0`), resolves to `mcp==2.3.0`, and under 2.x `build_server()` raises `AttributeError`. The stdio adapter is broken today. - HTTP (`server.py`, FastAPI) is hand-rolled: a minimal JSON-RPC dispatcher on `POST /mcp` with a fixed `protocolVersion: "2024-11-05"`, no session id, no streamed responses, errors as JSON-RPC errors rather than `isError` results. `GET /mcp` is 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_KEYS` is never compared against anything. The key-management scripts manage keys the server ignores. - `oauth.py` validates bearer tokens as Gitea OAuth tokens (JWT via JWKS, else `/login/oauth/userinfo`). - `oauth_flow.py` proxies the authorization code flow (PKCE S256, HMAC-signed state) and runs a small DCR registry on disk. It does not check the `redirect_uri` against 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 of `tools/__init__.py` re-exports, `structlog`, `pytest-httpx`, the API-key scripts. `search_code` targets a non-existent route. `list_repositories` is capped at 50 results. - Docs: `PLAN.md` phases 7 and 8 unchecked though done; `CLAUDE.md` says publish runs on a `v*` tag (it runs on push to dev/main); API keys are documented as working in four docs; `write-mode.md`, `roadmap.md` and `todo.md` claim merge and content writes are impossible while `gitea_request` can 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 `mcp` 2.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. - `uv` lockfile, 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: 1. **Tool-list size.** 538 tools is a large `tools/list` payload. Claude Code handles it with tool search; claude.ai and Mistral Le Chat may degrade or truncate. Mitigation: `TOOLSETS`, `READ_ONLY` and `DISABLED_TOOLS`; the README gives a recommended slim profile for Le Chat. Needs owner testing against the real clients. 2. **"FastMCP" no longer exists.** In `mcp` 2.x FastMCP was renamed `MCPServer`, and it cannot register tools from raw JSON schemas. We use the SDK's low-level `Server`, which is the same package and transport stack (ADR 0002). 3. **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). 4. **Gitea scope fallback.** Gitea grants full access for unknown scope strings. The proxy sends a fixed, validated scope list. 5. **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. 6. **Shared CI runner.** Integration and MCP jobs need Docker from the job container. Per-run unique names and `always()` cleanup; the existing `docker image prune` / `builder prune` steps are removed. 7. **Breaking change.** All tool names change (`gitea__`), the FastAPI server, YAML policy and API keys are removed. Version goes to 1.0.0. 8. **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.