docs: retarget setup to Claude connectors
test / test (push) Has been cancelled
lint / lint (push) Has been cancelled
docker / test (pull_request) Successful in 13s
docker / lint (pull_request) Successful in 2m3s
lint / lint (pull_request) Successful in 16s
test / test (pull_request) Successful in 14s
docker / docker-test (pull_request) Successful in 42s
docker / docker-publish (pull_request) Has been skipped

This commit is contained in:
2026-06-13 21:05:21 +02:00
parent 541124e92a
commit b275f5c0c2
10 changed files with 135 additions and 193 deletions
+5 -2
View File
@@ -12,14 +12,17 @@
- Returns OAuth protected resource metadata used by MCP clients.
- `GET /.well-known/oauth-authorization-server`
- Returns OAuth authorization server metadata.
- `POST /register`
- Registers an OAuth client and persists the client metadata.
- `POST /oauth/token`
- Proxies OAuth authorization-code token exchange to Gitea.
## MCP Endpoints
- `GET /mcp/tools`: list tool definitions.
- `POST /mcp/tool/call`: execute a tool.
- `GET /mcp/sse` and `POST /mcp/sse`: MCP SSE transport.
- `GET /mcp` and `POST /mcp`: streamable HTTP transport.
- `GET /mcp/sse` and `POST /mcp/sse`: MCP SSE transport alias.
- `POST /mcp/tool/call`: direct tool-call endpoint.
Authentication requirements:
+21 -14
View File
@@ -2,20 +2,22 @@
## Overview
AegisGitea MCP is a Python 3.10+ application built on **FastAPI**. It acts as a bridge between an AI client (such as ChatGPT) and a self-hosted Gitea instance, implementing the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
AegisGitea MCP is a Python 3.10+ application built on **FastAPI**. It acts as a bridge between an AI client (such as Claude, Claude Code, or Cowork) and a self-hosted Gitea instance, implementing the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
```
AI Client (ChatGPT)
AI Client (Claude / Claude Code / Cowork)
│ HTTP (Authorization: Bearer <key>)
┌────────────────────────────────────────────┐
│ FastAPI Server │
│ server.py │
│ - Route: GET/POST /mcp │
│ - Route: POST /mcp/tool/call │
│ - Route: GET /mcp/tools │
│ - Route: GET /health │
│ - SSE support (GET/POST /mcp/sse)
│ - Streamable HTTP transport
│ - Legacy SSE alias (GET/POST /mcp/sse) │
└───────┬───────────────────┬────────────────┘
│ │
┌────▼────┐ ┌────▼──────────────┐
@@ -91,7 +93,7 @@ Key methods:
| Method | Gitea endpoint |
|---|---|
| `get_current_user()` | `GET /api/v1/user` |
| `list_repositories()` | `GET /api/v1/repos/search` |
| `list_repositories()` | `GET /api/v1/user/repos` |
| `get_repository()` | `GET /api/v1/repos/{owner}/{repo}` |
| `get_file_contents()` | `GET /api/v1/repos/{owner}/{repo}/contents/{path}` |
| `get_tree()` | `GET /api/v1/repos/{owner}/{repo}/git/trees/{ref}` |
@@ -134,7 +136,7 @@ All handlers return a plain string. `server.py` wraps this in an `MCPToolCallRes
2. FastAPI routes the request to the tool-call handler in server.py
3. auth.validate_api_key() checks the Authorization header
3. OAuth middleware validates the Bearer token via Gitea OIDC/JWKS or userinfo
├── Fail → AuditLogger.log_access_denied() → HTTP 401 / 429
└── Pass → continue
@@ -142,27 +144,32 @@ All handlers return a plain string. `server.py` wraps this in an `MCPToolCallRes
5. Tool dispatcher looks up the tool by name (mcp_protocol.get_tool_by_name)
6. Tool handler function (tools/repository.py) is called
6. Policy engine checks read/write mode and repository/path policy
7. GiteaClient makes an async HTTP call to the Gitea API
7. If GITEA_TOKEN is configured, service-PAT authz checks
GET /repos/{owner}/{repo}/collaborators/{user}/permission
8. Result (or error) is returned to server.py
8. Tool handler function (tools/repository.py) is called
9. AuditLogger.log_tool_invocation(status="success" | "error")
9. GiteaClient makes an async HTTP call to the Gitea API
10. MCPToolCallResponse is returned to the client
10. Result (or error) is returned to server.py
11. AuditLogger.log_tool_invocation(status="success" | "error")
12. MCPToolCallResponse is returned to the client
```
---
## Key Design Decisions
**Read-only by design.** The MCP tools only read data from Gitea. No write operations are implemented.
**Read by default, writes opt-in.** Read tools are available by default. Write-capable tools require `WRITE_MODE=true`, repository write policy/whitelist approval, and `write:repository` authorization.
**Gitea controls access.** The server does not maintain its own repository ACL. The Gitea bot user's permissions are the source of truth. If the bot cannot access a repo, the server cannot either.
**Gitea controls repository access.** Without `GITEA_TOKEN`, Gitea enforces repository permissions on API calls made with the user's token. With `GITEA_TOKEN`, the service PAT can only execute after the server verifies the requesting user's actual repository permission through Gitea and writes an audit denial if the check fails.
**Public tool discovery.** `GET /mcp/tools` requires no authentication so that ChatGPT's plugin system can discover the available tools without credentials. All other endpoints require authentication.
**Public tool discovery.** `GET /mcp/tools` requires no authentication so that MCP clients can discover the available tools without credentials. All other endpoints require authentication.
**Stateless server.** No database or persistent state beyond the audit log file. Rate limit counters are in-memory and reset on restart.
**Minimal persisted state.** The audit log is persisted for tamper-evident review. Dynamic OAuth client registrations are persisted when DCR is enabled. Rate limit counters and short-lived authz caches are in-memory and reset on restart.
**Async throughout.** FastAPI + `httpx.AsyncClient` means all Gitea API calls are non-blocking, allowing the server to handle concurrent requests efficiently.
+6 -1
View File
@@ -14,8 +14,10 @@ cp .env.example .env
| `OAUTH_MODE` | No | `false` | Enables OAuth-oriented validation settings |
| `GITEA_OAUTH_CLIENT_ID` | Yes when `OAUTH_MODE=true` | - | OAuth client id |
| `GITEA_OAUTH_CLIENT_SECRET` | Yes when `OAUTH_MODE=true` | - | OAuth client secret |
| `OAUTH_EXPECTED_AUDIENCE` | No | empty | Expected JWT audience; defaults to client id |
| `OAUTH_EXPECTED_AUDIENCE` | No | empty | Additional accepted JWT audience beyond the MCP resource and Gitea client id |
| `OAUTH_CACHE_TTL_SECONDS` | No | `300` | OIDC discovery/JWKS cache TTL |
| `OAUTH_STATE_SECRET` | Yes when `OAUTH_MODE=true` | - | HMAC secret for signed OAuth state wrappers |
| `OAUTH_REDIRECT_ALLOWLIST` | No | empty | Additional allowed redirect URIs for OAuth clients |
## MCP Server Settings
@@ -27,6 +29,8 @@ cp .env.example .env
| `ALLOW_INSECURE_BIND` | No | `false` | Explicit opt-in required for `0.0.0.0` bind |
| `LOG_LEVEL` | No | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
| `STARTUP_VALIDATE_GITEA` | No | `true` | Validate OIDC discovery endpoint at startup |
| `DCR_ENABLED` | No | `true` | Enable dynamic client registration at `/register` |
| `DCR_STORAGE_PATH` | No | `/var/lib/aegis-mcp/dcr_clients.json` | Persisted OAuth client registry path |
## Security and Limits
@@ -41,6 +45,7 @@ cp .env.example .env
| `MAX_TOOL_RESPONSE_CHARS` | No | `20000` | Max chars in text fields |
| `REQUEST_TIMEOUT_SECONDS` | No | `30` | Upstream timeout for Gitea calls |
| `SECRET_DETECTION_MODE` | No | `mask` | `off`, `mask`, `block` |
| `REPO_AUTHZ_CACHE_TTL_SECONDS` | No | `60` | TTL for cached per-user repository permission checks |
## Write Mode
+38 -25
View File
@@ -4,7 +4,7 @@
- Python 3.10 or higher
- A running Gitea instance
- A Gitea bot user with access to the repositories you want to expose
- A Gitea OAuth2 application for this MCP server
- `make` (optional but recommended)
## 1. Install
@@ -31,14 +31,12 @@ pip install -e .
# dev: pip install -e ".[dev]"
```
## 2. Create a Gitea Bot User
## 2. Create a Gitea OAuth2 Application
1. In your Gitea instance, create a dedicated user (e.g. `ai-bot`).
2. Grant that user **read access** to any repositories the AI should be able to see.
3. Generate an API token for the bot user:
- Go to **User Settings** > **Applications** > **Generate Token**
- Give it a descriptive name (e.g. `aegis-mcp-token`)
- Copy the token — you will not be able to view it again.
1. In Gitea, open **User Settings > Applications**.
2. Create an OAuth2 application for AegisGitea-MCP.
3. Set the redirect URI to `https://<host>/oauth/callback`.
4. Copy the client ID and client secret.
## 3. Configure
@@ -48,27 +46,31 @@ Copy the example environment file and fill in your values:
cp .env.example .env
```
Minimum required settings in `.env`:
Minimum OAuth settings in `.env`:
```env
GITEA_URL=https://gitea.example.com
GITEA_TOKEN=<your-bot-user-token>
AUTH_ENABLED=true
MCP_API_KEYS=<your-generated-api-key>
OAUTH_MODE=true
GITEA_OAUTH_CLIENT_ID=<your-gitea-oauth-client-id>
GITEA_OAUTH_CLIENT_SECRET=<your-gitea-oauth-client-secret>
PUBLIC_BASE_URL=https://<host>
OAUTH_STATE_SECRET=<random-32-byte-minimum-secret>
```
`GITEA_TOKEN` is optional. If it is set, use a narrowly scoped service PAT and only grant it repository access you are prepared to expose after per-user authorization checks. If it is not set, Gitea REST calls use the authenticated user's OAuth token directly.
See [Configuration](configuration.md) for the full list of settings.
## 4. Generate an API Key
## 4. Optional Standard API Key Mode
The MCP server requires clients to authenticate with a bearer token. Generate one:
For non-OAuth deployments, configure `GITEA_TOKEN` and `MCP_API_KEYS`. Generate an API key with:
```bash
make generate-key
# or: python scripts/generate_api_key.py
```
Copy the printed key into `MCP_API_KEYS` in your `.env` file.
Copy the printed key into `MCP_API_KEYS` in your `.env` file and set `OAUTH_MODE=false`.
## 5. Run
@@ -88,24 +90,35 @@ curl http://localhost:8080/health
## 6. Connect an AI Client
### ChatGPT
### Claude
Use this single URL in the ChatGPT MCP connector:
In claude.ai, open **Settings > Connectors > Add custom connector** and paste:
```
http://<host>:8080/mcp/sse?api_key=<your-api-key>
https://<host>/mcp
```
ChatGPT uses the SSE transport: it opens a persistent GET stream on this URL and sends tool call messages back via POST to the same URL. The `api_key` query parameter is the recommended method because the ChatGPT interface does not support setting custom request headers.
Claude discovers OAuth metadata, registers through `/register`, and uses PKCE S256 automatically.
### Other MCP clients
### Claude Code
Clients that support custom headers can use:
```bash
claude mcp add --transport http aegis-gitea https://<host>/mcp
```
- **SSE URL:** `http://<host>:8080/mcp/sse`
- **Tool discovery URL:** `http://<host>:8080/mcp/tools` (no auth required)
- **Tool call URL:** `http://<host>:8080/mcp/tool/call`
- **Authentication:** `Authorization: Bearer <your-api-key>`
Claude Code uses the same remote MCP and OAuth metadata. Local development loopback callbacks are allowed by default.
### Cowork
Cowork uses the same connector infrastructure and MCP URL as Claude.
### SSE compatibility
If your client still expects SSE transport, use:
- **SSE URL:** `https://<host>/mcp/sse`
- **Tool discovery URL:** `https://<host>/mcp/tools` (no auth required)
- **Tool call URL:** `https://<host>/mcp/tool/call`
For a production deployment behind a reverse proxy, see [Deployment](deployment.md).
+1 -1
View File
@@ -4,7 +4,7 @@ AegisGitea MCP is a security-first [Model Context Protocol (MCP)](https://modelc
## Overview
AegisGitea MCP acts as a secure bridge between AI assistants (such as ChatGPT) and your Gitea instance. It exposes a limited set of read-only tools that allow an AI to browse repositories and read file contents, while enforcing strict authentication, rate limiting, and comprehensive audit logging.
AegisGitea MCP acts as a secure bridge between AI assistants (such as Claude, Claude Code, or Cowork) and your Gitea instance. It exposes read tools and opt-in write tools while enforcing per-user OAuth, repository authorization, policy checks, rate limiting, and tamper-evident audit logging.
**Version:** 0.1.0 (Alpha)
**License:** MIT
+15 -14
View File
@@ -1,15 +1,15 @@
# Troubleshooting
## "Internal server error (-32603)" from ChatGPT
## "Internal server error (-32603)" from Claude
**Symptom:** ChatGPT shows `Internal server error` with JSON-RPC error code `-32603` when trying to use Gitea tools.
**Symptom:** Claude shows `Internal server error` with JSON-RPC error code `-32603` when trying to use Gitea tools.
**Cause:** The OAuth token stored by ChatGPT was issued without Gitea API scopes (e.g. `read:repository`). This happens when the initial authorization request didn't include the correct `scope` parameter. The token passes OIDC validation (openid/profile/email) but gets **403 Forbidden** from Gitea's REST API.
**Cause:** In user-token mode, the OAuth token stored by the client may have been issued without Gitea API scopes (e.g. `read:repository`). In service-PAT mode, the call may fail because the authenticated user does not have the required repository permission or the permission probe cannot be completed.
**Fix:**
1. In Gitea: Go to **Settings > Applications > Authorized OAuth2 Applications** and revoke the MCP application.
2. In ChatGPT: Go to **Settings > Connected apps** and disconnect the Gitea integration.
3. Re-authorize: Use the ChatGPT integration again. It will trigger a fresh OAuth flow with the correct scopes (`read:repository`).
2. In Claude: disconnect the MCP server and authenticate again.
3. Re-authorize: Use the MCP connector again. It will trigger a fresh OAuth flow. For repository-targeted calls in service-PAT mode, also verify the signed-in Gitea user has read/write access to the target repository.
**Verification:** Check the server logs for `oauth_auth_summary`. A working token shows:
```
@@ -24,19 +24,19 @@ oauth_token_lacks_api_scope: status=403 login=alice
**Symptom:** Tool calls return 403 with a message about re-authorizing.
**Cause:** Same root cause as above — the OAuth token doesn't have the required Gitea API scopes. The middleware's API scope probe detected this and returned a clear error instead of letting it fail deep in the tool handler.
**Cause:** The OAuth token does not have the required API scope in user-token mode, or the per-user repository permission check denied the request in service-PAT mode.
**Fix:** Same as above — revoke and re-authorize.
**Fix:** Revoke and re-authorize if the token lacks API scope. If the error mentions repository permission, grant the signed-in Gitea user the required repository access or use a repository they can access.
## ChatGPT caches stale tokens
## Claude caches stale tokens
**Symptom:** After fixing the OAuth configuration, ChatGPT still sends the old token.
**Symptom:** After fixing the OAuth configuration, Claude still sends the old token.
**Cause:** ChatGPT caches access tokens and doesn't automatically re-authenticate when the server configuration changes.
**Cause:** The client caches access tokens and doesn't automatically re-authenticate when the server configuration changes.
**Fix:**
1. In ChatGPT: **Settings > Connected apps** > disconnect the integration.
2. Start a new conversation and use the integration again this forces a fresh OAuth flow.
1. Disconnect the server in the client.
2. Start a new conversation and use the integration again - this forces a fresh OAuth flow.
## How OAuth scopes work with Gitea
@@ -48,9 +48,9 @@ Gitea's OAuth2/OIDC implementation uses **granular scopes** for API access:
| `write:repository` | Create/edit issues, PRs, comments, files |
| `openid` | OIDC identity (login, email) |
When an OAuth application requests authorization, the `scope` parameter in the authorize URL determines what permissions the resulting token has. If only OIDC scopes are requested (e.g. `openid profile email`), the token will validate via the userinfo endpoint but will be rejected by Gitea's REST API with 403.
When an OAuth application requests authorization, the `scope` parameter in the authorize URL determines what permissions the resulting token has. If only OIDC scopes are requested (e.g. `openid profile email`), the token can establish identity but may not be usable for direct Gitea REST calls. When `GITEA_TOKEN` is configured, the server uses OIDC for identity and checks the user's repository permission before using the service PAT.
The MCP server's `openapi-gpt.yaml` file controls which scopes ChatGPT requests. Ensure it includes:
The MCP server's OAuth metadata controls which scopes the client requests. Ensure it includes:
```yaml
scopes:
read:repository: "Read access to Gitea repositories"
@@ -73,3 +73,4 @@ Every authenticated request emits a structured log line:
- `api_probe=fail:403` — token lacks API scopes, request rejected with re-auth guidance
- `api_probe=skip:cached` — previous probe passed, cached result used
- `api_probe=skip:error` — network error during probe, request allowed to proceed
- `repository_permission_denied` in the audit log — the user lacks required read/write permission for a service-PAT call