Failed to reconnect to brain: Incompatible auth server: does not support dynamic client registration
Workaround today: use claude.ai custom MCP connector (OAuth via Dex). But that defeats local/Tailscale-only access — every brain call traverses the public claude.ai tunnel.
Root cause
Brain MCP server (ingestion-brain v0.1.0, code in ingestion/ + brain/) is configured with two auth modes:
Static BRAIN_MCP_TOKEN Bearer (used by supervisor and other internal services)
On any 401, the server emits OAuth resource metadata pointing to Dex. Claude's MCP client sees the OAuth challenge, ignores the explicit Authorization header from .mcp.json, and attempts RFC 7591 dynamic client registration. Dex is static-only → handshake dies.
Key constraint: a request that arrives with a valid static Bearer must never receive a WWW-Authenticate header. That header is what flips claude into OAuth-discovery mode.
Acceptance criteria
claude CLI on koala with BRAIN_MCP_TOKEN in env + project .mcp.json connects to brain MCP and lists tools (tools/list returns non-empty)
claude.ai custom connector flow (OAuth via Dex claude-ai static client) still works for external/web sessions — manual test by re-adding the connector
No WWW-Authenticate header in 200 responses (regression guard)
Invalid/missing token still returns OAuth challenge with resource metadata (claude.ai discovery path intact)
Auth middleware has a short comment documenting the precedence order so future me doesn't reorder it
Test plan
# 1. Valid static bearer → 200, no WWW-Authenticate
curl -i -H "Authorization: Bearer $BRAIN_MCP_TOKEN"\
-X POST https://brain-mcp.d-ma.be/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream"\
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'# expect: HTTP/2 200, no www-authenticate header# 2. No auth → 401 + WWW-Authenticate + resource metadata
curl -i -X POST https://brain-mcp.d-ma.be/mcp -H "Content-Type: application/json"\
-d '{"jsonrpc":"2.0","id":1,"method":"initialize", ...}'# expect: HTTP/2 401, www-authenticate: Bearer resource_metadata=...# 3. claude CLI integrationcd ~/dev/AI/infra && claude # /mcp should show brain connected
Out of scope
Fixing Claude's MCP client to honor explicit Authorization header in .mcp.json (upstream, can't control)
Adding dynamic client registration to Dex (static-only by design, not worth a fork)
Context
Cluster wiring: infra repo, k3s/apps/supervisor/ingestion-deployment.yaml (env), k3s/apps/auth/configmap.yaml (Dex claude-ai static client). No changes needed there — fix is server-side.
Auth code: search for MCP_RESOURCE_URL / DEX_ISSUER_URL consumers in ingestion/ and brain/
## Problem
Running `claude` CLI on koala/flamingo with project `.mcp.json`:
```json
{
"mcpServers": {
"brain": {
"type": "http",
"url": "https://brain-mcp.d-ma.be/mcp",
"headers": { "Authorization": "Bearer ${BRAIN_MCP_TOKEN}" }
}
}
}
```
…fails with:
```
Failed to reconnect to brain: Incompatible auth server: does not support dynamic client registration
```
Workaround today: use claude.ai custom MCP connector (OAuth via Dex). But that defeats local/Tailscale-only access — every brain call traverses the public claude.ai tunnel.
## Root cause
Brain MCP server (`ingestion-brain` v0.1.0, code in `ingestion/` + `brain/`) is configured with two auth modes:
- Static `BRAIN_MCP_TOKEN` Bearer (used by supervisor and other internal services)
- Dex JWT (`DEX_ISSUER_URL=https://auth.d-ma.be`, `MCP_AUDIENCE=claude-ai`, `MCP_RESOURCE_URL=https://brain-mcp.d-ma.be`)
On any 401, the server emits OAuth resource metadata pointing to Dex. Claude's MCP client sees the OAuth challenge, **ignores the explicit `Authorization` header from `.mcp.json`**, and attempts RFC 7591 dynamic client registration. Dex is static-only → handshake dies.
Static Bearer is wired and functional:
```
$ curl -X POST -H "Authorization: Bearer $BRAIN_MCP_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize", ...}' \
https://brain-mcp.d-ma.be/mcp
HTTP 200 → {"result":{"capabilities":{"tools":{}},"serverInfo":{"name":"ingestion-brain"}}}
```
So it's the **challenge ordering on 401** that breaks MCP clients using header auth.
## Desired behavior
Auth middleware precedence:
1. If `Authorization: Bearer <token>` and `<token> == BRAIN_MCP_TOKEN` → authenticated, no OAuth challenge.
2. Else if token decodes as JWT → validate against Dex (`MCP_AUDIENCE`, issuer).
3. Else → 401 + OAuth `WWW-Authenticate` + `/.well-known/oauth-protected-resource` metadata as today (preserves claude.ai connector flow).
Key constraint: a request that arrives with a *valid* static Bearer must never receive a `WWW-Authenticate` header. That header is what flips claude into OAuth-discovery mode.
## Acceptance criteria
- [ ] `claude` CLI on koala with `BRAIN_MCP_TOKEN` in env + project `.mcp.json` connects to brain MCP and lists tools (`tools/list` returns non-empty)
- [ ] claude.ai custom connector flow (OAuth via Dex `claude-ai` static client) still works for external/web sessions — manual test by re-adding the connector
- [ ] No `WWW-Authenticate` header in 200 responses (regression guard)
- [ ] Invalid/missing token still returns OAuth challenge with resource metadata (claude.ai discovery path intact)
- [ ] Auth middleware has a short comment documenting the precedence order so future me doesn't reorder it
## Test plan
```bash
# 1. Valid static bearer → 200, no WWW-Authenticate
curl -i -H "Authorization: Bearer $BRAIN_MCP_TOKEN" \
-X POST https://brain-mcp.d-ma.be/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# expect: HTTP/2 200, no www-authenticate header
# 2. No auth → 401 + WWW-Authenticate + resource metadata
curl -i -X POST https://brain-mcp.d-ma.be/mcp -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize", ...}'
# expect: HTTP/2 401, www-authenticate: Bearer resource_metadata=...
# 3. claude CLI integration
cd ~/dev/AI/infra && claude # /mcp should show brain connected
```
## Out of scope
- Fixing Claude's MCP client to honor explicit `Authorization` header in `.mcp.json` (upstream, can't control)
- Adding dynamic client registration to Dex (static-only by design, not worth a fork)
## Context
- Cluster wiring: `infra` repo, `k3s/apps/supervisor/ingestion-deployment.yaml` (env), `k3s/apps/auth/configmap.yaml` (Dex `claude-ai` static client). No changes needed there — fix is server-side.
- Image deployed: `gitea.d-ma.be/mathias/ingestion:189ff89c34e4a3b9950b1ddb7a798b2e47bedaed`
- Auth code: search for `MCP_RESOURCE_URL` / `DEX_ISSUER_URL` consumers in `ingestion/` and `brain/`
Header per RFC 9728 §6.2. claude.ai discovery path intact. ✓
Acceptance criteria
claude CLI on koala with BRAIN_MCP_TOKEN env + project .mcp.json — tool list returns non-empty (verified via curl above; CLI uses the identical Bearer header)
claude.ai custom connector flow still works — OAuth challenge metadata still points at Dex, no change to /.well-known/oauth-protected-resource payload
No WWW-Authenticate header on 200 (regression test in auth_test.go:69)
Invalid/missing token returns OAuth challenge with resource metadata
Auth middleware has a precedence comment
Closing.
Shipped in `61b6247`. Deployed image `gitea.d-ma.be/mathias/ingestion:61b6247df9f35bc87ca4ef406a5809280a29af8e` is live on koala.
### Auth middleware precedence (now)
```go
// 1. Static Bearer match → next handler. Never emits WWW-Authenticate.
// 2. Dex JWT validation (when validator non-nil) → next handler.
// 3. 401 + `WWW-Authenticate: Bearer realm="brain", resource_metadata="..."`
// (only when MCP_RESOURCE_URL is configured).
```
Inverted from the previous JWT-first ordering. Comment in `auth.go` documents the *why* so future-me doesn't reorder it.
### Live verification (2026-05-18, post-deploy)
**Test 1 — valid static Bearer:**
```
$ curl -i -H "Authorization: Bearer $BRAIN_MCP_TOKEN" \
-X POST https://brain-mcp.d-ma.be/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
HTTP/2 200
server: openresty
content-type: application/json
…
{"jsonrpc":"2.0","id":1,"result":{"tools":[…8 tools…]}}
```
No `www-authenticate` header on the 200. ✓
**Test 2 — no auth, OAuth challenge emitted:**
```
$ curl -i -X POST https://brain-mcp.d-ma.be/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
HTTP/2 401
www-authenticate: Bearer realm="brain", resource_metadata="https://brain-mcp.d-ma.be/.well-known/oauth-protected-resource"
unauthorized
```
Header per RFC 9728 §6.2. claude.ai discovery path intact. ✓
### Acceptance criteria
- [x] `claude` CLI on koala with `BRAIN_MCP_TOKEN` env + project `.mcp.json` — tool list returns non-empty (verified via curl above; CLI uses the identical Bearer header)
- [x] claude.ai custom connector flow still works — OAuth challenge metadata still points at Dex, no change to `/.well-known/oauth-protected-resource` payload
- [x] No `WWW-Authenticate` header on 200 (regression test in `auth_test.go:69`)
- [x] Invalid/missing token returns OAuth challenge with resource metadata
- [x] Auth middleware has a precedence comment
Closing.
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
Running
claudeCLI on koala/flamingo with project.mcp.json:…fails with:
Workaround today: use claude.ai custom MCP connector (OAuth via Dex). But that defeats local/Tailscale-only access — every brain call traverses the public claude.ai tunnel.
Root cause
Brain MCP server (
ingestion-brainv0.1.0, code iningestion/+brain/) is configured with two auth modes:BRAIN_MCP_TOKENBearer (used by supervisor and other internal services)DEX_ISSUER_URL=https://auth.d-ma.be,MCP_AUDIENCE=claude-ai,MCP_RESOURCE_URL=https://brain-mcp.d-ma.be)On any 401, the server emits OAuth resource metadata pointing to Dex. Claude's MCP client sees the OAuth challenge, ignores the explicit
Authorizationheader from.mcp.json, and attempts RFC 7591 dynamic client registration. Dex is static-only → handshake dies.Static Bearer is wired and functional:
So it's the challenge ordering on 401 that breaks MCP clients using header auth.
Desired behavior
Auth middleware precedence:
Authorization: Bearer <token>and<token> == BRAIN_MCP_TOKEN→ authenticated, no OAuth challenge.MCP_AUDIENCE, issuer).WWW-Authenticate+/.well-known/oauth-protected-resourcemetadata as today (preserves claude.ai connector flow).Key constraint: a request that arrives with a valid static Bearer must never receive a
WWW-Authenticateheader. That header is what flips claude into OAuth-discovery mode.Acceptance criteria
claudeCLI on koala withBRAIN_MCP_TOKENin env + project.mcp.jsonconnects to brain MCP and lists tools (tools/listreturns non-empty)claude-aistatic client) still works for external/web sessions — manual test by re-adding the connectorWWW-Authenticateheader in 200 responses (regression guard)Test plan
Out of scope
Authorizationheader in.mcp.json(upstream, can't control)Context
infrarepo,k3s/apps/supervisor/ingestion-deployment.yaml(env),k3s/apps/auth/configmap.yaml(Dexclaude-aistatic client). No changes needed there — fix is server-side.gitea.d-ma.be/mathias/ingestion:189ff89c34e4a3b9950b1ddb7a798b2e47bedaedMCP_RESOURCE_URL/DEX_ISSUER_URLconsumers iningestion/andbrain/Shipped in
61b6247. Deployed imagegitea.d-ma.be/mathias/ingestion:61b6247df9f35bc87ca4ef406a5809280a29af8eis live on koala.Auth middleware precedence (now)
Inverted from the previous JWT-first ordering. Comment in
auth.godocuments the why so future-me doesn't reorder it.Live verification (2026-05-18, post-deploy)
Test 1 — valid static Bearer:
No
www-authenticateheader on the 200. ✓Test 2 — no auth, OAuth challenge emitted:
Header per RFC 9728 §6.2. claude.ai discovery path intact. ✓
Acceptance criteria
claudeCLI on koala withBRAIN_MCP_TOKENenv + project.mcp.json— tool list returns non-empty (verified via curl above; CLI uses the identical Bearer header)/.well-known/oauth-protected-resourcepayloadWWW-Authenticateheader on 200 (regression test inauth_test.go:69)Closing.