Move MCP server to SDK v2 (legacy sessions only) - #174
Merged
Merged
Conversation
…to v1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDxYQLKhDxwoYe8GV2noDY
…only) Swap every @modelcontextprotocol/sdk (v1) import in src/ and scripts/ to the v2 packages: McpServer and isInitializeRequest from @modelcontextprotocol/server, NodeStreamableHTTPServerTransport from @modelcontextprotocol/node, and SSEServerTransport from @modelcontextprotocol/server-legacy/sse (not the package root, which augments Express req.auth). Tools now use registerTool with zod-v4 input schemas wrapped in z.object. Tests use Client and InMemoryTransport from @modelcontextprotocol/client, and the fake servers in collect.test.ts and knowledge-tool.test.ts now match the zod-v4 and registerTool shapes. Comments in server.ts, sse-handlers.ts and the tests describe only what the code does and which SDK behaviour it relies on. Expected wire differences (the only allowed changes): - E1: tools/list inputSchema uses the v2 JSON Schema 2020-12 shape, without the default additionalProperties: false or the execution field. - E2: a call to an unknown tool returns a JSON-RPC -32602 error instead of a CallToolResult with isError: true. - E3: serverInfo and SDK version strings. serverInfo name and version come from config, so this may be empty. Unknown-session 404 (-32001) and the modern-request 400 are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDxYQLKhDxwoYe8GV2noDY
Drop the @modelcontextprotocol/sdk dependency now that all server and client code runs on the v2 packages. No transitive consumer remains in the tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDxYQLKhDxwoYe8GV2noDY
Add a contract test that builds the real server through createMcpServer with a search, knowledge, bash and collect tool, calls tools/list over the v2 Client and InMemoryTransport, and compares each inputSchema to an exact expected object: properties, types, descriptions, bounds, defaults, enums, required, $schema, and the absence of additionalProperties. The expected values come from the SDK v2 tools/list capture. Apart from $schema (draft-07 to 2020-12) and additionalProperties (no longer sent), every field matches the SDK v1 capture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDxYQLKhDxwoYe8GV2noDY
Add a contract test that sends invalid arguments through a real tools/call for all 4 tools (search, knowledge, bash, collect). The cases come from a fixed copy of the pinned schemas: missing required fields, wrong types, values below the minimum and above the maximum, and a value not in the enum. Each case checks that the result is an isError "Input validation error" naming the field, and that no embedding, DB, or bash exec double was called. A valid call per tool proves that the doubles are reached. Also make the collect "rejects invalid input" test check the error text, the field it names, and that insertCollectedData was not called. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDxYQLKhDxwoYe8GV2noDY
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.
What
This moves the Pathfinder MCP server from
@modelcontextprotocol/sdk1.28 to the split v2 packages:@modelcontextprotocol/server2.2.0,@modelcontextprotocol/node2.1.0 and@modelcontextprotocol/server-legacy/sse2.2.0. Tool schemas move to zod 4 through thezod-v4alias and are registered withregisterToolandz.object. All traffic stays on the legacy sessionful protocol. The P1 unknown-session 404 and the P2 analytics behaviour are unchanged. This is phase P3 of the stateless-MCP migration plan.Changes
1356c19Add MCP SDK v2 server, node, server-legacy and the zod v4 alias next to v17bae503Move the MCP server, transports and tools to SDK v2 (legacy sessions only)0b7a71aRemove MCP SDK v1980932cPin the inputSchema that tools/list advertisesa6f5038Pin that tools/call rejects arguments outside the advertised inputSchemaExpected wire differences
tools/listschemas:inputSchema.$schemachanges from draft-07 tohttps://json-schema.org/draft/2020-12/schema, andadditionalProperties: falseis no longer advertised. Unknown keys are still stripped at runtime exactly as on v1.tools/list:execution.taskSupport: "forbidden"is no longer sent. The 2025-11-25 spec schema says absenttaskSupportdefaults to"forbidden", so clients see the same value.tools/callfor a tool that does not exist now returns a JSON-RPC error-32602"Tool no-such-tool not found" instead of anisError: trueresult. HTTP status stays 200.argumentsis treated as{}.X-Accel-Buffering: noheader.{"name":"pathfinder-docs","version":"1.4.0"}in both).Proof (red → green on the real surface)
RED = base
443892e(SDK v1 1.28.0). GREEN = this branch. Both runs used the same captures against the same local Postgres:/mcpand against/sseMechanical wire diff of RED against GREEN: VERDICT PASS. 34 hunks, all expected, 0 unexpected.
Unchanged on the wire: the P1 404 with
-32001"Session not found" for a fake session id on POST, GET and DELETE (POST echoes the id), a fresh session oninitializewith a stale header, the/sseunknown-session 404, the 400 for the modernserver/discoverprobe, the 405 for GET /mcp without a session, and the[mcp] 404 unknown-session-idlog line.Analytics continuity: the scoped
query_logrows are identical in RED and GREEN across 8 sources and 28 rows. Every row has the handshake fields populated (transport, protocol eralegacy, protocol version2025-11-25, client name), with 0 rows missing handshake data.auth_client_idis populated on all 4 bearer-client rows. The/analyticssummary endpoint matches SQL for every P2 count.Tests
tools-list-input-schema.contract.test.tspins the advertisedinputSchemafor all 4 tools. Mutation proved it is not vacuous.tools-call-input-validation.contract.test.tscovers 31 invalid-argument cases plus positive controls. Mutation proved it is not vacuous.npm ci.Review
Tier-3 code review, 5 full rounds. The SDK swap introduced no behaviour defect. Every behaviour finding was checked against v1 with side-by-side probes and was identical there. The remaining pre-existing issues are tracked as follow-ups below and are not part of this PR.
Follow-ups (not in this PR)
ensureSession/503 comments and deadif (!accepted)inserver.ts, the impossible-race comments insse-handlers.ts, and the 429 mislabel on a session-id collision.initialize(406, 415 or 400) keeps its session and IP slot until the reaper runs, and logs a false "New session".onerroris never wired, so SDK errors are dropped.limithas no.int()in the search and knowledge tools.sessionStateManageris not passed tocreateSseHandlers, so SSE session state is never cleaned up.scripts/integration-test.tscounts anisErrorresult as a pass.types.tsand the remaining schemas to zod 4.🤖 Generated with Claude Code
https://claude.ai/code/session_01EDxYQLKhDxwoYe8GV2noDY