# Deployment This page covers running the HTTP server for remote clients. For a local stdio setup see [getting-started.md](getting-started.md). Read [hardening.md](hardening.md) as well: this server hands the power of its Gitea token to every client that can authenticate. ## Choose a mode - Admin-token only: set `GITEA_TOKEN` and `MCP_BEARER_TOKEN`. Every client with the bearer token acts as the token owner. - Per-user OAuth: additionally set `OAUTH_ENABLED=true` with the OAuth settings. Users sign in with their own Gitea account and act under their own permissions. The static bearer keeps working next to it as the admin identity; leave `MCP_BEARER_TOKEN` unset to turn that off. ## Docker Image: `git.hiddenden.cafe/hiddenden/aegisgitea-mcp`. Tag `dev` is built from the dev branch and `latest` from main. `uv run aegis-gitea-mcp setup` prints this compose service, with your port: ```yaml services: aegis-gitea-mcp: image: git.hiddenden.cafe/hiddenden/aegisgitea-mcp:dev restart: unless-stopped env_file: .env environment: MCP_TRANSPORT: http MCP_HOST: 0.0.0.0 # allowed because authentication is configured DATA_DIR: /var/lib/aegis-mcp ports: - "127.0.0.1:8080:8080" volumes: - aegis-mcp-data:/var/lib/aegis-mcp read_only: true tmpfs: - /tmp cap_drop: - ALL security_opt: - no-new-privileges:true volumes: aegis-mcp-data: ``` Points that matter: - Inside the container the server must bind `0.0.0.0` so the published port works. That is allowed only because `MCP_BEARER_TOKEN` or OAuth is configured; otherwise it exits with code 2. - The port is published on `127.0.0.1` only. The reverse proxy on the same host is the public entry point. - `DATA_DIR` must be a persistent volume writable by the container user. It holds the audit log and the encrypted OAuth token store (`oauth-tokens.json`, mode 0600). Losing the volume loses all OAuth logins and the audit history. Back it up, and protect the backup like the secrets it protects. - Keep `.env` out of version control and out of the image (mode 0600; `setup` writes it that way). Compose passes it as environment, so it is visible to `docker inspect` for anyone with Docker access. Check it: `curl http://127.0.0.1:8080/health`. ## Reverse proxy and TLS Terminate TLS at a reverse proxy. `PUBLIC_BASE_URL` must be the public https URL (it is required for OAuth, and http is only accepted for loopback hosts). Mistral Le Chat needs a valid public certificate. Caddy: ``` gitea-mcp.example.com { reverse_proxy 127.0.0.1:8080 { flush_interval -1 } } ``` nginx: inside the proxying `location`, set `proxy_buffering off;` so streamed responses are not held back. Also forward `Host` (`proxy_set_header Host $host;`) and keep the default read timeout in mind for long tool calls. The server rejects requests whose `Host` is not allowed (DNS-rebinding protection). Allowed hosts are the loopback names with the port, the host of `PUBLIC_BASE_URL`, and `ALLOWED_HOSTS`. If you reach it under another name, add that name to `ALLOWED_HOSTS`. ## OAuth application in Gitea `setup --public-base-url https://gitea-mcp.example.com` registers the application for you as the admin's user application. To do it by hand: in Gitea open Settings, Applications, Manage OAuth2 Applications, create an application named `aegis-gitea-mcp` as a confidential client with the redirect URI `https://gitea-mcp.example.com/oauth/callback`, and put the client id and secret in `GITEA_OAUTH_CLIENT_ID` and `GITEA_OAUTH_CLIENT_SECRET`. Then set `OAUTH_ENABLED=true`, `PUBLIC_BASE_URL` and `TOKEN_ENCRYPTION_KEY` (at least 32 characters, `openssl rand -hex 32`). Gitea has no instance-wide OAuth application API, so the application belongs to the admin account that created it. Re-running setup reuses it; if the stored client secret is missing, Gitea has to regenerate it, and setup asks before doing so. `OAUTH_SCOPES` is the scope list sent to Gitea. Keep it to granular scopes. Narrow it (for example to `read:repository,read:issue`) to limit what signed-in users' tokens can do through this server regardless of their Gitea rights. ## Upgrades - Application: pull the new image (or package) and restart. The data volume is kept. - `TOKEN_ENCRYPTION_KEY` must stay the same, or stored OAuth logins can no longer be decrypted and users have to sign in again. - Upgrading Gitea: tool names and schemas come from the pinned swagger, so a Gitea upgrade does not change them. To pick up a newer API, re-pin the swagger and regenerate the catalog: ```bash curl -fsS https://git.example.com/swagger.v1.json -o spec/gitea-swagger.v1.json uv run python scripts/generate_catalog.py uv run python scripts/check_docs.py --write uv run pytest ``` Review the diff of `tools.json` and `docs/tools.md`: renamed or removed operations change tool names. Until the new spec is pinned, `gitea_api_request` reaches new endpoints. ## CI The workflows in `.gitea/workflows/` lint, test and build. The `docs-check` job runs `uv run python scripts/check_docs.py`, which fails when `docs/tools.md` or the README tool summary is stale, or when a document names a tool that does not exist. `scripts/generate_catalog.py --check` fails when `tools.json` does not match the pinned swagger.