Reviewed-on: #7
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
- Open
https://git.hiddenden.cafe/user/settings/applications(or admin application settings). - Create an OAuth2 app.
- Set redirect URI to the ChatGPT callback URL shown after creating a New App.
- Save the app and keep:
Client IDClient Secret
Required scopes:
read:repositorywrite:repository(only needed when using write tools)
2) Configure this MCP server
cp .env.example .env
Set OAuth-first values:
GITEA_URL=https://git.hiddenden.cafe
OAUTH_MODE=true
GITEA_OAUTH_CLIENT_ID=<your-client-id>
GITEA_OAUTH_CLIENT_SECRET=<your-client-secret>
OAUTH_EXPECTED_AUDIENCE=<optional; defaults to client id>
3) Configure ChatGPT New App
In ChatGPT New App:
- MCP server URL:
https://<your-mcp-domain>/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:
{
"resource": "https://git.hiddenden.cafe",
"authorization_servers": ["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
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 <user token>
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":"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
Authorizationheaders 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,latesttag onmain.
Docker hardening
docker/Dockerfile uses a multi-stage build, non-root runtime user, production env flags, minimal runtime dependencies, and a healthcheck.
Commands
make testmake lintmake formatmake docker-buildmake docker-up
Documentation
docs/api-reference.mddocs/security.mddocs/configuration.mddocs/deployment.mddocs/write-mode.md