Some checks failed
test / test (push) Failing after 19s
docker / lint (pull_request) Failing after 21s
lint / lint (pull_request) Failing after 21s
lint / lint (push) Failing after 1m29s
docker / test (pull_request) Failing after 16s
test / test (pull_request) Failing after 18s
docker / docker-test (pull_request) Has been skipped
docker / docker-publish (pull_request) Has been skipped
153 lines
4.0 KiB
Markdown
153 lines
4.0 KiB
Markdown
# 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=<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:
|
|
|
|
```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://<mcp-host>/.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 <user token>
|
|
```
|
|
|
|
## Example curl
|
|
|
|
Protected resource metadata:
|
|
|
|
```bash
|
|
curl -s https://<mcp-host>/.well-known/oauth-protected-resource | jq
|
|
```
|
|
|
|
Expected 401 challenge when missing token:
|
|
|
|
```bash
|
|
curl -i https://<mcp-host>/mcp/tool/call \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"tool":"list_repositories","arguments":{}}'
|
|
```
|
|
|
|
Authenticated tool call:
|
|
|
|
```bash
|
|
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 `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`
|