feat(mcp): OAuth 2.1 sign-in for connectors
Adds OAuth 2.1 (RFC 9728 discovery + RFC 7591 dynamic client registration +
PKCE) so spec-conformant MCP servers like https://legaldatahunter.com/mcp
work without the user pasting any token.
The MCP TypeScript SDK does almost all the heavy lifting via its `auth()`
helper - discovery, DCR, PKCE, code exchange, refresh. We only have to plug
in an OAuthClientProvider whose getters/setters read and write the row's
oauth_* columns, plus an HMAC-signed state token so the popup callback can
look the row up without a server-side session.
DB
- migration 002 + inline patch to the one-shot:
alter table user_mcp_servers
add auth_type ('headers'|'oauth' default 'headers'),
add oauth_metadata jsonb,
add oauth_tokens jsonb,
add oauth_code_verifier text;
Backend
- New `lib/mcp/oauth.ts`:
- `DbOAuthProvider` implements OAuthClientProvider, persists everything
on the user_mcp_servers row.
- "initiate" mode (used by /oauth/start) captures the authorize URL into
a property so the route can return it for the popup; "use" mode (used
by chat) throws ReauthRequiredError when the SDK wants the user back,
so the caller can mark the row reauth_required.
- signOAuthState/verifyOAuthState - HMAC over user_id+server_id (5 min
TTL) reusing DOWNLOAD_SIGNING_SECRET. No DB round-trip on callback.
- `lib/mcp/client.ts`: accepts an optional authProvider passed through to
StreamableHTTPClientTransport - the SDK auto-attaches Authorization
headers and auto-refreshes on 401.
- `lib/mcp/servers.ts`: builds a DbOAuthProvider for OAuth rows that have
tokens; rows without tokens are skipped (UI surfaces a "Sign in" button
in settings instead).
- New `routes/mcpOauth.ts` mounted at /mcp/oauth: public callback that
verifies state, finishes the SDK auth() flow, and returns a small HTML
page that postMessage()s the opener and closes the popup.
- `routes/mcpServers.ts`:
- POST /:id/oauth/start kicks off discovery + DCR via the SDK and
returns { authorize_url } for the frontend popup.
- POST creates honor `auth_type`; PATCH/test/list now project + return
auth_type and a boolean oauth_authorized (the access_token itself
never round-trips to the browser).
- `BACKEND_PUBLIC_URL` env var (defaults to http://localhost:${PORT}) used
to build the OAuth redirect URI; documented in `.env.example`.
Frontend
- `account/mcp/page.tsx`:
- Authentication mode radio in the Add form: "API key / headers" vs
"OAuth (auto-discover)". Headers section hides itself in OAuth mode.
- Save button label switches to "Save & sign in" for OAuth, which
immediately opens the authorize popup. The page polls listMcpServers
until oauth_authorized flips, then auto-runs tool discovery.
- Per-card status pills: "OAuth · signed in" (blue) / "OAuth · sign-in
required" (amber). Cards in the latter state show a "Sign in" button
instead of "Test".
- Simplified copy per user feedback: dropped the OAuth explainer block,
redundant "By saving..." trust pill, and helper text under Name and
URL inputs. Single load-bearing trust warning at top of page remains.
- `mikeApi.ts`: `startMcpOauth(id)` wrapper.
Security notes for reviewers
- access_token / refresh_token / oauth_metadata are stored at-rest in
jsonb (RLS owner-only). Per-row encryption deferred to a separate
hardening PR - matches existing precedent for user_profiles.{claude,
gemini}_api_key.
- State token is HMAC-signed with DOWNLOAD_SIGNING_SECRET, 5 min TTL,
carries user_id + server_id only. CSRF-safe across the popup hop with
no server-side session needed.
- Public client (token_endpoint_auth_method=none, PKCE-protected) - no
client secret needed for confidential storage.
| Repository | nforum/mike |
|---|---|
| Author | Zacharie Laik <zacharie@goodlegal.fr> |
| Authored | |
| Parents | fad06aca |
| Stats | 12 files changed , +734 , -48 |
| Part of | MCP "Connectors": user-configurable MCP servers with OAuth 2.1 |
Capture this commit into my fork
Download a Markdown prompt that tells Claude how to port this
exact commit into your working tree. Run it via
claude -p < capture-commit-52749e6e.md
from inside the repo you want the change in.