feat: add Streamable HTTP MCP server#3
Merged
Conversation
Adds a read-only Model Context Protocol server so AI clients can discover and query engineering standards from the same PostgreSQL database used by the Fastify HTTP API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- toolError now serializes AppError code and details into JSON so MCP clients get structured error metadata, not just the message string - get_standard validates rule_key against the same regex as the HTTP API to fail fast on malformed keys before touching the database - list_standards uses z.coerce.number() for limit/offset so LLM-generated string values round-trip correctly - Remove redundant per-type catch branches in get_standard and applicable_standards — both paths returned toolError(err) identically - Add test/mcp-tools.test.ts covering all four tools, changed_paths array mapping, empty-result cases, and AppError serialization Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- toolError now includes statusCode alongside code and message so MCP clients can distinguish error types (400/404/409) without mapping codes - changed_paths uses .min(1) to reject empty arrays, aligning with the HTTP API's "omit instead of empty" contract - SIGINT/SIGTERM handlers now catch shutdown errors and exit non-zero instead of silently swallowing Prisma disconnect failures - Refactor tool handlers into createHandlers(service) so tests call them directly without reaching into SDK private fields; McpServer and _registeredTools are no longer referenced in tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Non-AppError exceptions now return a structured JSON internal_error
payload and log to stderr, preventing internal detail leakage
- listStandards defaults status to "active" in the handler itself so
the service receives an explicit filter and applies latest-by-ruleKey
deduplication correctly when called with no arguments
- applicableStandards defaults its argument to {} so invoking the
handler with no args does not throw on destructuring
- Test updated to call listStandards({}) and assert all returned rules
are active, actually exercising the default-status contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds src/mcp/http-server.ts — a second MCP entrypoint using StreamableHTTPServerTransport that exposes the same four tools and two resources as the existing stdio server, without touching either the stdio server or the Fastify HTTP API. Key details: - Uses Node's built-in node:http (no new framework dependency) - Mounts POST/GET/DELETE /mcp per the Streamable HTTP spec - Hard auth guard via x-api-key header (MCP_API_KEY env var required; unset → all requests rejected 401) - Per-session McpServer + transport stored in a Map, cleaned up on close - isMain guard prevents server startup when the file is imported in tests - New scripts: mcp:http:dev (tsx) and mcp:http:start (compiled output) - README documents env vars, run commands, and client config block - 6 unit tests for the isAuthorized helper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds Model Context Protocol (MCP) support to the standards API by introducing MCP tool/resource registrations plus two runnable MCP server entrypoints: a stdio server and a Streamable HTTP server secured via an API key.
Changes:
- Adds MCP tool handlers and registers four tools (
list_standards,get_standard,latest_standards,applicable_standards) plus two resources (standards://latest,standards://rule/{rule_key}). - Introduces two MCP entrypoints: stdio (
src/mcp/server.ts) and Streamable HTTP (src/mcp/http-server.ts) with session management + API key auth. - Adds unit tests for MCP handlers and HTTP auth helper; updates README and package scripts; adds
@modelcontextprotocol/sdkdependency.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/mcp-tools.test.ts | Adds unit tests for MCP tool handler behavior and error serialization. |
| test/mcp-http-server.test.ts | Adds unit tests for the isAuthorized helper. |
| src/mcp/tools.ts | Implements MCP tool handler creation + tool registration with Zod schemas. |
| src/mcp/server.ts | Adds stdio MCP server entrypoint wiring Prisma + tools/resources. |
| src/mcp/resources.ts | Registers MCP resources for latest standards and rule lookup. |
| src/mcp/http-server.ts | Adds Streamable HTTP MCP server with API key auth + session transport management. |
| README.md | Documents MCP usage, scripts, environment variables, and client configuration. |
| package.json | Adds MCP scripts and the @modelcontextprotocol/sdk dependency. |
| package-lock.json | Locks new dependency tree for MCP SDK and transitive packages. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Enforce 1 MiB body size cap in readBody; stream is destroyed on overflow (BodyTooLargeError) to avoid unbounded memory use - Return 413 for oversized bodies and 400 for invalid JSON so clients get actionable errors instead of a 500 - Close McpServer in transport.onclose to prevent listener/resource leaks when sessions disconnect - Call res.end() in serverError even when headers are already sent so SSE streams don't hang after a mid-stream error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- package.json / README.md: keep mcp:http:dev and mcp:http:start scripts and Streamable HTTP section added in this branch - src/mcp/tools.ts: take main's version which includes statusCode: 500 in the internal_error fallback (added in PR #2) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use JSON-RPC code -32600 (Invalid Request) in rejectBadRequest instead of -32000 (server error range) — helps clients classify client errors correctly - Distinguish BadJsonError from I/O stream errors in handlePost body catch; unexpected stream errors now return 500 rather than 400 - Normalize process.argv[1] with path.resolve() before comparing to import.meta.url so the isMain guard works when npm passes a relative path (e.g. tsx src/mcp/http-server.ts from project root) - Extract session management into exported createMcpHttpHandler factory so tests can inject a memory-backed service and inspect the sessions map without a real database - Add 6 integration tests covering: 400 for non-init POST, GET, and DELETE without session IDs; session created and stored on initialize; subsequent POST routed to transport; 400 for unknown session ID on GET Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reject empty POST bodies with 400 before session/transport routing to avoid forwarding undefined to transport.handleRequest, which would throw internally and produce a misleading 500 - Fix shutdown hang with open SSE sessions: close all McpServers in the session map, clear the map, then call httpServer.closeAllConnections() before awaiting httpServer.close() so the process exits promptly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Return JSON-RPC shaped error bodies for oversized (-32600) and
unparseable (-32700 Parse error) POST bodies so MCP/JSON-RPC clients
can classify these failures correctly rather than receiving plain
{ error: '...' } objects
- Parse req.url with URL() and compare .pathname instead of the raw
string so requests with query strings (e.g. /mcp?foo=bar) are routed
correctly instead of getting a spurious 404
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
src/mcp/http-server.ts— a second MCP entrypoint usingStreamableHTTPServerTransportfrom@modelcontextprotocol/sdkPOST /mcp,GET /mcp, andDELETE /mcpusing Node's built-innode:http(no new framework dependency)x-api-keyheader matchingMCP_API_KEY; if the env var is unset, every request is rejected with401McpServer+StreamableHTTPServerTransportstored in aMap<string, Session>, keyed by the SDK-generated session ID, cleaned up viatransport.oncloseregisterToolsandregisterResources— zero duplication of tool/resource logicisMainguard (process.argv[1] === fileURLToPath(import.meta.url)) prevents the server andPrismaClientfrom starting when the module is imported by testsmcp:http:dev(tsx, no build) andmcp:http:start(compiled output atdist/src/mcp/http-server.js)isAuthorizedhelper covering: unset key, empty key, missing header, wrong header, exact match, and array header valueWhat's unchanged
The stdio MCP server (
src/mcp/server.ts) and the Fastify HTTP API (src/server.ts,src/app.ts,src/http/routes.ts) are untouched.Test plan
npm run lintpasses (TypeScript strict, no errors)npm run buildpasses and producesdist/src/mcp/http-server.jsnpm testpasses — 35/35 tests across 3 files, no regressionsMCP_API_KEY=secret npm run mcp:http:devstarts on port 3001curl -s http://localhost:3001/mcpreturns{"error":"Missing or invalid API key"}with HTTP 401🤖 Generated with Claude Code