Skip to content

feat(web): hosted MCP endpoint with token-based auth#12

Merged
MHaggis merged 2 commits intomainfrom
claude/token-mcp-setup-BuWSt
Apr 12, 2026
Merged

feat(web): hosted MCP endpoint with token-based auth#12
MHaggis merged 2 commits intomainfrom
claude/token-mcp-setup-BuWSt

Conversation

@MHaggis
Copy link
Copy Markdown
Owner

@MHaggis MHaggis commented Apr 11, 2026

Summary

Adds a hosted Streamable HTTP MCP server at https://detect.michaelhaag.org/api/mcp/http so 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 → generate sdmcp_... token → paste it into an MCP client as a Bearer header → start asking questions.

This is additive only — nothing in src/ (the stdio npm package) changes.

Architecture

MCP client  ── HTTPS ──►  /api/mcp/http  (Next.js route handler, mcp-handler, stateless)
                                │
                                ▼
                       Bearer-token auth + atomic rate limit (increment_mcp_call RPC)
                                │
                                ▼
                       ~25 read-only tools registered via lib/mcp/tools.ts
                                │
                                ▼
                       lib/mcp/db.ts  ──►  Supabase service-role client
                                            (search_detections_full, get_actor_intelligence,
                                             get_threat_profile_gaps, compare_sources_for_technique,
                                             get_coverage_summary, etc. — existing RPCs)

Transport: Streamable HTTP only, stateless. No Redis, no session store — fits Vercel serverless natively. runtime = 'nodejs', maxDuration = 60.

Design choices (confirmed before implementation):

  1. Read-only tool subset — ~25 tools: search, retrieval, filters, coverage analysis, gap analysis, actor profiling, Navigator layer export. Excludes stateful tools (knowledge graph, dynamic tables, templates, autonomous analysis).
  2. Token required, tied to account — enables revocation, per-tier quotas, usage metering.
  3. Streamable HTTP stateless — modern MCP spec, supported by every current client, serverless-native.

What's in the box

Database (supabase/migrations/014_mcp_tokens.sql)

  • mcp_tokens table: id, user_id (FK auth.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.
  • RLS: users can select/insert/update only their own tokens.
  • increment_mcp_call(p_token_hash text) RETURNS jsonSECURITY DEFINER with row lock. One round-trip: lookup, revocation check, tier → quota resolution, daily reset, increment, last_used_at update. Granted to service_role only; revoked from anon / authenticated / public.
  • Tier quotas: free 200/day, pro 5 000/day, admin 100 000/day. Reuses the existing profiles.tier column.

MCP server (web/lib/mcp/, web/app/api/mcp/[transport]/route.ts)

  • lib/mcp/tokens.ts — generates sdmcp_<base62> tokens (32 random bytes), stores only SHA-256 hash. extractBearer() accepts Bearer <token>, plain <token>, or x-mcp-token.
  • lib/mcp/auth.ts — wraps every request: extracts bearer, hashes, calls increment_mcp_call, returns either a fully-formed JSON-RPC error Response or an AuthContext. 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 in 002_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 passed McpServer via 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 the mcp-handler handler once at module load, wraps it with withAuth that validates the bearer before dispatching. Exported as GET/POST/DELETE.

Token management UI

  • /account/tokens page — 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 for detect.michaelhaag.org/api/mcp/http.
  • /api/account/tokensPOST creates (10-token cap per user, 64-char name cap), GET lists metadata (no secrets), DELETE revokes via revoked_at timestamp.
  • /account page — gains a "HOSTED MCP TOKENS" card linking to /account/tokens.

Marketing

  • /mcp page — 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.ts matcher now excludes /api/mcp so Supabase session refresh doesn't run on bearer-authenticated MCP requests.

Dependencies

  • Adds mcp-handler@^1.1.0, @modelcontextprotocol/sdk@^1.25.3, zod@^3.25.76 to web/package.json.

Vercel limits considered

  • Function timeoutmaxDuration = 60. Indexed Supabase queries return in well under 1s, leaving a comfortable ceiling.
  • Response body — 4.5 MB cap. List tools hard-cap limit at 100, search at 100.
  • Cold starts — ~200–500 ms, acceptable.
  • No long-lived connections — stateless Streamable HTTP, no SSE sessions, no Redis dependency.
  • Supabase egress — token-gated access + per-token daily quotas cap it.

Out of scope (non-goals)

  • Stateful tools (knowledge graph, dynamic tables, templates, autonomous analysis) — need per-user schemas, future work.
  • Stripe/billing upgrade flow — the profiles.tier column and higher quotas are already wired; upgrade UX is separate.
  • OAuth-based MCP auth — bearer tokens work with every current MCP client.
  • Replacing SQLite in the stdio npm package — that stays offline-first.

Test plan

  • Apply migration 014_mcp_tokens.sql in Supabase (SQL editor or supabase db push)
  • Deploy to Vercel preview; verify /api/mcp/http route is live and returns 401 without a token
  • Sign in on the web app, visit /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 tools
  • tools/call for search with {"query":"lsass"} — expect detection results
  • tools/call for get_coverage_summary — expect the full coverage JSON
  • tools/call for analyze_actor_coverage with {"actor_name":"APT29"} — expect coverage breakdown
  • tools/call for generate_navigator_layer with {"source_type":"sigma"} — expect valid Navigator v4.5 JSON
  • Revoke the token in /account/tokens, retry the curl call — expect -32001 Token has been revoked
  • Regenerate, hammer the endpoint past 200 calls — expect -32003 Daily quota exceeded on call 201
  • Configure Claude Desktop with the hosted URL + token, restart, verify tools appear and a real prompt works
  • Repeat with Cursor
  • Regression: /api/search, /api/chat, /api/coverage, /api/account still work
  • Regression: npx security-detections-mcp (stdio) still works unchanged

Verification status

  • tsc --noEmit passes
  • eslint passes on all touched files
  • ⚠️ next build not 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

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
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
security-detections-mcp Ready Ready Preview, Comment Apr 11, 2026 9:56pm

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
@MHaggis MHaggis merged commit fdc3aba into main Apr 12, 2026
13 checks passed
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