claude.ai on Forgejo, signed in through Forseti
Connect claude.ai to a self-hosted Forgejo with forgejo-mcp and Forseti SSO, without a personal access token.
I wanted claude.ai to read my repositories on a self-hosted Forgejo, without pasting a personal access token into a connector. Turns out Forgejo 16 added the missing piece; Forseti needed one more.
Versions
- Forseti 0.2.24 or newer: access-token claims on resource registry rows
- Forgejo 17.0.1 (tested); Authorized Integrations exist since 16.0
- forgejo-mcp v3.3.0,
resource-servermode - Ory Hydra v26.2.0
How it fits
- claude.ai logs in at Forseti and gets a JWT access token
- The token carries
forgejo_aud: the audience of your Forgejo Authorized Integration - forgejo-mcp checks the token, signs a 5-minute JWT for you, calls Forgejo with it
- Forgejo matches it to your integration and applies its permissions
No forge token anywhere. forgejo-mcp’s one secret is its signing key.
Server (once)
1. forgejo-mcp on its own host, e.g. mcp.git.example.com:
forgejo-mcp:
image: git.b4mad.industries/agentic-forges/forgejo-mcp:v3.3.0
command: ["--transport", "http", "--http-port", "8080"]
environment:
- FORGEJO_URL=https://git.example.com
- FORGEJO_MCP_HOST=0.0.0.0
- FORGEJO_MCP_ALLOWED_HOSTS=mcp.git.example.com
- FORGEJO_MCP_AUTH_MODE=resource-server
- FORGEJO_MCP_AUTHORIZATION_SERVER=https://accounts.example.com/hydra
- FORGEJO_MCP_RESOURCE=https://mcp.git.example.com/mcp
- FORGEJO_MCP_SCOPES_SUPPORTED=openid offline_access
- FORGEJO_MCP_FORGEJO_JWT_ISSUER=https://mcp.git.example.com/issuer
- FORGEJO_MCP_FORGEJO_JWT_SIGNING_KEY_FILE=/run/secrets/signing-key.pem
volumes:
- ./signing-key.pem:/run/secrets/signing-key.pem:ro # EC P-256, owned by 65532
offline_access, or the 5-minute access token means a new login every 5 minutes- Proxy only
/mcp,/.well-known/oauth-protected-resource[/mcp],/issuer/.well-known/openid-configurationand/issuer/jwks.json; 404 the rest, never redirect - The name has to resolve to a public IP, from Forgejo too (it refuses issuers on private addresses)
2. Hydra - lift the claim to the top level of the JWT (otherwise it only sits under ext):
oauth2:
allowed_top_level_claims:
- forgejo_aud
3. Forseti - /admin/resources/new:
- Resource:
https://mcp.git.example.com/mcp - Access-token claims:
forgejo_aud - Only tokens for this resource get the claim; Stackpit, Sortello etc. don’t
Per user
4. Forgejo - Settings → Authorized Integrations → Generic JWT:
- Issuer:
https://mcp.git.example.com/issuer - Claim rules:
{"rules": [{"claim": "sub", "compare": "eq", "value": "<Kratos identity id>"}]} - Permissions: as narrow as you can live with (
read:repository,read:user, …) - Copy the audience it shows:
u:1:<uuid>
5. Forseti admin - store it on the identity (metadata_admin is admin-only):
curl -X PATCH http://kratos:4434/admin/identities/<id> \
-H 'Content-Type: application/json' \
-d '[{"op":"add","path":"/metadata_admin","value":{"forgejo_aud":"u:1:<uuid>"}}]'
This replaces metadata_admin, so carry over existing keys.
6. claude.ai - Settings → Connectors → Add custom connector:
- URL:
https://mcp.git.example.com/mcp - Leave the OAuth fields empty (claude.ai registers itself via CIMD)
- Connect, log in, approve; ask “Who am I on git.example.com?”
Gotchas
- The
subrule isn’t optional. Forgejo finds the integration by issuer and audience alone; without the rule, anyone carrying your audience acts as you. - The claim is fixed at consent. Changed the audience? Disconnect and reconnect - a refresh keeps the old one.
- The integration’s permissions are the only limit. forgejo-mcp exposes every tool to whoever logs in.
- The signing key acts for every user whose integration trusts the issuer. Treat it like an admin token.
- Org-scoped Forseti admins can’t set claims (by design); only instance admins.
- No admin UI for
metadata_adminyet, hence the curl. If this gets used, a field on the identity page comes next.