claude.ai only supports OAuth 2.0 for remote MCP servers — no static Bearer token option in the UI (only client ID + client secret fields). The brain MCP endpoint at https://brain-mcp.d-ma.be/mcp currently uses a static Bearer token (BRAIN_MCP_TOKEN), which claude.ai cannot use.
Result: "Couldn't reach the MCP server" in claude.ai — the server returns 401 with no WWW-Authenticate header, so claude.ai has no way to authenticate.
Solution
Implement a minimal OAuth 2.0 client credentials flow on the ingestion server. claude.ai exchanges a client ID + secret for an access token, then uses that token as the Bearer on /mcp. The existing BearerAuth middleware stays unchanged — the issued token is just BRAIN_MCP_TOKEN.
What to build
New package: ingestion/internal/oauth/
metadata.go — serves GET /.well-known/oauth-authorization-server:
## Problem
claude.ai only supports OAuth 2.0 for remote MCP servers — no static Bearer token option in the UI (only client ID + client secret fields). The brain MCP endpoint at `https://brain-mcp.d-ma.be/mcp` currently uses a static Bearer token (`BRAIN_MCP_TOKEN`), which claude.ai cannot use.
Result: "Couldn't reach the MCP server" in claude.ai — the server returns `401` with no `WWW-Authenticate` header, so claude.ai has no way to authenticate.
## Solution
Implement a minimal OAuth 2.0 client credentials flow on the ingestion server. claude.ai exchanges a client ID + secret for an access token, then uses that token as the Bearer on `/mcp`. The existing `BearerAuth` middleware stays unchanged — the issued token is just `BRAIN_MCP_TOKEN`.
## What to build
### New package: `ingestion/internal/oauth/`
**`metadata.go`** — serves `GET /.well-known/oauth-authorization-server`:
```json
{
"issuer": "https://brain-mcp.d-ma.be",
"token_endpoint": "https://brain-mcp.d-ma.be/oauth/token",
"grant_types_supported": ["client_credentials"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"]
}
```
**`token.go`** — serves `POST /oauth/token`:
- Accepts `grant_type=client_credentials` + `client_id` + `client_secret` (form-encoded or Basic auth)
- Validates against `OAUTH_CLIENT_ID` + `OAUTH_CLIENT_SECRET` env vars
- Returns `{"access_token": "<BRAIN_MCP_TOKEN>", "token_type": "bearer"}`
- Returns `400`/`401` on bad grant type or wrong credentials
### Routes added to `ingestion/cmd/server/main.go`
```
GET /.well-known/oauth-authorization-server (no auth)
POST /oauth/token (no auth — credentials in body)
```
Existing `/mcp` route with `BearerAuth` unchanged.
### New env vars
| Var | Purpose |
|-----|---------|
| `OAUTH_CLIENT_ID` | Client ID to register for claude.ai |
| `OAUTH_CLIENT_SECRET` | Client secret to register for claude.ai |
Both required at startup (exit 1 if missing, same pattern as `BRAIN_MCP_TOKEN`).
## Setup after shipping
1. Set `OAUTH_CLIENT_ID` + `OAUTH_CLIENT_SECRET` in the ingestion pod Secret (SOPS-encrypted in infra repo)
2. In claude.ai Settings → Integrations: URL = `https://brain-mcp.d-ma.be/mcp`, client ID + secret matching above
3. claude.ai discovers token endpoint → exchanges credentials → Bearer works
## Out of scope
- Authorization code flow (not needed for single-user personal server)
- JWT tokens (static `BRAIN_MCP_TOKEN` is sufficient)
- Token expiry / refresh (long-lived token acceptable here)
Superseded by #6. Dex is already deployed at auth.d-ma.be — the correct approach is JWT validation middleware with static token fallback, not a bespoke client credentials server in the ingestion pod. Closing in favour of #6.
Superseded by #6. Dex is already deployed at `auth.d-ma.be` — the correct approach is JWT validation middleware with static token fallback, not a bespoke client credentials server in the ingestion pod. Closing in favour of #6.
Shipped in 58c5741. CI/CD auto-rebuilds the ingestion image and updates infra.
What landed
New package ingestion/internal/oauth:
MetadataHandler(issuer) — GET /.well-known/oauth-authorization-server
TokenHandler(cfg) — POST /oauth/token with client_credentials grant only
Wiring in cmd/server/main.go: routes mount only when bothOAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET are set. Setting one alone → exit 1 (deliberate fail-fast on misconfiguration). MCP_RESOURCE_URL doubles as the issuer.
The access token returned is BRAIN_MCP_TOKEN — no JWT machinery downstream, so the dual-auth middleware shipped in #9 already accepts it.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
claude.ai only supports OAuth 2.0 for remote MCP servers — no static Bearer token option in the UI (only client ID + client secret fields). The brain MCP endpoint at
https://brain-mcp.d-ma.be/mcpcurrently uses a static Bearer token (BRAIN_MCP_TOKEN), which claude.ai cannot use.Result: "Couldn't reach the MCP server" in claude.ai — the server returns
401with noWWW-Authenticateheader, so claude.ai has no way to authenticate.Solution
Implement a minimal OAuth 2.0 client credentials flow on the ingestion server. claude.ai exchanges a client ID + secret for an access token, then uses that token as the Bearer on
/mcp. The existingBearerAuthmiddleware stays unchanged — the issued token is justBRAIN_MCP_TOKEN.What to build
New package:
ingestion/internal/oauth/metadata.go— servesGET /.well-known/oauth-authorization-server:token.go— servesPOST /oauth/token:grant_type=client_credentials+client_id+client_secret(form-encoded or Basic auth)OAUTH_CLIENT_ID+OAUTH_CLIENT_SECRETenv vars{"access_token": "<BRAIN_MCP_TOKEN>", "token_type": "bearer"}400/401on bad grant type or wrong credentialsRoutes added to
ingestion/cmd/server/main.goExisting
/mcproute withBearerAuthunchanged.New env vars
OAUTH_CLIENT_IDOAUTH_CLIENT_SECRETBoth required at startup (exit 1 if missing, same pattern as
BRAIN_MCP_TOKEN).Setup after shipping
OAUTH_CLIENT_ID+OAUTH_CLIENT_SECRETin the ingestion pod Secret (SOPS-encrypted in infra repo)https://brain-mcp.d-ma.be/mcp, client ID + secret matching aboveOut of scope
BRAIN_MCP_TOKENis sufficient)Superseded by #6. Dex is already deployed at
auth.d-ma.be— the correct approach is JWT validation middleware with static token fallback, not a bespoke client credentials server in the ingestion pod. Closing in favour of #6.Shipped in
58c5741. CI/CD auto-rebuilds the ingestion image and updates infra.What landed
New package
ingestion/internal/oauth:MetadataHandler(issuer)—GET /.well-known/oauth-authorization-serverTokenHandler(cfg)—POST /oauth/tokenwithclient_credentialsgrant onlyWiring in
cmd/server/main.go: routes mount only when bothOAUTH_CLIENT_IDandOAUTH_CLIENT_SECRETare set. Setting one alone → exit 1 (deliberate fail-fast on misconfiguration).MCP_RESOURCE_URLdoubles as the issuer.The access token returned is
BRAIN_MCP_TOKEN— no JWT machinery downstream, so the dual-auth middleware shipped in #9 already accepts it.Acceptance criteria
Tests (
ingestion/internal/oauth/{metadata,token}_test.go):client_secret_post: form-encodedclient_id/client_secret→ 200 +{access_token, token_type: "bearer"}client_secret_basic: HTTP Basic header → 200client_secret→ 401 +{"error":"invalid_client"}grant_type(e.g.password) → 400 +{"error":"unsupported_grant_type"}:→ 401Setup before claude.ai can use it
OAUTH_CLIENT_ID=brain-mcp-claudeai,OAUTH_CLIENT_SECRET=$(openssl rand -hex 32)infra/k3s/apps/supervisor/secrets.enc.yamlvia SOPSsecrets-revisionannotation bump or pod restart)https://brain-mcp.d-ma.be/mcp, client ID + secret matching above/.well-known/oauth-authorization-server→ exchanges creds → uses Bearer on/mcpClosing.