Files
AegisGitea-MCP/docs/architecture.md
T
Latte b275f5c0c2
test / test (push) Has been cancelled
lint / lint (push) Has been cancelled
docker / test (pull_request) Successful in 13s
docker / lint (pull_request) Successful in 2m3s
lint / lint (pull_request) Successful in 16s
test / test (pull_request) Successful in 14s
docker / docker-test (pull_request) Successful in 42s
docker / docker-publish (pull_request) Has been skipped
docs: retarget setup to Claude connectors
2026-06-13 21:05:21 +02:00

176 lines
7.6 KiB
Markdown

# Architecture
## Overview
AegisGitea MCP is a Python 3.10+ application built on **FastAPI**. It acts as a bridge between an AI client (such as Claude, Claude Code, or Cowork) and a self-hosted Gitea instance, implementing the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
```
AI Client (Claude / Claude Code / Cowork)
│ HTTP (Authorization: Bearer <key>)
┌────────────────────────────────────────────┐
│ FastAPI Server │
│ server.py │
│ - Route: GET/POST /mcp │
│ - Route: POST /mcp/tool/call │
│ - Route: GET /mcp/tools │
│ - Route: GET /health │
│ - Streamable HTTP transport │
│ - Legacy SSE alias (GET/POST /mcp/sse) │
└───────┬───────────────────┬────────────────┘
│ │
┌────▼────┐ ┌────▼──────────────┐
│ auth │ │ Tool dispatcher │
│ auth.py│ │ (server.py) │
└────┬────┘ └────────┬──────────┘
│ │
│ ┌────────▼──────────┐
│ │ Tool handlers │
│ │ tools/repo.py │
│ └────────┬──────────┘
│ │
│ ┌────────▼──────────┐
│ │ GiteaClient │
│ │ gitea_client.py │
│ └────────┬──────────┘
│ │ HTTPS
│ ▼
│ Gitea instance
┌────▼────────────────────┐
│ AuditLogger │
│ audit.py │
│ /var/log/aegis-mcp/ │
│ audit.log │
└─────────────────────────┘
```
---
## Source Modules
### `server.py`
The entry point and FastAPI application. Responsibilities:
- Defines all HTTP routes
- Reads configuration on startup and initialises `GiteaClient`
- Applies authentication middleware to protected routes
- Dispatches tool calls to the appropriate handler function
- Handles CORS
### `auth.py`
API key validation. Responsibilities:
- `APIKeyValidator` class: holds the set of valid keys, tracks failed attempts per IP
- Constant-time comparison to prevent timing side-channels
- Rate limiting: blocks IPs that exceed `MAX_AUTH_FAILURES` within `AUTH_FAILURE_WINDOW`
- Helper functions for key generation and hashing
- Singleton pattern (`get_validator()`) with test-friendly reset (`reset_validator()`)
### `config.py`
Pydantic `BaseSettings` model. Responsibilities:
- Loads all configuration from environment variables or `.env`
- Validates values (log level enum, token format, key minimum length)
- Parses comma-separated `MCP_API_KEYS` into a list
- Exposes computed properties (e.g. base URL for Gitea API)
### `gitea_client.py`
Async HTTP client for the Gitea API. Responsibilities:
- Wraps `httpx.AsyncClient` with bearer token authentication
- Maps HTTP status codes to typed exceptions (`GiteaAuthenticationError`, `GiteaNotFoundError`, etc.)
- Enforces file size limit before returning file contents
- Logs all API calls to the audit logger
Key methods:
| Method | Gitea endpoint |
|---|---|
| `get_current_user()` | `GET /api/v1/user` |
| `list_repositories()` | `GET /api/v1/user/repos` |
| `get_repository()` | `GET /api/v1/repos/{owner}/{repo}` |
| `get_file_contents()` | `GET /api/v1/repos/{owner}/{repo}/contents/{path}` |
| `get_tree()` | `GET /api/v1/repos/{owner}/{repo}/git/trees/{ref}` |
### `audit.py`
Structured audit logging using `structlog`. Responsibilities:
- Initialises a `structlog` logger writing JSON to the configured log file
- `log_tool_invocation()`: records tool calls with result and correlation ID
- `log_access_denied()`: records failed authentication
- `log_security_event()`: records rate limit triggers and other security events
- Auto-generates UUID correlation IDs when none is provided
### `mcp_protocol.py`
MCP data models and tool registry. Responsibilities:
- Pydantic models: `MCPTool`, `MCPToolCallRequest`, `MCPToolCallResponse`, `MCPListToolsResponse`
- `AVAILABLE_TOOLS` list: the canonical list of tools exposed to clients
- `get_tool_by_name()`: lookup helper used by the dispatcher
### `tools/repository.py`
Concrete tool handler functions. Responsibilities:
- `list_repositories_tool()`: calls `GiteaClient.list_repositories()`, formats the result
- `get_repository_info_tool()`: calls `GiteaClient.get_repository()`, formats metadata
- `get_file_tree_tool()`: calls `GiteaClient.get_tree()`, flattens to a list of paths
- `get_file_contents_tool()`: calls `GiteaClient.get_file_contents()`, decodes base64
All handlers return a plain string. `server.py` wraps this in an `MCPToolCallResponse`.
---
## Request Lifecycle
```
1. Client sends POST /mcp/tool/call
2. FastAPI routes the request to the tool-call handler in server.py
3. OAuth middleware validates the Bearer token via Gitea OIDC/JWKS or userinfo
├── Fail → AuditLogger.log_access_denied() → HTTP 401 / 429
└── Pass → continue
4. AuditLogger.log_tool_invocation(status="pending")
5. Tool dispatcher looks up the tool by name (mcp_protocol.get_tool_by_name)
6. Policy engine checks read/write mode and repository/path policy
7. If GITEA_TOKEN is configured, service-PAT authz checks
GET /repos/{owner}/{repo}/collaborators/{user}/permission
8. Tool handler function (tools/repository.py) is called
9. GiteaClient makes an async HTTP call to the Gitea API
10. Result (or error) is returned to server.py
11. AuditLogger.log_tool_invocation(status="success" | "error")
12. MCPToolCallResponse is returned to the client
```
---
## Key Design Decisions
**Read by default, writes opt-in.** Read tools are available by default. Write-capable tools require `WRITE_MODE=true`, repository write policy/whitelist approval, and `write:repository` authorization.
**Gitea controls repository access.** Without `GITEA_TOKEN`, Gitea enforces repository permissions on API calls made with the user's token. With `GITEA_TOKEN`, the service PAT can only execute after the server verifies the requesting user's actual repository permission through Gitea and writes an audit denial if the check fails.
**Public tool discovery.** `GET /mcp/tools` requires no authentication so that MCP clients can discover the available tools without credentials. All other endpoints require authentication.
**Minimal persisted state.** The audit log is persisted for tamper-evident review. Dynamic OAuth client registrations are persisted when DCR is enabled. Rate limit counters and short-lived authz caches are in-memory and reset on restart.
**Async throughout.** FastAPI + `httpx.AsyncClient` means all Gitea API calls are non-blocking, allowing the server to handle concurrent requests efficiently.