feat(ingestion): add OAuth 2.0 client credentials flow to brain MCP server #5
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.