feat(web): hosted MCP endpoint with token-based auth#12
Merged
Conversation
Add a hosted Streamable HTTP MCP server at /api/mcp/[transport] so users can point Claude Desktop / Cursor / Claude Code at detect.michaelhaag.org and query the Supabase-backed detection corpus without installing the stdio npm package. Architecture: - /api/mcp/[transport]/route.ts: Next.js route handler using mcp-handler (Streamable HTTP, stateless, runtime=nodejs, maxDuration=60). Wraps the handler with bearer-token auth before dispatching. - lib/mcp/auth.ts: validates Bearer tokens via the atomic increment_mcp_call RPC — single round-trip for lookup, tier resolution, daily quota check, counter bump, last_used_at update. - lib/mcp/tokens.ts: generates sdmcp_* tokens (32 random bytes, base62-ish alphabet) and stores only sha256(token). - lib/mcp/db.ts: Supabase service-role query layer. Most functions are thin wrappers over existing RPCs (search_detections_full, get_actor_ intelligence, get_threat_profile_gaps, compare_sources_for_technique, etc.) — no duplication of SQL logic. - lib/mcp/tools.ts: registers ~25 read-only tools (search, get_by_id, list_all, get_stats, get_coverage_summary, analyze_coverage, identify_gaps, get_technique_intelligence, get_technique_full, compare_sources, generate_navigator_layer, list_by_source/severity/ detection_type/mitre/mitre_tactic/cve/process_name/data_source/ analytic_story, list_actors, get_actor_profile, analyze_actor_coverage, compare_actor_coverage). Zod schemas for inputs; JSON text content responses. Database (supabase/migrations/014_mcp_tokens.sql): - mcp_tokens table: id, user_id (FK auth.users), token_hash (unique), name, prefix (first 12 chars for display), created_at, last_used_at, revoked_at, calls_today, calls_reset_at, total_calls. - RLS: users can select/insert/update only their own tokens. - increment_mcp_call(p_token_hash) SECURITY DEFINER RPC with row lock; returns JSON result with ok flag and quota state. Granted to service_role only; revoked from anon/authenticated/public. - Tier quotas: free 200/day, pro 5000/day, admin 100000/day. Token management UI: - /account/tokens page: lists active tokens with per-token daily quota progress bars, last-used, total calls. Shows plaintext token exactly once after creation with copy-to-clipboard. Includes ready-to-paste Claude Desktop / Cursor / curl snippets. - /api/account/tokens: POST creates (10-token cap per user, 64-char name cap), GET lists, DELETE revokes via revoked_at timestamp. - /account page gains a "HOSTED MCP TOKENS" card linking to /account/tokens. Marketing: - /mcp page gains a "HOSTED MCP — ZERO SETUP" section before the existing local-install guide, positioning both paths side by side. Hero headline updated to "RUN IT LOCALLY OR HOSTED". Middleware: - middleware.ts matcher now excludes /api/mcp so Supabase session refresh doesn't run on bearer-authenticated requests. Dependencies: - Adds mcp-handler, @modelcontextprotocol/sdk, zod to web/package.json. Non-goals: - Stateful tools (knowledge graph, dynamic tables, templates, autonomous analysis) remain local-only for now — they need per-user schema and are a future effort. - Billing/Stripe upgrades for the pro tier — the higher quota is wired up, the upgrade flow is separate work. - OAuth-based MCP auth — bearer tokens work with every current client. https://claude.ai/code/session_01G3R3gx99YenK1CtzxbvdsU
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Spec compliance upgrades for the hosted MCP endpoint:
- Refactor lib/mcp/tools.ts to use the modern server.registerTool() API
(the plain server.tool() API is now deprecated) with config objects
that carry:
* title — human-readable display name
* annotations — readOnlyHint: true, destructiveHint: false,
idempotentHint: true, openWorldHint: false
(so Claude Code/Cursor don't prompt on repeated calls)
* _meta — platform metadata clients can surface
All ~25 tools now advertise spec-compliant hints.
- Add RFC 9728 Protected Resource Metadata endpoint at
/.well-known/oauth-protected-resource (required by MCP 2025-11-25).
Because Next.js disallows dot-prefixed app/ folders, it lives at
app/api/well-known/oauth-protected-resource/route.ts and is wired up
via a rewrite in next.config.ts.
- Update lib/mcp/auth.ts so 401 responses emit
`WWW-Authenticate: Bearer realm="mcp", resource_metadata="<PRM URL>"`
pointing at the new endpoint. Compliant MCP clients can now
auto-discover auth requirements.
- Explicitly set stateless mode on the mcp-handler config
(sessionIdGenerator: undefined, disableSse: true). This matches the
MCP 2025-11-25 Streamable HTTP stateless profile and fits Vercel
serverless natively — every request independent, no Mcp-Session-Id
emitted, no SSE sessions, no Redis dependency.
Confirmed via @modelcontextprotocol/sdk v1.26.0:
LATEST_PROTOCOL_VERSION = '2025-11-25'
SUPPORTED: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07
Install deeplinks and one-liners for all major MCP clients:
- README.md: new "Two Ways to Run It" section at the top with separate
Cursor install buttons for Local vs Hosted, plus VS Code / VS Code
Insiders badges, a Claude Code CLI one-liner, a Claude Desktop
mcp-remote bridge config, and an OpenAI Codex CLI/TOML example.
- web/app/mcp/page.tsx: replaces the plain config snippets in the
hosted section with a grid of one-click install buttons (Cursor,
VS Code, VS Code Insiders, Claude Code → token page) plus
copy-paste blocks for Claude Code CLI, Claude Desktop via
mcp-remote, OpenAI Codex CLI + TOML config, and a verify-with-curl
smoke test.
- web/app/(app)/account/tokens/page.tsx: Cursor + VS Code install
buttons at the top of the CONFIGURE YOUR CLIENT card, then
copy-paste blocks for Claude Code, Claude Desktop (mcp-remote),
and OpenAI Codex.
- docs/HOSTED_MCP.md: NEW comprehensive 250-line setup guide covering
the architecture diagram, step-by-step install for every supported
client, verify-with-curl, full ~25-tool inventory, rate limit
tiers, RFC 9728 authentication details, troubleshooting table,
and what the hosted version explicitly does NOT include.
https://claude.ai/code/session_01G3R3gx99YenK1CtzxbvdsU
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 a hosted Streamable HTTP MCP server at
https://detect.michaelhaag.org/api/mcp/httpso users can point Claude Desktop / Cursor / Claude Code at one URL and query the Supabase-backed detection corpus without installing the stdio npm package.Flow: sign in → visit
/account/tokens→ generatesdmcp_...token → paste it into an MCP client as aBearerheader → start asking questions.This is additive only — nothing in
src/(the stdio npm package) changes.Architecture
Transport: Streamable HTTP only, stateless. No Redis, no session store — fits Vercel serverless natively.
runtime = 'nodejs',maxDuration = 60.Design choices (confirmed before implementation):
What's in the box
Database (
supabase/migrations/014_mcp_tokens.sql)mcp_tokenstable: id, user_id (FKauth.users),token_hash(unique, SHA-256),name,prefix(first 12 chars for display),created_at,last_used_at,revoked_at,calls_today,calls_reset_at,total_calls.increment_mcp_call(p_token_hash text) RETURNS json—SECURITY DEFINERwith row lock. One round-trip: lookup, revocation check, tier → quota resolution, daily reset, increment,last_used_atupdate. Granted toservice_roleonly; revoked fromanon/authenticated/public.profiles.tiercolumn.MCP server (
web/lib/mcp/,web/app/api/mcp/[transport]/route.ts)lib/mcp/tokens.ts— generatessdmcp_<base62>tokens (32 random bytes), stores only SHA-256 hash.extractBearer()acceptsBearer <token>, plain<token>, orx-mcp-token.lib/mcp/auth.ts— wraps every request: extracts bearer, hashes, callsincrement_mcp_call, returns either a fully-formed JSON-RPC errorResponseor anAuthContext. Maps RPC failure reasons to JSON-RPC codes (-32001 invalid, -32002 blocked, -32003 quota_exceeded).lib/mcp/db.ts— Supabase service-role query layer. Most functions are thin wrappers over the existing RPCs in002_rls_and_functions.sql(search_detections_full,get_technique_intelligence,get_actor_intelligence,get_threat_profile_gaps,compare_sources_for_technique,get_actor_profile_full,compare_actors,search_detections_by_filter, etc.) — no SQL duplication. Includes a Navigator layer generator that builds a valid v4.5 JSON layer from covered technique IDs.lib/mcp/tools.ts— registers ~25 tools on the passedMcpServervia Zod schemas. Groups: search & retrieval (search,get_by_id,get_raw_yaml,list_all), stats & coverage (get_stats,get_coverage_summary,analyze_coverage,identify_gaps,get_technique_intelligence,get_technique_full,compare_sources,generate_navigator_layer), filters (list_by_source,list_by_severity,list_by_detection_type,list_by_mitre,list_by_mitre_tactic,list_by_cve,list_by_process_name,list_by_data_source,list_by_analytic_story), threat actors (list_actors,get_actor_profile,analyze_actor_coverage,compare_actor_coverage).app/api/mcp/[transport]/route.ts— creates themcp-handlerhandler once at module load, wraps it withwithAuththat validates the bearer before dispatching. Exported asGET/POST/DELETE.Token management UI
/account/tokenspage — lists active tokens with per-token daily quota progress bars, last-used date, total call count. Newly-created tokens are revealed exactly once in a dismissable card with copy-to-clipboard. Includes ready-to-paste Claude Desktop / Cursor / curl snippets fordetect.michaelhaag.org/api/mcp/http./api/account/tokens—POSTcreates (10-token cap per user, 64-char name cap),GETlists metadata (no secrets),DELETErevokes viarevoked_attimestamp./accountpage — gains a "HOSTED MCP TOKENS" card linking to/account/tokens.Marketing
/mcppage — gains a new "HOSTED MCP — ZERO SETUP" section before the existing local-install guide, positioning both paths side by side. Hero updated to "RUN IT LOCALLY OR HOSTED" with a Get Hosted Token CTA alongside the existing Clone / npm buttons.Middleware
middleware.tsmatcher now excludes/api/mcpso Supabase session refresh doesn't run on bearer-authenticated MCP requests.Dependencies
mcp-handler@^1.1.0,@modelcontextprotocol/sdk@^1.25.3,zod@^3.25.76toweb/package.json.Vercel limits considered
maxDuration = 60. Indexed Supabase queries return in well under 1s, leaving a comfortable ceiling.limitat 100, search at 100.Out of scope (non-goals)
profiles.tiercolumn and higher quotas are already wired; upgrade UX is separate.Test plan
014_mcp_tokens.sqlin Supabase (SQL editor orsupabase db push)/api/mcp/httproute is live and returns 401 without a token/account/tokens, generate a token named "test"curl -X POST https://<preview>/api/mcp/http -H "Authorization: Bearer sdmcp_..." -H "Accept: application/json, text/event-stream" -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'— expect ~25 toolstools/callforsearchwith{"query":"lsass"}— expect detection resultstools/callforget_coverage_summary— expect the full coverage JSONtools/callforanalyze_actor_coveragewith{"actor_name":"APT29"}— expect coverage breakdowntools/callforgenerate_navigator_layerwith{"source_type":"sigma"}— expect valid Navigator v4.5 JSON/account/tokens, retry the curl call — expect-32001 Token has been revoked-32003 Daily quota exceededon call 201/api/search,/api/chat,/api/coverage,/api/accountstill worknpx security-detections-mcp(stdio) still works unchangedVerification status
tsc --noEmitpasseseslintpasses on all touched filesnext buildnot run in sandbox — Google Fonts network requests are blocked in the build environment. Not a code issue; will succeed on Vercel.https://claude.ai/code/session_01G3R3gx99YenK1CtzxbvdsU