fix(mcp-brain-server): OAuth support so claude.ai connectors can register - #958
fix(mcp-brain-server): OAuth support so claude.ai connectors can register#958PogeystickJoe wants to merge 2 commits into
Conversation
…ors can register The Permit Brain claude.ai connector (https://ubuntu1.tail6b157c.ts.net/sse) has never completed setup. claude.ai's remote MCP connector flow performs OAuth discovery + dynamic client registration before offering to connect; this server had no OAuth routes at all, so registration fails with 'Couldn't register with the sign-in service' every time. Adds a minimal, additive OAuth layer (crates/mcp-brain-server/src/oauth.rs): - GET /.well-known/oauth-authorization-server (RFC 8414 metadata) - GET /.well-known/oauth-protected-resource (RFC 9728 metadata) - POST /register (RFC 7591 dynamic client registration) - GET/POST /authorize (bridges to the EXISTING BRAIN_API_KEY/BRAIN_SYSTEM_KEY as the sign-in credential -- issues an auth code, no new secret material) - POST /token (authorization_code grant, PKCE S256 verified) Deliberately does not touch the existing bearer-key auth path or AppState -- merged in as a separate sub-router with its own state, wired in via .merge(oauth_router) right before the CORS/compression layers. Verified: passes clean (1m21s, 0 errors, 0 warnings) against a fresh checkout of upstream main. Known limitations, flagged for review, not resolved in this draft: - Client/code/token store is in-memory (DashMap), process-lifetime only -- a restart clears registered clients and forces reconnect. Worth promoting to the existing persistent store before relying on this long-term. - verify_system_key() / the memories-API key check is not yet updated to also accept an OAuth-issued access token -- issued tokens exist but nothing consults them yet on the protected endpoints. Left out on purpose to avoid touching the existing, working auth path without review. - This ALSO requires the host to actually be reachable from the public internet (Tailscale Funnel, not just tailnet-only Serve) -- confirmed via Anthropic's own docs that claude.ai's remote MCP connections originate from Anthropic's cloud infrastructure, not the user's browser/device. That's an infra change, not a code change; not made here, flagged in the companion issue/ADR for an explicit decision. Fixes the connector registration failure referenced by claude.ai error IDs ofid_b644a31dfbf5d339 and ofid_5d625b13463e61cc. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_014J3L618BmhC1udYcqkXJur
…the OAuth default brain_base_url() fell back to a hardcoded private hostname when BRAIN_PUBLIC_URL was unset. That is deployment configuration, not a library default, and it does not belong compiled into a general-purpose crate: any operator who forgot to set the env var would silently publish someone else's host inside their own RFC 8414 metadata document. Falls back to http://127.0.0.1:3000 instead, so a local `cargo run` works out of the box and a misconfigured deployment fails visibly against itself rather than advertising a third party. No behaviour change for anyone who sets BRAIN_PUBLIC_URL, which every real deployment must.
Correction, filed same day — please read before reviewing "Fix 2" contextThis PR's companion ADR originally presented public Tailscale Funnel exposure as a reasonable option alongside tailnet-only. That was wrong, filed without checking this repo's/permit-platform's prior brain-security history first. Checked now: permitplace/permit-platform#609 and #626 confirmed a real cross-client data leak in this exact brain corpus (a query for an Austin, TX permit returned a hit carrying another client's real name/project/researcher metadata). The fix for that — This PR's OAuth fix is still correct and needed — the connector genuinely can't register without it, that part doesn't change. But once OAuth works, if the server is ever made publicly reachable (which it also needs to be, for claude.ai to reach it at all — separate infra decision, not part of this PR), a direct MCP connection would bypass the guard and hit the raw, unscoped corpus. Worth deciding, before merge or at least before any Funnel decision, whether an equivalent scoping/PII check should live inside Full writeup: permitplace/permit-platform#706 (ADR-181) and permitplace/permit-platform#705 (issue, updated with the same correction). |
ruvnet
left a comment
There was a problem hiding this comment.
Dream Machine security review — REJECT
Frozen hypothesis: this branch provides an OAuth 2.1-compatible Claude.ai connection path while preserving authorization integrity.
The reachable source contradicts that claim. Registered callback binding and PKCE are not enforced end-to-end, issued bearer tokens are not yet validated by the protected route, and all six exact-head workflows require authorization. That leaves both interoperability and the authorization boundary unverified. I am intentionally withholding reproduction details from this public review.
Before this can be made ready: handle the security-sensitive gaps through a private advisory/review path; add negative tests for callback binding, PKCE, client/code/token binding, replay, expiry, and audience; add a full register → authorize → token → protected-resource test; require native, WASM, supply-chain, regression, formatting, and Workspace CI on this exact head.
What this is
The problem. The "Permit Brain" claude.ai connector (
https://ubuntu1.tail6b157c.ts.net/sse) has never successfully connected. Every attempt fails with "Couldn't register with Permit Brain's sign-in service... add an OAuth Client ID in the connector settings" (claude.ai error refsofid_b644a31dfbf5d339,ofid_5d625b13463e61cc).The fix.
mcp-brain-serverhas zero OAuth support — confirmed by grepping its full route table, nothing under/.well-known/oauth-*or/registerexists. claude.ai's remote-connector setup performs OAuth discovery + dynamic client registration (RFC 8414 / RFC 7591) before it will offer to connect at all, so against a server with none of that, registration fails outright, before any query is ever attempted. This PR adds a minimal, additive OAuth layer.Why it matters. Nobody has been able to use this brain from claude.ai at all, ever, via this connector — not a degraded experience, a complete non-start.
Risk. Low for existing functionality — the new routes are merged as a separate sub-router with their own state; nothing in the existing bearer-key auth path,
AppState, or any existing route is touched. Compiles clean. Real risk is in the new surface itself (see Known limitations below) — this is a draft for review, not asserted production-ready.Evidence (verified directly, live, on the actual host — not inferred)
grep -n "oauth\|/register" crates/mcp-brain-server/src/routes.rs→ zero hits besides the word "authentication" in an unrelated string.curl https://ubuntu1.tail6b157c.ts.net/...fails to connect (curl: (7)), andtailscale serve statuson the host returns "No serve config" (as of 2026-09-02, before this session enabled tailnet-only Serve as a diagnostic step).tailscale serve). That's an infra decision, not a code change, and is not made in this PR — flagged in the companion issue for an explicit call, since it makes a private data store publicly reachable and shouldn't happen as a side effect of a code PR.cargo check -p mcp-brain-serveragainst this branch, fresh checkout of upstreammain— 0 errors, 0 warnings, 1m21s.Known limitations (flagged for review, not resolved here)
DashMap), process-lifetime only. A restart clears every registered client and forces re-registration in claude.ai. Fine for a first draft; should move to the existing persistent store before relying on this long-term.verify_system_key()(and the memories-API key check) is not yet updated to also accept an OAuth-issued access token — tokens are issued but nothing on the protected/v1/*endpoints consults them yet. Left out deliberately to avoid touching the existing, working auth path without your review of the approach first.Companion artifacts
docs/adr/ADR-179-permit-brain-oauth-connector.mdAssigning to @ruvnet as a fix request per Mike — happy to adjust the approach (e.g. persistent store, wiring the token check into the existing auth path, PKCE requirements) to match your conventions elsewhere in this crate.
🤖 Generated with claude-flow
https://claude.ai/code/session_014J3L618BmhC1udYcqkXJur