Skip to content

fix(mcp-brain-server): OAuth support so claude.ai connectors can register - #958

Draft
PogeystickJoe wants to merge 2 commits into
ruvnet:mainfrom
PogeystickJoe:fix/mcp-brain-server-oauth-connector
Draft

fix(mcp-brain-server): OAuth support so claude.ai connectors can register#958
PogeystickJoe wants to merge 2 commits into
ruvnet:mainfrom
PogeystickJoe:fix/mcp-brain-server-oauth-connector

Conversation

@PogeystickJoe

@PogeystickJoe PogeystickJoe commented Sep 2, 2026

Copy link
Copy Markdown

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 refs ofid_b644a31dfbf5d339, ofid_5d625b13463e61cc).

The fix. mcp-brain-server has zero OAuth support — confirmed by grepping its full route table, nothing under /.well-known/oauth-* or /register exists. 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)

  • No OAuth routes exist: grep -n "oauth\|/register" crates/mcp-brain-server/src/routes.rs → zero hits besides the word "authentication" in an unrelated string.
  • No HTTPS listener currently serves this hostname either: curl https://ubuntu1.tail6b157c.ts.net/... fails to connect (curl: (7)), and tailscale serve status on the host returns "No serve config" (as of 2026-09-02, before this session enabled tailnet-only Serve as a diagnostic step).
  • Anthropic's own docs confirm (MCP connector docs, custom connectors help) that claude.ai's remote MCP connections originate from Anthropic's cloud infrastructure, not the user's browser/device, and that the target server "must be reachable over the public internet from Anthropic's IP ranges." A private/VPN/tailnet-only host — even one the user can reach fine themselves — will not connect. This means the OAuth fix alone is not sufficient; the host also needs genuine public reachability (Tailscale Funnel, not just 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.
  • Compiles clean: cargo check -p mcp-brain-server against this branch, fresh checkout of upstream main — 0 errors, 0 warnings, 1m21s.

Known limitations (flagged for review, not resolved here)

  1. Client/auth-code/token store is in-memory (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.
  2. 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.
  3. Requires the public-reachability infra change noted above to actually work end-to-end from claude.ai.

Companion artifacts

  • Issue with full repro/evidence: permitplace/permit-platform#705
  • ADR with design + pseudocode: permitplace/permit-platform docs/adr/ADR-179-permit-brain-oauth-connector.md

Assigning 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

PogeystickJoe and others added 2 commits September 2, 2026 16:13
…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.
@PogeystickJoe

Copy link
Copy Markdown
Author

Correction, filed same day — please read before reviewing "Fix 2" context

This 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 — guardedBrainSearch() — was built and merged, but it lives in the Cloud Run agent's own code (cloud-run/permit-research-agent/lib/brain-guard.js in permit-platform), not in mcp-brain-server at all. I checked this whole checkout for anything guard/scoping-related and found nothing.

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 mcp-brain-server itself so it applies regardless of which path reaches it (Cloud Run or direct MCP). Happy to take a pass at that too if useful, or defer entirely to your judgment on how it should fit alongside the existing guard design.

Full writeup: permitplace/permit-platform#706 (ADR-181) and permitplace/permit-platform#705 (issue, updated with the same correction).

@ruvnet ruvnet left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants