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
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:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user