b275f5c0c2
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
176 lines
5.7 KiB
Markdown
176 lines
5.7 KiB
Markdown
# AegisGitea-MCP
|
|
|
|
Security-first MCP server for self-hosted Gitea with per-user OAuth2/OIDC authentication for Claude, Claude Code, and Cowork.
|
|
|
|
AegisGitea-MCP exposes MCP tools over Streamable HTTP and a legacy SSE alias. Each user authenticates with Gitea through OAuth2/OIDC; repository authorization is checked per user before any service PAT call is allowed.
|
|
|
|
## 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 the redirect URI to this MCP server's callback: `https://<your-mcp-domain>/oauth/callback`.
|
|
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>
|
|
PUBLIC_BASE_URL=https://<your-mcp-domain>
|
|
OAUTH_STATE_SECRET=<random-32-byte-minimum-secret>
|
|
```
|
|
|
|
`GITEA_TOKEN` is optional in OAuth mode. Without it, Gitea REST calls use the user's OAuth access token directly, so Gitea enforces permissions on every API call. With it, the token acts as a service PAT for API execution, but the MCP server first checks the requesting user's permission on the target repository through Gitea and denies the call if the user lacks the required read/write permission.
|
|
|
|
### 3) Configure Claude, Claude Code, or Cowork
|
|
|
|
Claude's hosted, desktop, mobile, Claude Code, and Cowork surfaces share the same remote MCP connector infrastructure. There is no Claude-specific server code path.
|
|
|
|
In claude.ai:
|
|
|
|
1. Open **Settings > Connectors**.
|
|
2. Choose **Add custom connector**.
|
|
3. Paste `https://<your-mcp-domain>/mcp`.
|
|
4. Complete the OAuth consent flow. Dynamic Client Registration (`/register`) handles Claude client registration.
|
|
|
|
In Claude Code:
|
|
|
|
```bash
|
|
claude mcp add --transport http aegis-gitea https://<your-mcp-domain>/mcp
|
|
```
|
|
|
|
Cowork uses the same connector model and MCP URL.
|
|
|
|
Manual OAuth client configuration remains available for clients that do not use DCR:
|
|
|
|
- MCP server URL: `https://<your-mcp-domain>/mcp`
|
|
- Authentication: OAuth
|
|
- OAuth client ID: the client id returned by `/register` or your preconfigured client id
|
|
- OAuth client secret: only for confidential clients
|
|
|
|
Hosted Claude callbacks are allowed by default: `https://claude.ai/api/mcp/auth_callback` and `https://claude.com/api/mcp/auth_callback`. Loopback redirects for Claude Code local development are allowed for `http://127.0.0.1:*` and `http://localhost:*`.
|
|
|
|
### 4) OAuth-protected MCP behavior
|
|
|
|
The server publishes protected-resource metadata:
|
|
|
|
- `GET /.well-known/oauth-protected-resource`
|
|
|
|
Example response:
|
|
|
|
```json
|
|
{
|
|
"resource": "https://gitea-mcp.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
|
|
Claude / Claude Code / Cowork
|
|
-> Authorization Code Flow
|
|
-> Gitea OAuth2/OIDC (issuer: https://git.hiddenden.cafe)
|
|
-> Access token
|
|
-> MCP Server (/mcp, /mcp/sse, /mcp/tool/call)
|
|
-> OIDC discovery + JWKS cache
|
|
-> Scope enforcement (read:repository / write:repository)
|
|
-> Policy allow/deny
|
|
-> If GITEA_TOKEN is set: check Gitea collaborator permission for <user, repo>
|
|
-> Gitea API call with either the user token or the service PAT after authz
|
|
```
|
|
|
|
## 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":"get_repository_info","arguments":{"owner":"acme","repo":"demo"}}'
|
|
```
|
|
|
|
## 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:
|
|
- identity comes from Gitea OIDC/JWKS or userinfo validation.
|
|
- without `GITEA_TOKEN`, API calls use the user's token and Gitea enforces permissions.
|
|
- with `GITEA_TOKEN`, every repository-targeted call first checks the user's Gitea permission and fails closed if the check cannot be made.
|
|
|
|
## 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`
|