feat(mcp): add Connectors — URL+headers and OAuth 2.1#32
Open
ZachLaik wants to merge 3 commits intowillchen96:mainfrom
Open
feat(mcp): add Connectors — URL+headers and OAuth 2.1#32ZachLaik wants to merge 3 commits intowillchen96:mainfrom
ZachLaik wants to merge 3 commits intowillchen96:mainfrom
Conversation
Lets users register Streamable-HTTP MCP servers from the Settings page.
Tools discovered from each enabled server are merged into the per-request
tool set under the `mcp__<slug>__<tool>` prefix and dispatched back to the
right server via runToolCalls. Headers (e.g. `Authorization: Bearer ...`)
are stored on the row.
Backend
- New `user_mcp_servers` table (RLS owner-only) with migration 001 + the
same DDL inlined in the one-shot schema.
- `lib/mcp/{client,servers,types}.ts`: thin wrapper around
@modelcontextprotocol/sdk's StreamableHTTPClientTransport, per-request
loader, schema converter (MCP `inputSchema` -> Mike's OpenAIToolSchema)
with 64-char tool-name truncation.
- `runLLMStream` and `runToolCalls` accept an optional `mcpServers` list;
chat routes load + close clients in a try/finally.
- New `routes/mcpServers.ts` mounted at `/user/mcp-servers` with
GET/POST/PATCH/DELETE plus `/test` for connect-and-list-tools probing.
All handlers filter by user_id since the backend uses the service role
key.
Frontend
- New `account/mcp` settings tab and page: add/edit/delete servers, toggle
enabled, run test connection. Header values are masked in the form
(type=password) and the GET endpoint returns header keys only.
- `mikeApi.ts`: typed CRUD wrappers.
Notes for review
- Header values are stored via the same RLS-only model used today for
`user_profiles.claude_api_key`/`gemini_api_key`. Per-row encryption is
a clean follow-up.
- OAuth-protected MCP servers are out of scope for this PR; a follow-up
will add an OAuth 2.1 client (PKCE + dynamic client registration) so
spec-conformant servers (e.g. https://legaldatahunter.com/mcp) work
without manual token paste.
Polish on top of the initial MCP support commit. Same scope (no auth/marketplace yet),
just smoothing the rough edges from a real test session.
UX
- Settings tab + chat-input button renamed to "Connectors". MCP is mentioned in
the page description (with a link to modelcontextprotocol.io) so the protocol
is still discoverable.
- New `Connectors` button next to Documents / Workflows in the chat input opens a
popover with a per-server toggle switch. Hides itself when the user has no
connectors configured.
- Tool calls in chat now render `Running <Server> · <tool>` (friendly) instead of
the raw `mcp__<slug>__<tool>` prefix; the original name still routes correctly.
- After each MCP tool call, a result block shows ✓/✗ + first line of output, with
a "Show details" toggle that expands pretty-printed JSON arguments and the full
text output.
- New connectors auto-discover their tool list immediately on save (no extra Test
click). Re-enabling a disabled connector also auto-tests.
- Settings card redesigned: status pill, header chips, expandable per-tool
descriptions with More/Less. Sanitises Name field if it looks like a Bearer
token was pasted into it (best-effort safety net).
- Amber "only add connectors you trust" notice at the top of the page and a
compact restated form inside the Add panel.
Backend
- New SSE event type `mcp_tool_result` with `{ server, tool, ok, args, output }`.
args/output capped at 4 KB each before persistence (the model still receives
the untruncated tool output — only the user-visible preview is capped).
- `tool_call_start` now optionally carries `display_name`; the renderer
prefers it.
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.
nforum
pushed a commit
to nforum/mike
that referenced
this pull request
May 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Connectors: users can plug their own Model Context Protocol servers into Mike via Settings → Connectors, with no code change. Tools discovered from each enabled connector are merged into the chat assistant's per-request tool list and dispatched back to the right server at call time.
Two auth modes are supported:
Authorization: Bearer ...). Works for self-hosted servers and any token-issuing service.https://legaldatahunter.com/mcp.What's in
Backend (Express/TS)
user_mcp_serverstable (RLS owner-only) with two migrations + the same DDL inlined into000_one_shot_schema.sql.lib/mcp/{client,servers,types,oauth}.ts— Streamable-HTTP client wrapper around the official@modelcontextprotocol/sdk, per-request loader, MCPinputSchema→ Mike'sOpenAIToolSchemaconverter (with 64-char tool-name guard),OAuthClientProviderimpl backed by the row's oauth_* columns.routes/mcpServers.ts(/user/mcp-servers) — CRUD +/test(connect + list_tools probe) +/oauth/start(returns authorize URL for popup).routes/mcpOauth.ts(/mcp/oauth/callback) — public callback that verifies an HMAC-signed state token, finishes the SDKauth()flow, andpostMessages the opener.lib/chatTools.ts— extendsrunLLMStreamandrunToolCallswith anmcp__dispatch branch; emitsmcp_tool_resultSSE events with capped args/output for in-chat observability.routes/{chat,projectChat}.ts— load enabled connectors at request start, close clients infinally.Frontend (Next.js)
/account/mcp: add / edit / delete / enable-disable connectors. Auth-mode radio (headers vs OAuth). For OAuth: Save & sign in opens a popup, the page polls until tokens land, then auto-runs tool discovery. For headers: tools are auto-discovered immediately on save.mcp_tool_resultblock in assistant messages:Called <Server> · <tool>with a one-line preview and an expand for full pretty-printed JSON args + raw output.mikeApi.tstyped client wrappers.Env
BACKEND_PUBLIC_URL(defaults tohttp://localhost:${PORT}) used to build the OAuth callback URL. Added to.env.example.What's not in (deliberate, follow-up PRs)
oauth_tokens. Currently they're stored at-rest in jsonb (RLS owner-only), which matches the existing precedent set byuser_profiles.{claude,gemini}_api_key. Worth a dedicated hardening PR.Security notes for reviewers
/user/mcp-servers/*handlers explicitly filter byuser_id = res.locals.userId. RLS on the table is belt-and-suspenders for any direct client access.localhost/127.0.0.1.GET /user/mcp-serversresponse returns header keys only and a booleanoauth_authorized— header values and access tokens never round-trip to the browser, even to the row's owner (defense in depth — RLS would allow it).DOWNLOAD_SIGNING_SECRET, 5 min TTL, carriesuser_id+server_idonly. CSRF-safe across the popup hop with no server-side session needed.token_endpoint_auth_method=none, PKCE-protected) — no client secret to store confidentially.mcp_tool_resultSSE events truncate args + output to 4 KB before persistence to keepchat_messages.contentfrom bloating; the model still sees the full untruncated tool output.Test plan
npm run build --prefix backendcleannpx eslintclean on changed frontend fileshttps://mcp.deepwiki.com/mcp) → tools auto-discovered → chat invokes a tool →mcp_tool_resultblock shows args/output → reload chat, persisted blocks render the same.last_errorpopulated, chat still works.Authorizationheader against a real authed MCP → end-to-end tool call succeeds.https://legaldatahunter.com/mcp→ popup opens at LDH → sign in → popup closes → row flips toOAuth · signed in→ tools auto-discover → chat invokes anmcp__legal-data-hunter__searchtool successfully.Note (not introduced by this PR)
npm run build --prefix frontendfails at prerender on/account/*pages becausefrontend/src/lib/supabase.tscallscreateClient("", "")at module load when env vars are absent. This affectsmainidentically; dev mode is unaffected. Happy to fix in a separate PR if helpful.🤖 Generated with Claude Code