Latte 45641f8e2c
docker / test (push) Successful in 36s
docker / lint (push) Successful in 41s
lint / lint (push) Successful in 44s
docker / test (pull_request) Successful in 37s
lint / lint (pull_request) Successful in 44s
test / test (push) Successful in 45s
docker / lint (pull_request) Successful in 42s
test / package (push) Successful in 1m6s
test / test (pull_request) Successful in 40s
test / package (pull_request) Successful in 57s
docker / docker (pull_request) Successful in 56s
docker / docker (push) Successful in 1m7s
docs: document dev and stable package channels
Document the two publish channels (aegis-gitea-mcp from main,
aegis-gitea-mcp-dev from dev), install commands for each, that both share
the aegis_gitea_mcp module so only one installs per environment, and the
merge-driven stable release flow (bump version -> PR into dev -> promote
dev to main; re-pushing main at the same version is a --check-url no-op).
2026-06-27 15:59:37 +02:00
.
2026-01-29 19:53:36 +01:00
.
2026-01-29 19:53:36 +01:00

AegisGitea-MCP

Security-first MCP server for self-hosted Gitea, available as two transports built on one shared core:

  • Local (stdio)uvx aegis-gitea-mcp. A single-user server for your own machine that authenticates with your Gitea Personal Access Token. No OAuth, no web stack. Ideal for Claude Desktop / Claude Code on your laptop.
  • Server (HTTP/OAuth)aegis-gitea-mcp[server] / Docker. The public, multi-user deployment with per-user OAuth2/OIDC, dynamic client registration, rate limiting, and per-user repository authorization. Exposes MCP over Streamable HTTP and a legacy SSE alias.

Both transports share the same tools, policy engine, secret sanitization, tamper-evident audit log, and — new in 0.2.0 — safe full-API coverage via the policy-gated gitea_request escape hatch plus resource-type-aware authorization for the admin/user/org surface.

Branching / contribution flow: HEAD -> feature branch -> dev -> main. All pull requests target dev; dev is merged to main for releases. Never commit or push directly to dev or main.

Run locally (stdio, single user)

Install nothing and run it with uv:

GITEA_URL=https://git.hiddenden.cafe \
GITEA_TOKEN=<your-gitea-personal-access-token> \
uvx aegis-gitea-mcp

Or install it:

pip install aegis-gitea-mcp        # core only (local stdio)
aegis-gitea-mcp                    # reads GITEA_URL + GITEA_TOKEN (or a .env file)

Wire it into Claude Code:

claude mcp add aegis-gitea -- uvx aegis-gitea-mcp
# with env values:
claude mcp add aegis-gitea -e GITEA_URL=https://git.hiddenden.cafe -e GITEA_TOKEN=<pat> -- uvx aegis-gitea-mcp

Or Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "aegis-gitea": {
      "command": "uvx",
      "args": ["aegis-gitea-mcp"],
      "env": {
        "GITEA_URL": "https://git.hiddenden.cafe",
        "GITEA_TOKEN": "<your-gitea-personal-access-token>"
      }
    }
  }
}

The local server resolves your PAT's Gitea user at startup and pins every call to that identity. The policy engine and WRITE_MODE gate still apply (writes are off by default), and the audit log is written to a per-user path (e.g. %LOCALAPPDATA%\aegis-gitea-mcp\audit.log on Windows, ~/.local/state/aegis-gitea-mcp/audit.log on Linux). See docs/local-quickstart.md.

Securing MCP with Gitea OAuth (public server)

The HTTP/OAuth server needs the web stack: install with pip install 'aegis-gitea-mcp[server]' (or use Docker) and run aegis-gitea-mcp-server.

This guide uses the live deployment values as the running example:

Thing Value
Gitea instance (GITEA_URL) https://git.hiddenden.cafe
This MCP server (PUBLIC_BASE_URL) https://gitea-mcp.hiddenden.cafe
OAuth callback to register in Gitea https://gitea-mcp.hiddenden.cafe/oauth/callback
MCP URL you give to Claude https://gitea-mcp.hiddenden.cafe/mcp

Substitute your own hostnames if they differ. The two URLs are different hosts: git.* is Gitea, gitea-mcp.* is this proxy.

1) Create a Gitea OAuth2 application

  1. Open https://git.hiddenden.cafe/user/settings/applications (or admin application settings).
  2. Create an OAuth2 app.
  3. Set the redirect URI to this MCP server's callback (not Gitea's own host): https://gitea-mcp.hiddenden.cafe/oauth/callback This is the only redirect URI Gitea needs — the MCP server forwards each client's real callback through a signed state parameter.
  4. Save the app and copy the generated Client ID and Client Secret.

Required scopes:

  • read:repository
  • write:repository (only needed when using write tools)

2) Configure this MCP server

cp .env.example .env

Fill in exactly these values in .env (everything else has safe defaults):

# The Gitea instance this server talks to
GITEA_URL=https://git.hiddenden.cafe

# Per-user OAuth mode (recommended)
OAUTH_MODE=true
GITEA_OAUTH_CLIENT_ID=<client-id-from-step-1>
GITEA_OAUTH_CLIENT_SECRET=<client-secret-from-step-1>

# Public URL of THIS server (no trailing slash). Claude's MCP URL is this + /mcp
PUBLIC_BASE_URL=https://gitea-mcp.hiddenden.cafe

# Secret that signs the OAuth proxy state. Generate with: openssl rand -hex 32
OAUTH_STATE_SECRET=<random-32-byte-minimum-secret>

# Where dynamically-registered OAuth clients are stored — MUST be a writable,
# persistent path. The default matches the aegis-mcp-data volume in compose.
DCR_STORAGE_PATH=/var/lib/aegis-mcp/dcr_clients.json

2b) Service PAT (GITEA_TOKEN) — needed in practice

Gitea issues OIDC access tokens that carry only openid/profile/email. They establish identity but cannot call the repository REST API, so in pure-OAuth mode most tools fail (you will see a generic error, or list_repositories returning nothing usable). Configure a service PAT so the tools actually work:

  1. Create a dedicated bot account in Gitea (not a personal account).
  2. Generate a Personal Access Token with least privilege:
    • read:repository
    • write:repository only if you enable WRITE_MODE
  3. Set it in .env:
GITEA_TOKEN=<bot-personal-access-token>

This does not weaken per-user security. OAuth remains authoritative: before every repository call the server verifies that the signed-in user has permission on the target repo through Gitea (_verify_user_repository_access) and denies it otherwise. The PAT only performs the API call after that check; OAuth provides identity, per-user authorization, and audit attribution.

Note: with a service PAT, list_repositories is scoped to the signed-in user — it returns only the repositories that user owns or contributes to (resolved via Gitea's repo search with the uid filter), not everything the bot can see. Visibility of private repos still depends on what the service token itself can access. All other tools require an explicit owner/repo and run the per-user permission check first.

2a) Required writable volumes (read-only container)

The provided docker-compose.yml runs the container with a read-only root filesystem. The server therefore needs two writable volumes, both already wired up in compose:

Path Purpose Volume
/var/log/aegis-mcp tamper-evident audit log aegis-mcp-logs
/var/lib/aegis-mcp dynamic client registration store (DCR_STORAGE_PATH) aegis-mcp-data

If /var/lib/aegis-mcp is not writable/persistent, the OAuth authorize, token, and register endpoints fail and the browser shows a bare Internal Server Error during login. Keep the aegis-mcp-data volume mounted (or point DCR_STORAGE_PATH at another writable, persistent location), and make sure it survives restarts so registered clients are not lost.

3) Configure Claude, Claude Code, or Cowork

Claude's hosted, desktop, mobile, Claude Code, and Cowork surfaces share the same remote MCP connector infrastructure. There is no Claude-specific server code path.

In claude.ai:

  1. Open Settings > Connectors.
  2. Choose Add custom connector.
  3. Paste https://gitea-mcp.hiddenden.cafe/mcp.
  4. Complete the OAuth consent flow. Dynamic Client Registration (/register) handles Claude client registration.

In Claude Code:

claude mcp add --transport http aegis-gitea https://gitea-mcp.hiddenden.cafe/mcp

Cowork uses the same connector model and MCP URL.

Manual OAuth client configuration remains available for clients that do not use DCR:

  • MCP server URL: https://gitea-mcp.hiddenden.cafe/mcp
  • Authentication: OAuth
  • OAuth client ID: the client id returned by /register or your preconfigured client id
  • OAuth client secret: only for confidential clients

Hosted Claude callbacks are allowed by default: https://claude.ai/api/mcp/auth_callback and https://claude.com/api/mcp/auth_callback. Loopback redirects for Claude Code local development are allowed for http://127.0.0.1:* and http://localhost:*.

4) OAuth-protected MCP behavior

The server publishes protected-resource metadata:

  • GET /.well-known/oauth-protected-resource

Example response:

{
  "resource": "https://gitea-mcp.hiddenden.cafe",
  "authorization_servers": [
    "https://gitea-mcp.hiddenden.cafe",
    "https://git.hiddenden.cafe"
  ],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["read:repository", "write:repository"],
  "resource_documentation": "https://hiddenden.cafe/docs/mcp-gitea"
}

If a tool call is missing/invalid auth, MCP endpoints return 401 with:

WWW-Authenticate: Bearer resource_metadata="https://<mcp-host>/.well-known/oauth-protected-resource", scope="read:repository"

Architecture

Claude / Claude Code / Cowork
  -> Authorization Code Flow
  -> Gitea OAuth2/OIDC (issuer: https://git.hiddenden.cafe)
  -> Access token
   -> MCP Server (/mcp, /mcp/sse, /mcp/tool/call)
     -> OIDC discovery + JWKS cache
     -> Scope enforcement (read:repository / write:repository)
     -> Policy allow/deny
     -> If GITEA_TOKEN is set: check Gitea collaborator permission for <user, repo>
     -> Gitea API call with either the user token or the service PAT after authz

Example curl

Protected resource metadata:

curl -s https://<mcp-host>/.well-known/oauth-protected-resource | jq

Expected 401 challenge when missing token:

curl -i https://<mcp-host>/mcp/tool/call \
  -H "Content-Type: application/json" \
  -d '{"tool":"list_repositories","arguments":{}}'

Authenticated tool call:

curl -s https://<mcp-host>/mcp/tool/call \
  -H "Authorization: Bearer <user_access_token>" \
  -H "Content-Type: application/json" \
  -d '{"tool":"get_repository_info","arguments":{"owner":"acme","repo":"demo"}}'

Threat model

  • Shared bot tokens are dangerous:
    • one leaked token can expose all repositories reachable by that bot account.
    • blast radius is repository-wide and cross-user.
  • Token-in-URL is insecure:
    • URLs leak via logs, proxies, browser history, and referers.
    • bearer tokens must be sent in Authorization headers only.
  • Per-user OAuth reduces lateral access:
    • identity comes from Gitea OIDC/JWKS or userinfo validation.
    • without GITEA_TOKEN, API calls use the user's token and Gitea enforces permissions.
    • with GITEA_TOKEN, every repository-targeted call first checks the user's Gitea permission and fails closed if the check cannot be made.

CI/CD

Gitea workflows were added under .gitea/workflows/:

  • lint.yml: Ruff + formatting + mypy.
  • test.yml: lint + pytest + enforced coverage (>=80%).
  • docker.yml: lint+test gated Docker build, SHA tag, latest tag on main.
  • publish.yml: on a v* tag, lint+test gated uv build + publish the Python package to the Gitea PyPI registry (see docs/packaging.md).

Docker hardening

docker/Dockerfile uses a multi-stage build, non-root runtime user, production env flags, minimal runtime dependencies, and a healthcheck.

Commands

  • make test
  • make lint
  • make format
  • make docker-build
  • make docker-up

Documentation

  • docs/local-quickstart.md — local stdio install and client wiring
  • docs/packaging.md — build & publish with uv
  • docs/api-reference.md
  • docs/security.md — incl. resource-type-aware authorization
  • docs/configuration.md
  • docs/deployment.md
  • docs/write-mode.md
  • docs/raw-api.md — the gitea_request escape hatch
S
Description
AegisGitea MCP is a private, security-first MCP (Model Context Protocol) server that enables controlled, auditable, read-only AI access to a self-hosted Gitea environment. The system allows ChatGPT (Business / Developer environment) to inspect repositories, code, commits, issues, and pull requests only through explicit MCP tool calls, while all access control is dynamically managed through a dedicated bot user inside Gitea itself.
Readme MIT 1.1 MiB
v0.2.0 Latest
2026-06-27 14:26:07 +00:00
Languages
Python 99%
Makefile 0.4%
Shell 0.3%
Dockerfile 0.3%