feat: add read-only MCP server for AI-spend data#112
Conversation
Expose the Hub's read-only data via a Model Context Protocol server so MCP clients (Claude Desktop/Code, Cursor, etc.) can query AI spend directly. - Mount mcp-handler at /api/mcp/mcp (Streamable HTTP) with withMcpAuth - Shared-secret auth (MCP_SERVER_SECRET), dormant-by-default when unset - 7 read-only tools delegating to the existing tested read layer: list_ai_tools, get_user_cost_profile, get_claude_spend_summary, list_claude_workspaces, get_budget_status, get_copilot_usage_summary, list_recent_sync_events - Exclude /api/mcp from NextAuth middleware; add to agent deny-list - Centralize centsToUsd in utils.ts (reused by formatCurrency) - Unit tests for format, auth, data shaping, and tool registration - Docs: docs/mcp-server.md and docs/mcp-implementation-plan.html https://claude.ai/code/session_01JbYxs25bCVAFyzayB3barx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
There was a problem hiding this comment.
Pull request overview
This PR adds a read-only Model Context Protocol (MCP) server endpoint to the AI Developer Hub so MCP clients can query AI spend/budget/sync health data via a shared-secret bearer token, reusing existing read-layer logic and keeping the feature dormant unless MCP_SERVER_SECRET is configured.
Changes:
- Introduces an MCP server route (
/api/mcp/mcp) with shared-secret auth and a set of 7 read-only tools. - Adds MCP data/format/tool registration helpers under
src/lib/mcp/*, plus unit tests covering auth, formatting, data assembly, and tool wiring. - Wires configuration/security hardening (middleware exclusion for
/api/mcp, agent deny-list entry, optional env var, and centralizedcentsToUsd).
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/mcp/tools.test.ts | Unit tests validating tool registration, arg forwarding, and error degradation behavior. |
| tests/unit/mcp/format.test.ts | Unit tests for cents→USD conversion and MCP result envelopes. |
| tests/unit/mcp/data.test.ts | Unit tests for each MCP tool’s data assembly with mocked DB/loaders. |
| tests/unit/mcp/auth.test.ts | Unit tests for shared-secret validation and constant-time comparison behavior. |
| src/middleware.ts | Excludes /api/mcp from NextAuth middleware matching to avoid login redirects for MCP clients. |
| src/lib/utils.ts | Adds centsToUsd helper and reuses it in formatCurrency. |
| src/lib/mcp/tools.ts | Registers the 7 MCP tools with Zod input schemas and safeJsonResult wrapping. |
| src/lib/mcp/format.ts | Adds pure MCP formatting/result helpers and re-exports centsToUsd. |
| src/lib/mcp/data.ts | Implements read-only tool backing functions by delegating to existing read layers and DB reads. |
| src/lib/mcp/auth.ts | Implements shared-secret bearer auth with constant-time comparison and “dormant by default” behavior. |
| src/lib/env.ts | Adds optional MCP_SERVER_SECRET with minimum length validation and reformats schema declarations. |
| src/lib/agent-auth.ts | Adds /api/mcp to the built-in deny list for the agent as defense-in-depth. |
| src/app/api/mcp/[transport]/route.ts | Adds the MCP endpoint route using mcp-handler + withMcpAuth. |
| package.json | Adds pinned MCP dependencies (@modelcontextprotocol/sdk@1.26.0, mcp-handler@1.1.0). |
| pnpm-lock.yaml | Lockfile updates to include MCP dependencies and their transitive packages. |
| docs/mcp-server.md | Operator/client setup docs for the MCP server and tool list. |
| docs/mcp-implementation-plan.html | Detailed implementation plan and rationale document committed to the repo. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Require copilotSyncEnabled on the GitHub connection, matching getActiveConnection() in copilot-data.ts, so a connection without Copilot sync isn't reported as connected with stale/absent data - Default the since/until range using UTC (formatUtcDateOnly) instead of date-fns local-timezone format(), keeping the YYYY-MM-DD day boundary timezone-stable like the rest of the codebase https://claude.ai/code/session_01JbYxs25bCVAFyzayB3barx
Summary
Adds a read-only Model Context Protocol (MCP) server so MCP clients (Claude Desktop/Code, Cursor, etc.) can query the Hub's AI-spend data in natural language. It reuses the existing, already-tested read layer rather than duplicating logic, and is dormant by default (off until
MCP_SERVER_SECRETis set).POST /api/mcp/mcp(Streamable HTTP viamcp-handler+ official MCP SDK)MCP_SERVER_SECRET), constant-time compare, viawithMcpAuthTools (7)
list_ai_toolsaiTools+accessTiersget_user_cost_profilefetchProfileDataInternalget_claude_spend_summaryloadDashboardKpislist_claude_workspacesloadWorkspaceListget_budget_statusgetActiveBudget+getBudgetWithCosts+buildBudgetForecastget_copilot_usage_summarycopilotUsageMetrics+copilotBillingSnapshotslist_recent_sync_eventssyncEvents+loadSyncStatusAll monetary fields are returned as both integer cents (
*Cents) and derived USD (*Usd).Architecture
src/app/api/mcp/[transport]/route.ts— thin route (createMcpHandler+withMcpAuth)src/lib/mcp/{auth,tools,data,format}.ts— auth, tool registration, data assembly, pure helpers/api/mcpfrom the NextAuth middleware matcher (clean401instead of a/loginredirect) and added it to the nighthawk agent deny-list as defense-in-depth, mirroring/api/syncMCP_SERVER_SECRETadded toenv.ts(optional,.min(16)), matchingPROFILE_API_SECRETcentsToUsdinutils.ts(now reused byformatCurrency)Design notes / decisions
@modelcontextprotocol/sdk@1.26.0to satisfymcp-handler@1.1.0's exact peer; SDK 1.26 supports the app's Zod v4 via Standard Schema. Tool inputs are kept to flat strings/numbers/optionals to avoid known Zod-v4 quirks (discriminated unions, description propagation).src/lib/mcp/*and is unit-tested independently ofmcp-handler, so swapping transports later is a route-file change, not a rewrite.docs/mcp-implementation-plan.html; operator/client setup is indocs/mcp-server.md.Testing / QA
pnpm typecheck✅ ·pnpm lint(zero warnings) ✅ ·pnpm test✅ 431 passed (incl. new format/auth/data/tools suites)pnpm build: compiles and type-checks ✅. Static prerender of the unrelated/reports/budgetpage fails only because this sandbox has noDATABASE_URL(that page does an unguarded DB call at prerender time); not caused by this change. The MCP route isforce-dynamicand never prerendered.Configuration
{ "mcpServers": { "ai-developer-hub": { "type": "http", "url": "https://<your-hub-host>/api/mcp/mcp", "headers": { "Authorization": "Bearer <MCP_SERVER_SECRET>" } } } }https://claude.ai/code/session_01JbYxs25bCVAFyzayB3barx
Generated by Claude Code