The audit logger is opened at startup (exit 2 on failure) and obtained before every call, which is refused with audit_unavailable otherwise. Audit records only take validated path and sudo values and use the request id as correlation id. The rate limiter evicts idle keys, caps tracked keys, audits the first excess per key and window and uses the settings passed to the app. FORWARDED_ALLOW_IPS is now a setting, and startup warns about a missing host allow-list and unavailable OAuth.
5.9 KiB
Deployment
This page covers running the HTTP server for remote clients. For a local stdio setup see getting-started.md. Read 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_TOKENandMCP_BEARER_TOKEN. Every client with the bearer token acts as the token owner. - Per-user OAuth: additionally set
OAUTH_ENABLED=truewith 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; leaveMCP_BEARER_TOKENunset 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:
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.0so the published port works. That is allowed only becauseMCP_BEARER_TOKENor OAuth is configured; otherwise it exits with code 2. - The port is published on
127.0.0.1only. The reverse proxy on the same host is the public entry point. DATA_DIRmust 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
.envout of version control and out of the image (mode 0600;setupwrites it that way). Compose passes it as environment, so it is visible todocker inspectfor 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. With a non-loopback
MCP_HOST (the container) and neither PUBLIC_BASE_URL nor ALLOWED_HOSTS, every remote Host
is rejected with 421, and the server logs a warning at startup naming the two variables.
The server trusts X-Forwarded-For only from FORWARDED_ALLOW_IPS (default 127.0.0.1). When
the proxy reaches the container from another address, for example a Docker network, add that
address or network (FORWARDED_ALLOW_IPS=172.18.0.0/16). Without it every request looks like it
comes from the proxy and shares one per-IP rate limit bucket. * trusts any peer, so use it only
when the port is reachable solely from the proxy.
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_KEYmust 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:
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 pytestReview the diff of
tools.jsonanddocs/tools.md: renamed or removed operations change tool names. Until the new spec is pinned,gitea_api_requestreaches 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.