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.
245 lines
19 KiB
Markdown
245 lines
19 KiB
Markdown
# 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_<domain>_<action>`), 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.
|