diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..9d160c6 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,255 @@ +# API Reference + +## HTTP Endpoints + +### `GET /` + +Returns basic server information. No authentication required. + +**Response** + +```json +{ + "name": "AegisGitea MCP", + "version": "0.1.0", + "status": "running" +} +``` + +--- + +### `GET /health` + +Health check endpoint. No authentication required. + +**Response** + +```json +{ + "status": "healthy", + "gitea_connected": true +} +``` + +Returns HTTP 200 when healthy. Returns HTTP 503 when Gitea is unreachable. + +--- + +### `GET /mcp/tools` + +Returns the list of available MCP tools. No authentication required (needed for ChatGPT tool discovery). + +**Response** + +```json +{ + "tools": [ + { + "name": "list_repositories", + "description": "...", + "inputSchema": { ... } + } + ] +} +``` + +--- + +### `POST /mcp/tool/call` + +Executes an MCP tool. **Authentication required.** + +**Request headers** + +``` +Authorization: Bearer +Content-Type: application/json +``` + +**Request body** + +```json +{ + "name": "", + "arguments": { ... } +} +``` + +**Response** + +```json +{ + "content": [ + { + "type": "text", + "text": "..." + } + ], + "isError": false +} +``` + +On error, `isError` is `true` and `text` contains the error message. + +--- + +### `GET /mcp/sse` + +Server-Sent Events stream endpoint. Authentication required. Used for streaming MCP sessions. + +--- + +### `POST /mcp/sse` + +Sends a client message over an active SSE session. Authentication required. + +--- + +## Authentication + +All authenticated endpoints require a bearer token: + +``` +Authorization: Bearer +``` + +Alternatively, the key can be passed as a query parameter (useful for tools that do not support custom headers): + +``` +GET /mcp/tool/call?api_key= +``` + +--- + +## MCP Tools + +### `list_repositories` + +Lists all Gitea repositories accessible to the bot user. + +**Arguments:** none + +**Example response text** + +``` +Found 3 repositories: + +1. myorg/backend - Backend API service [Python] ★ 42 +2. myorg/frontend - React frontend [TypeScript] ★ 18 +3. myorg/infra - Infrastructure as code [HCL] ★ 5 +``` + +--- + +### `get_repository_info` + +Returns metadata for a single repository. + +**Arguments** + +| Name | Type | Required | Description | +|---|---|---|---| +| `owner` | string | Yes | Repository owner (user or organisation) | +| `repo` | string | Yes | Repository name | + +**Example response text** + +``` +Repository: myorg/backend +Description: Backend API service +Language: Python +Stars: 42 +Forks: 3 +Default branch: main +Private: false +URL: https://gitea.example.com/myorg/backend +``` + +--- + +### `get_file_tree` + +Returns the file and directory structure of a repository. + +**Arguments** + +| Name | Type | Required | Default | Description | +|---|---|---|---|---| +| `owner` | string | Yes | — | Repository owner | +| `repo` | string | Yes | — | Repository name | +| `ref` | string | No | default branch | Branch, tag, or commit SHA | +| `recursive` | boolean | No | `false` | Recursively list all subdirectories | + +> **Note:** Recursive mode is disabled by default to limit response size. Enable with care on large repositories. + +**Example response text** + +``` +File tree for myorg/backend (ref: main): + +src/ +src/main.py +src/config.py +tests/ +tests/test_main.py +README.md +requirements.txt +``` + +--- + +### `get_file_contents` + +Returns the contents of a single file. + +**Arguments** + +| Name | Type | Required | Default | Description | +|---|---|---|---|---| +| `owner` | string | Yes | — | Repository owner | +| `repo` | string | Yes | — | Repository name | +| `filepath` | string | Yes | — | Path to the file within the repository | +| `ref` | string | No | default branch | Branch, tag, or commit SHA | + +**Limits** + +- Files larger than `MAX_FILE_SIZE_BYTES` (default 1 MB) are rejected. +- Binary files that cannot be decoded as UTF-8 are returned as raw base64. + +**Example response text** + +``` +Contents of myorg/backend/src/main.py (ref: main): + +import fastapi +... +``` + +--- + +## Error Responses + +All errors follow this structure: + +```json +{ + "content": [ + { + "type": "text", + "text": "Error: " + } + ], + "isError": true +} +``` + +Common error scenarios: + +| Scenario | HTTP Status | `isError` | +|---|---|---| +| Missing or invalid API key | 401 | — (rejected before tool runs) | +| Rate limited IP address | 429 | — | +| Tool not found | 404 | — | +| Repository not found in Gitea | 200 | `true` | +| File too large | 200 | `true` | +| Gitea API unavailable | 200 | `true` | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..f69fdfd --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,168 @@ +# Architecture + +## Overview + +AegisGitea MCP is a Python 3.10+ application built on **FastAPI**. It acts as a bridge between an AI client (such as ChatGPT) and a self-hosted Gitea instance, implementing the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). + +``` +AI Client (ChatGPT) + │ + │ HTTP (Authorization: Bearer ) + ▼ +┌────────────────────────────────────────────┐ +│ FastAPI Server │ +│ server.py │ +│ - Route: POST /mcp/tool/call │ +│ - Route: GET /mcp/tools │ +│ - Route: GET /health │ +│ - SSE support (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/repos/search` | +| `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. auth.validate_api_key() checks the Authorization header + ├── 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. Tool handler function (tools/repository.py) is called + │ +7. GiteaClient makes an async HTTP call to the Gitea API + │ +8. Result (or error) is returned to server.py + │ +9. AuditLogger.log_tool_invocation(status="success" | "error") + │ +10. MCPToolCallResponse is returned to the client +``` + +--- + +## Key Design Decisions + +**Read-only by design.** The MCP tools only read data from Gitea. No write operations are implemented. + +**Gitea controls access.** The server does not maintain its own repository ACL. The Gitea bot user's permissions are the source of truth. If the bot cannot access a repo, the server cannot either. + +**Public tool discovery.** `GET /mcp/tools` requires no authentication so that ChatGPT's plugin system can discover the available tools without credentials. All other endpoints require authentication. + +**Stateless server.** No database or persistent state beyond the audit log file. Rate limit counters 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. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..104f538 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,104 @@ +# Configuration + +All configuration is done through environment variables. Copy `.env.example` to `.env` and set the values before starting the server. + +```bash +cp .env.example .env +``` + +--- + +## Gitea Settings + +| Variable | Required | Default | Description | +|---|---|---|---| +| `GITEA_URL` | Yes | — | Base URL of your Gitea instance (e.g. `https://gitea.example.com`) | +| `GITEA_TOKEN` | Yes | — | API token of the Gitea bot user | + +The `GITEA_TOKEN` must be a token belonging to a user that has at least read access to all repositories you want the AI to access. The server validates the token on startup by calling the Gitea `/api/v1/user` endpoint. + +--- + +## MCP Server Settings + +| Variable | Required | Default | Description | +|---|---|---|---| +| `MCP_HOST` | No | `0.0.0.0` | Interface to bind to | +| `MCP_PORT` | No | `8080` | Port to listen on | +| `MCP_DOMAIN` | No | — | Public domain name (used for Traefik labels in Docker) | +| `LOG_LEVEL` | No | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | + +--- + +## Authentication Settings + +| Variable | Required | Default | Description | +|---|---|---|---| +| `AUTH_ENABLED` | No | `true` | Enable or disable API key authentication | +| `MCP_API_KEYS` | Yes (if auth enabled) | — | Comma-separated list of valid API keys | +| `MAX_AUTH_FAILURES` | No | `5` | Number of failed attempts before rate limiting an IP | +| `AUTH_FAILURE_WINDOW` | No | `300` | Time window in seconds for counting failures | + +### API Key Requirements + +- Minimum length: 32 characters +- Recommended: generate with `make generate-key` (produces 64-character hex keys) +- Multiple keys: separate with commas — useful during key rotation + +```env +# Single key +MCP_API_KEYS=abc123... + +# Multiple keys (grace period during rotation) +MCP_API_KEYS=newkey123...,oldkey456... +``` + +> **Warning:** Setting `AUTH_ENABLED=false` disables all authentication. Only do this in isolated development environments. + +--- + +## File Access Settings + +| Variable | Required | Default | Description | +|---|---|---|---| +| `MAX_FILE_SIZE_BYTES` | No | `1048576` | Maximum file size the server will return (bytes). Default: 1 MB | +| `REQUEST_TIMEOUT_SECONDS` | No | `30` | Timeout for upstream Gitea API calls (seconds) | + +--- + +## Audit Logging Settings + +| Variable | Required | Default | Description | +|---|---|---|---| +| `AUDIT_LOG_PATH` | No | `/var/log/aegis-mcp/audit.log` | Absolute path for the JSON audit log file | + +The directory is created automatically if it does not exist (requires write permission). + +--- + +## Full Example + +```env +# Gitea +GITEA_URL=https://gitea.example.com +GITEA_TOKEN=abcdef1234567890abcdef1234567890 + +# Server +MCP_HOST=0.0.0.0 +MCP_PORT=8080 +MCP_DOMAIN=mcp.example.com +LOG_LEVEL=INFO + +# Auth +AUTH_ENABLED=true +MCP_API_KEYS=a1b2c3d4e5f6...64chars +MAX_AUTH_FAILURES=5 +AUTH_FAILURE_WINDOW=300 + +# Limits +MAX_FILE_SIZE_BYTES=1048576 +REQUEST_TIMEOUT_SECONDS=30 + +# Audit +AUDIT_LOG_PATH=/var/log/aegis-mcp/audit.log +``` diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..fda84c6 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,126 @@ +# Deployment + +## Local / Development + +```bash +make install-dev +source venv/bin/activate # Linux/macOS +# venv\Scripts\activate # Windows + +cp .env.example .env +# Edit .env +make generate-key # Add key to .env +make run +``` + +The server listens on `http://0.0.0.0:8080` by default. + +--- + +## Docker + +### Build + +```bash +make docker-build +# or: docker build -f docker/Dockerfile -t aegis-gitea-mcp . +``` + +### Configure + +Create a `.env` file (copy from `.env.example`) with your settings before starting the container. + +### Run + +```bash +make docker-up +# or: docker-compose up -d +``` + +### Logs + +```bash +make docker-logs +# or: docker-compose logs -f +``` + +### Stop + +```bash +make docker-down +# or: docker-compose down +``` + +--- + +## docker-compose.yml Overview + +The included `docker-compose.yml` provides: + +- **Health check:** polls `GET /health` every 30 seconds +- **Audit log volume:** mounts a named volume at `/var/log/aegis-mcp` so logs survive container restarts +- **Resource limits:** 1 CPU, 512 MB memory +- **Security:** non-root user, `no-new-privileges` +- **Traefik labels:** commented out — uncomment and set `MCP_DOMAIN` to enable automatic HTTPS via Traefik + +### Enabling Traefik + +1. Set `MCP_DOMAIN=mcp.yourdomain.com` in `.env`. +2. Uncomment the Traefik labels in `docker-compose.yml`. +3. Make sure Traefik is running with a `web` and `websecure` entrypoint and Let's Encrypt configured. + +--- + +## Dockerfile Details + +The image uses a multi-stage build: + +| Stage | Base image | Purpose | +|---|---|---| +| `builder` | `python:3.11-slim` | Install dependencies | +| `final` | `python:3.11-slim` | Minimal runtime image | + +The final image: +- Runs as user `aegis` (UID 1000, GID 1000) +- Exposes port `8080` +- Entry point: `python -m aegis_gitea_mcp.server` + +--- + +## Production Checklist + +- [ ] `AUTH_ENABLED=true` and `MCP_API_KEYS` set to a strong key +- [ ] `GITEA_TOKEN` belongs to a dedicated bot user with minimal permissions +- [ ] TLS terminated at the reverse proxy (Traefik, nginx, Caddy, etc.) +- [ ] `AUDIT_LOG_PATH` points to a persistent volume +- [ ] Log rotation configured for the audit log file +- [ ] API key rotation scheduled (every 90 days recommended) +- [ ] `MAX_AUTH_FAILURES` and `AUTH_FAILURE_WINDOW` tuned for your threat model +- [ ] Resource limits configured in Docker/Kubernetes + +--- + +## Kubernetes (Basic) + +A minimal Kubernetes deployment is not included, but the server is stateless and the Docker image is suitable for use in Kubernetes. Key considerations: + +- Store `.env` values as a `Secret` and expose them as environment variables. +- Mount an `emptyDir` or PersistentVolumeClaim at the audit log path. +- Use a `readinessProbe` and `livenessProbe` on `GET /health`. +- Set `resources.requests` and `resources.limits` for CPU and memory. + +--- + +## Updating + +```bash +git pull +make docker-build +make docker-up +``` + +If you added a new key via `make generate-key` during the update, restart the container to pick up the new `.env`: + +```bash +docker-compose restart aegis-mcp +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..d345713 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,117 @@ +# Getting Started + +## Prerequisites + +- Python 3.10 or higher +- A running Gitea instance +- A Gitea bot user with access to the repositories you want to expose +- `make` (optional but recommended) + +## 1. Install + +```bash +git clone +cd AegisGitea-MCP + +# Install production dependencies +make install + +# Or install with dev dependencies (for testing and linting) +make install-dev +``` + +To install manually without `make`: + +```bash +python -m venv venv +source venv/bin/activate # Linux/macOS +# or: venv\Scripts\activate # Windows + +pip install -e . +# dev: pip install -e ".[dev]" +``` + +## 2. Create a Gitea Bot User + +1. In your Gitea instance, create a dedicated user (e.g. `ai-bot`). +2. Grant that user **read access** to any repositories the AI should be able to see. +3. Generate an API token for the bot user: + - Go to **User Settings** > **Applications** > **Generate Token** + - Give it a descriptive name (e.g. `aegis-mcp-token`) + - Copy the token — you will not be able to view it again. + +## 3. Configure + +Copy the example environment file and fill in your values: + +```bash +cp .env.example .env +``` + +Minimum required settings in `.env`: + +```env +GITEA_URL=https://gitea.example.com +GITEA_TOKEN= +AUTH_ENABLED=true +MCP_API_KEYS= +``` + +See [Configuration](configuration.md) for the full list of settings. + +## 4. Generate an API Key + +The MCP server requires clients to authenticate with a bearer token. Generate one: + +```bash +make generate-key +# or: python scripts/generate_api_key.py +``` + +Copy the printed key into `MCP_API_KEYS` in your `.env` file. + +## 5. Run + +```bash +make run +# or: python -m aegis_gitea_mcp.server +``` + +The server starts on `http://0.0.0.0:8080` by default. + +Verify it is running: + +```bash +curl http://localhost:8080/health +# {"status": "healthy", ...} +``` + +## 6. Connect an AI Client + +### ChatGPT + +Use this single URL in the ChatGPT MCP connector: + +``` +http://:8080/mcp/sse?api_key= +``` + +ChatGPT uses the SSE transport: it opens a persistent GET stream on this URL and sends tool call messages back via POST to the same URL. The `api_key` query parameter is the recommended method because the ChatGPT interface does not support setting custom request headers. + +### Other MCP clients + +Clients that support custom headers can use: + +- **SSE URL:** `http://:8080/mcp/sse` +- **Tool discovery URL:** `http://:8080/mcp/tools` (no auth required) +- **Tool call URL:** `http://:8080/mcp/tool/call` +- **Authentication:** `Authorization: Bearer ` + +For a production deployment behind a reverse proxy, see [Deployment](deployment.md). + +## Next Steps + +- [Configuration](configuration.md) — tune file size limits, rate limiting, log paths +- [API Reference](api-reference.md) — available tools and endpoints +- [Security](security.md) — understand authentication and audit logging +- [Deployment](deployment.md) — Docker and Traefik setup diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..fa14436 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,43 @@ +# AegisGitea MCP - Documentation + +AegisGitea MCP is a security-first [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that provides controlled AI access to self-hosted Gitea repositories. + +## Overview + +AegisGitea MCP acts as a secure bridge between AI assistants (such as ChatGPT) and your Gitea instance. It exposes a limited set of read-only tools that allow an AI to browse repositories and read file contents, while enforcing strict authentication, rate limiting, and comprehensive audit logging. + +**Version:** 0.1.0 (Alpha) +**License:** MIT +**Requires:** Python 3.10+ + +## Documentation + +| Document | Description | +|---|---| +| [Getting Started](getting-started.md) | Installation and first-time setup | +| [Configuration](configuration.md) | All environment variables and settings | +| [API Reference](api-reference.md) | HTTP endpoints and MCP tools | +| [Architecture](architecture.md) | System design and data flow | +| [Security](security.md) | Authentication, rate limiting, and audit logging | +| [Deployment](deployment.md) | Docker and production deployment | + +## Quick Start + +```bash +# 1. Clone and install +git clone +cd AegisGitea-MCP +make install-dev + +# 2. Configure +cp .env.example .env +# Edit .env with your Gitea URL and token + +# 3. Generate an API key +make generate-key + +# 4. Run +make run +``` + +The server starts at `http://localhost:8080`. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..3c981dc --- /dev/null +++ b/docs/security.md @@ -0,0 +1,155 @@ +# Security + +## Authentication + +AegisGitea MCP uses bearer token authentication. Clients must include a valid API key with every tool call. + +### How It Works + +1. The client sends `Authorization: Bearer ` with its request. +2. The server extracts the token and validates it against the configured `MCP_API_KEYS`. +3. Comparison is done in **constant time** to prevent timing attacks. +4. If validation fails, the failure is counted against the client's IP address. + +### Generating API Keys + +Use the provided script to generate cryptographically secure 64-character hex keys: + +```bash +make generate-key +# or: python scripts/generate_api_key.py +``` + +Keys must be at least 32 characters long. The script also saves metadata (creation date, expiration) to a `keys/` directory. + +### Multiple Keys (Grace Period During Rotation) + +You can configure multiple keys separated by commas. This allows you to add a new key and remove the old one without downtime: + +```env +MCP_API_KEYS=newkey...,oldkey... +``` + +Remove the old key from the list after all clients have been updated. + +--- + +## Key Rotation + +Rotate keys regularly (recommended: every 90 days). + +```bash +make rotate-key +# or: python scripts/rotate_api_key.py +``` + +The rotation script: +1. Reads the current key from `.env` +2. Generates a new key +3. Offers to replace the key immediately or add it alongside the old key (grace period) +4. Creates a backup of your `.env` before modifying it + +### Checking Key Age + +```bash +make check-key-age +# or: python scripts/check_key_age.py +``` + +Exit codes: `0` = OK, `1` = expiring within 7 days (warning), `2` = already expired (critical). + +--- + +## Rate Limiting + +Failed authentication attempts are tracked per client IP address. + +| Setting | Default | Description | +|---|---|---| +| `MAX_AUTH_FAILURES` | `5` | Maximum failures before the IP is blocked | +| `AUTH_FAILURE_WINDOW` | `300` | Rolling window in seconds | + +Once an IP exceeds the threshold, all further requests from that IP return HTTP 429 until the window resets. This is enforced entirely in memory — a server restart resets the counters. + +--- + +## Audit Logging + +All security-relevant events are written to a structured JSON log file. + +### Log Location + +Default: `/var/log/aegis-mcp/audit.log` +Configurable via `AUDIT_LOG_PATH`. + +The directory is created automatically on startup. + +### What Is Logged + +| Event | Description | +|---|---| +| Tool invocation | Every call to a tool: tool name, arguments, result status, correlation ID | +| Access denied | Failed authentication attempts: IP address, reason | +| Security event | Rate limit triggers, invalid key formats, startup authentication status | + +### Log Format + +Each entry is a JSON object on a single line: + +```json +{ + "timestamp": "2026-02-13T10:00:00Z", + "event": "tool_invocation", + "correlation_id": "a1b2c3d4-...", + "tool": "get_file_contents", + "owner": "myorg", + "repo": "backend", + "path": "src/main.py", + "result": "success", + "client_ip": "10.0.0.1" +} +``` + +### Using Logs for Monitoring + +Because entries are newline-delimited JSON, they are easy to parse: + +```bash +# Show all failed tool calls +grep '"result": "error"' /var/log/aegis-mcp/audit.log | jq . + +# Show all access-denied events +grep '"event": "access_denied"' /var/log/aegis-mcp/audit.log | jq . +``` + +--- + +## Access Control Model + +AegisGitea MCP does **not** implement its own repository access control. Access to repositories is determined entirely by the Gitea bot user's permissions: + +- If the bot user has no access to a repository, it will not appear in `list_repositories` and `get_repository_info` will return an error. +- Grant the bot user the minimum set of repository permissions needed. + +**Principle of least privilege:** create a dedicated bot user and grant it read-only access only to the repositories that the AI needs to see. + +--- + +## Network Security Recommendations + +- Run the MCP server behind a reverse proxy (e.g. Traefik or nginx) with TLS. +- Do not expose the server directly on a public port without TLS. +- Restrict inbound connections to known AI client IP ranges where possible. +- The `/mcp/tools` endpoint is intentionally public (required for ChatGPT plugin discovery). If this is undesirable, restrict it at the network/proxy level. + +--- + +## Container Security + +The provided Docker image runs with: + +- A non-root user (`aegis`, UID 1000) +- `no-new-privileges` security option +- CPU and memory resource limits (1 CPU, 512 MB) + +See [Deployment](deployment.md) for details.