# AegisGitea-MCP Security-first MCP server for self-hosted Gitea with per-user OAuth2/OIDC authentication. AegisGitea-MCP exposes MCP tools over HTTP/SSE and validates each user token against Gitea so tool access follows each user's actual repository permissions. ## Securing MCP with Gitea OAuth ### 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 redirect URI to the ChatGPT callback URL shown after creating a New App. 4. Save the app and keep: - `Client ID` - `Client Secret` Required scopes: - `read:repository` - `write:repository` (only needed when using write tools) ### 2) Configure this MCP server ```bash cp .env.example .env ``` Set OAuth-first values: ```env GITEA_URL=https://git.hiddenden.cafe OAUTH_MODE=true GITEA_OAUTH_CLIENT_ID= GITEA_OAUTH_CLIENT_SECRET= OAUTH_EXPECTED_AUDIENCE= ``` ### 3) Configure ChatGPT New App In ChatGPT New App: - MCP server URL: `https:///mcp/sse` - Authentication: OAuth - OAuth client ID: Gitea OAuth app client ID - OAuth client secret: Gitea OAuth app client secret After creation, copy the ChatGPT callback URL and add it to the Gitea OAuth app redirect URIs. ### 4) OAuth-protected MCP behavior The server publishes protected-resource metadata: - `GET /.well-known/oauth-protected-resource` Example response: ```json { "resource": "https://git.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: ```http WWW-Authenticate: Bearer resource_metadata="https:///.well-known/oauth-protected-resource", scope="read:repository" ``` ## Architecture ```text ChatGPT App -> Authorization Code Flow -> Gitea OAuth2/OIDC (issuer: https://git.hiddenden.cafe) -> Access token -> MCP Server (/mcp/sse, /mcp/tool/call) -> OIDC discovery + JWKS cache -> Scope enforcement (read:repository / write:repository) -> Per-request Gitea API calls with Authorization: Bearer ``` ## Example curl Protected resource metadata: ```bash curl -s https:///.well-known/oauth-protected-resource | jq ``` Expected 401 challenge when missing token: ```bash curl -i https:///mcp/tool/call \ -H "Content-Type: application/json" \ -d '{"tool":"list_repositories","arguments":{}}' ``` Authenticated tool call: ```bash curl -s https:///mcp/tool/call \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"tool":"list_repositories","arguments":{}}' ``` ## 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: - each call runs as the signed-in user. - users only see repositories they already have permission for in Gitea. ## 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`. ## 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/api-reference.md` - `docs/security.md` - `docs/configuration.md` - `docs/deployment.md` - `docs/write-mode.md`