ADR-0009 — Use MCP SDK v2 scoped packages via the modern-era factory path¶
- Status: Accepted — MANDATORY (protocol correctness)
- Date: 2026-08-17
- Supersedes: the open question OQ-01, which is dissolved rather than resolved
- Relates to: ADR-0004 · TIP §2, §24, §25, §26, §27, §92
- Evidence:
docs/research/sdk-v2-api-notes.md— verified 2026-08-17
Context¶
The architecture targets MCP revision 2026-07-28. An earlier pass inspected @modelcontextprotocol/sdk@1.30.0, found LATEST_PROTOCOL_VERSION = '2025-11-25', and concluded the official SDK could not speak the target revision — filed as a P0 blocker.
That conclusion was wrong, and for an instructive reason. There are two distributions:
| Package | Latest | Status |
|---|---|---|
@modelcontextprotocol/sdk | 1.30.0 | Legacy single package, capped at 2025-11-25 |
@modelcontextprotocol/{core,server,client} | 2.0.0, 2026-07-27 | Current split packages, implement 2026-07-28 |
Empirical probing of the v2 packages then surfaced a second, sharper fact. LATEST_PROTOCOL_VERSION is still "2025-11-25" in v2 — because it names the latest legacy-era version, not the SDK's capability ceiling. The SDK models two eras (type ProtocolEra = 'legacy' | 'modern') and carries MODERN_WIRE_REVISION = "2026-07-28" internally, unexported.
Which era you get depends on how you start the server, and nothing in the type signature warns you:
// LEGACY: answers initialize with 2025-11-25; server/discover → -32601
await new McpServer(info).connect(new StdioServerTransport());
// MODERN: server/discover → supportedVersions: ["2026-07-28"]
serveStdio(() => { const s = new McpServer(info); s.registerTool(...); return s; });
Both were confirmed on the raw JSON-RPC wire. This is a silent-downgrade hazard: the legacy path works, passes tests, and serves the wrong protocol revision.
Decision¶
- Use the v2 scoped packages —
@modelcontextprotocol/core,/server,/client. The legacy@modelcontextprotocol/sdkpackage is not a dependency of this project. - Start servers through the modern factory path only.
serveStdio(factory)for stdio;PerRequestHTTPServerTransport/createMcpHandlerfor HTTP.McpServer#connect()is legacy and must not appear in shipped code. - The factory returns a fresh
McpServerper invocation, matching the modern era's statelessness. No server instance is held across requests. - Register tool schemas as raw JSON Schema via
fromJsonSchema, never as authored Zod. Our schemas are generated;schema-normalizeremits JSON Schema 2020-12 and the SDK publishes it verbatim. - Do not reimplement what the SDK owns. §8 of the research notes enumerates the boundary: input validation, Origin/Host validation, era classification, MRTR primitives,
_metakeys, subscriptions, Tasks, bearer auth, and protocol error types are the SDK's. Upstream HTTP execution, binding, retry, response limits, andx-mcp-headerconstraint validation are ours. - Track the latest SDK. Protocol revision adoption follows SDK releases; we do not fork or reimplement transports.
Consequences¶
Positive. The target revision is available today, so no in-house transport work is needed — TIP §63's conditional 10–18 dev-day row is void and the MVP band stays 100–150. FR-BIND-007, FR-HTTP-MCP-006 and FR-POL-005 are implementable now and remain MVP/MUST. Several planned responsibilities are removed rather than added: the SDK validates tool input and performs Origin validation, so mcp-runtime begins after arguments are already valid. Risk R12 closes.
Negative. We depend on an undocumented-by-signature distinction between two entry points; a future SDK refactor could move it, and nothing but a test would tell us. LATEST_PROTOCOL_VERSION remains actively misleading, so any newcomer reading it will draw the wrong conclusion — which is why the enforcement below is a wire assertion rather than a constant comparison. Tracking the latest SDK means adopting its cadence, including major bumps on someone else's schedule.
Enforcement¶
- Era assertion test (mandatory). An E2E test drives
server/discoveragainst the spawned server and assertssupportedVersionscontains2026-07-28. This is the only reliable era check — comparing againstLATEST_PROTOCOL_VERSIONwould fail while the server is correct. - Lint ban.
McpServer#connect(is a restricted-syntax error outside test fixtures that deliberately exercise the legacy path. - Boundary.
boundariesscript: onlymcp-protocolmay import@modelcontextprotocol/*(ADR-0004).@modelcontextprotocol/clientis a devDependency of the E2E suite only, never a runtime dependency of a generated server. - Dependency pin. Exact versions in
package.json; SDK updates are planned work items, and each one re-runs the reproduction steps in the research notes. - Generated README states the actual negotiated revision, taken from the wire, not from a constant.