You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
MCP protocol revision 2026-07-28 makes the protocol stateless: the initialize handshake is replaced by per-request versioning in params._meta, servers MUST implement server/discover, version mismatches return UnsupportedProtocolVersionError (-32022), every result carries a required resultType, and list results carry required ttlMs/cacheScope caching hints (SEP-2549).
smithy-java's mcp-server currently implements the handshake era only (2024-11-05 … 2025-11-25). Python SDK ≥ 2.0.0 clients (released 2026-07-28; the 1.x line is in maintenance mode since then) connect today only via their backward-compatibility fallback path — and that fallback currently triggers off an invalid response rather than clean negotiation. Clients pinned to the modern revision, or any strict client, fail outright.
We run a large internal MCP gateway built on this engine (it also powers our Coral-service MCP servers), have validated the gaps end-to-end against Python SDK 2.0.0, and have working, tested patches we'd like to contribute.
What breaks today
Validated against mcp==2.0.0 (mode=auto / mode=legacy / pinned mode="2026-07-28"):
Unknown JSON-RPC methods return no response at all — both dispatch switches in McpService end in default -> null, so a request with an id (including the SDK's server/discover probe) gets an empty reply. This violates JSON-RPC, and the V2 SDK's auto-mode fallback only survives it by treating the resulting client-side parse error as legacy evidence. The description of Add MCP protocol version 2025-11-25 #1304 documents this exact symptom for initialize.
initialize with an unknown protocolVersion answers the oldest version — handleInitialize leaves the field unset for unknown versions, so the Smithy model default (2024-11-05) leaks into the result. The spec requires answering with the latest supported version. (Add MCP protocol version 2025-11-25 #1304 fixed one instance by adding 2025-11-25 to the known list; the general fallback remained.)
No server/discover, no -32022 — modern-era requests carrying _meta["io.modelcontextprotocol/protocolVersion"] are served handshake-shaped results, which then fail the SDK's strict wire validation (ListToolsResult: cacheScope/resultType/ttlMs Field required) instead of getting a clean version error.
No modern-era result shaping — resultType, ttlMs/cacheScope (on tools/list / prompts/list), and the _meta["io.modelcontextprotocol/serverInfo"] stamp are absent.
Proposed contribution (PR ladder)
Return -32601 Method not found for unknown methods that carry an id (notifications stay silently dropped). Makes the V2 auto-mode fallback deterministic instead of accidental.
initialize with an unknown requested version answers the latest supported version, via a registry-derived ProtocolVersion.latestVersion() (single source, can't drift as versions are added).
server/discover + -32022: discover answers a wire-complete DiscoverResult with supportedVersions from the registry; requests whose _meta names an unsupported version get -32022 with {"supported": [...], "requested": "..."}. A handshake-only server advertising no modern version is the documented "explicit legacy advertisement" that the Python/TypeScript/Go clients all answer by falling back to initialize cleanly — so this PR is safe and useful before full modern support lands.
Dual-era serving (2026-07-28): per-request version resolution (params._meta wins when it names a known version; the transport-provided hint otherwise), era-conditional result stamping (resultType on all results; ttlMs/cacheScope on tools/list/prompts/list; _meta serverInfo) — era-conditional because peers strictly validate results per era in both directions, so handshake-era responses must stay byte-identical; initialize capped at the latest handshake version (modern versions negotiate per request, never via handshake — the stdio transport's captured connection version needs the same clamp); era-aware capabilities (modern advertisement drops listChanged: true, which we can't honor without subscriptions/listen); modern-era tool failures mapped to -32603 with a sanitized message.
Each step is fully covered by unit + integration tests, and we've verified the ladder end-to-end: after (4), a Python 2.0.0 client in auto mode adopts the modern era via discover (no initialize on the wire), pinned-modern clients pass strict wire validation through tools/list and tools/call, and legacy responses are byte-identical to before (verified by byte-diffing captures).
Open questions where we'd like maintainer input
(a) Cache-hint configuration: we default ttlMs: 0 / cacheScope: "private" (the Python reference server's defaults — safe for authenticated servers). Should there be a per-method configuration surface on McpServerBuilder, or are constants acceptable for a first pass?
(c) Error hygiene: tool failures currently return JSON-RPC code 500 (outside the server-error space) with a full stack trace in the message (createErrorResponse has a TODO about this). We map modern-era failures to -32603 with a sanitized message and left legacy behavior untouched in case consumers depend on it — appetite for fixing legacy too?
(d) Unknown-tool errors currently share that same internal-error path; arguably they should be -32602 (invalid params).
(e) ping on modern-era requests: removed from the 2026-07-28 surface; the Python reference server answers it -32601 there. We stayed lenient (still serve it). Preference?
(f) stdio era semantics: the reference implementation locks a stream connection's era on the first request and refuses cross-era claims; our implementation resolves per request (more lenient). Preference?
(g) McpServerProxy pins one downstream protocol version at first-initialize and stamps it on every proxied request — modern passthrough would need the per-request version threaded through rpc(). We left this untouched; flagging it as follow-up work.
Timeline context
Python SDK 1.x has been in maintenance mode (security/critical fixes only) since 2026-07-28; the TypeScript v1 line is guaranteed bug/security fixes for at least six months from v2's release. There's no announced date for V2 SDKs dropping the legacy-handshake compatibility path, but when that happens, servers on this engine become unreachable for those clients — hence the interest in landing negotiation correctness (PRs 1–3) soon and the modern era (PR 4) after design alignment here.
We're happy to split, reshape, or re-scope any of this to fit your preferences.
Summary
MCP protocol revision 2026-07-28 makes the protocol stateless: the
initializehandshake is replaced by per-request versioning inparams._meta, servers MUST implementserver/discover, version mismatches returnUnsupportedProtocolVersionError(-32022), every result carries a requiredresultType, and list results carry requiredttlMs/cacheScopecaching hints (SEP-2549).smithy-java's
mcp-servercurrently implements the handshake era only (2024-11-05 … 2025-11-25). Python SDK ≥ 2.0.0 clients (released 2026-07-28; the 1.x line is in maintenance mode since then) connect today only via their backward-compatibility fallback path — and that fallback currently triggers off an invalid response rather than clean negotiation. Clients pinned to the modern revision, or any strict client, fail outright.We run a large internal MCP gateway built on this engine (it also powers our Coral-service MCP servers), have validated the gaps end-to-end against Python SDK 2.0.0, and have working, tested patches we'd like to contribute.
What breaks today
Validated against
mcp==2.0.0(mode=auto/mode=legacy/ pinnedmode="2026-07-28"):McpServiceend indefault -> null, so a request with anid(including the SDK'sserver/discoverprobe) gets an empty reply. This violates JSON-RPC, and the V2 SDK's auto-mode fallback only survives it by treating the resulting client-side parse error as legacy evidence. The description of Add MCP protocol version 2025-11-25 #1304 documents this exact symptom forinitialize.initializewith an unknownprotocolVersionanswers the oldest version —handleInitializeleaves the field unset for unknown versions, so the Smithy model default (2024-11-05) leaks into the result. The spec requires answering with the latest supported version. (Add MCP protocol version 2025-11-25 #1304 fixed one instance by adding 2025-11-25 to the known list; the general fallback remained.)server/discover, no-32022— modern-era requests carrying_meta["io.modelcontextprotocol/protocolVersion"]are served handshake-shaped results, which then fail the SDK's strict wire validation (ListToolsResult: cacheScope/resultType/ttlMs Field required) instead of getting a clean version error.resultType,ttlMs/cacheScope(ontools/list/prompts/list), and the_meta["io.modelcontextprotocol/serverInfo"]stamp are absent.Proposed contribution (PR ladder)
-32601 Method not foundfor unknown methods that carry anid(notifications stay silently dropped). Makes the V2 auto-mode fallback deterministic instead of accidental.initializewith an unknown requested version answers the latest supported version, via a registry-derivedProtocolVersion.latestVersion()(single source, can't drift as versions are added).server/discover+-32022: discover answers a wire-completeDiscoverResultwithsupportedVersionsfrom the registry; requests whose_metanames an unsupported version get-32022with{"supported": [...], "requested": "..."}. A handshake-only server advertising no modern version is the documented "explicit legacy advertisement" that the Python/TypeScript/Go clients all answer by falling back toinitializecleanly — so this PR is safe and useful before full modern support lands.params._metawins when it names a known version; the transport-provided hint otherwise), era-conditional result stamping (resultTypeon all results;ttlMs/cacheScopeontools/list/prompts/list;_metaserverInfo) — era-conditional because peers strictly validate results per era in both directions, so handshake-era responses must stay byte-identical;initializecapped at the latest handshake version (modern versions negotiate per request, never via handshake — the stdio transport's captured connection version needs the same clamp); era-aware capabilities (modern advertisement dropslistChanged: true, which we can't honor withoutsubscriptions/listen); modern-era tool failures mapped to-32603with a sanitized message.Each step is fully covered by unit + integration tests, and we've verified the ladder end-to-end: after (4), a Python 2.0.0 client in auto mode adopts the modern era via discover (no
initializeon the wire), pinned-modern clients pass strict wire validation throughtools/listandtools/call, and legacy responses are byte-identical to before (verified by byte-diffing captures).Open questions where we'd like maintainer input
ttlMs: 0/cacheScope: "private"(the Python reference server's defaults — safe for authenticated servers). Should there be a per-method configuration surface onMcpServerBuilder, or are constants acceptable for a first pass?nextCursor— Paginate MCP tools/list and prompts/list #1308 added pagination and Revert "Paginate MCP tools/list and prompts/list" #1312 reverted it a day later, and 2026-07-28 makes each page independently cacheable. What's the current thinking there?500(outside the server-error space) with a full stack trace in the message (createErrorResponsehas a TODO about this). We map modern-era failures to-32603with a sanitized message and left legacy behavior untouched in case consumers depend on it — appetite for fixing legacy too?-32602(invalid params).pingon modern-era requests: removed from the 2026-07-28 surface; the Python reference server answers it-32601there. We stayed lenient (still serve it). Preference?McpServerProxypins one downstream protocol version at first-initialize and stamps it on every proxied request — modern passthrough would need the per-request version threaded throughrpc(). We left this untouched; flagging it as follow-up work.Timeline context
Python SDK 1.x has been in maintenance mode (security/critical fixes only) since 2026-07-28; the TypeScript v1 line is guaranteed bug/security fixes for at least six months from v2's release. There's no announced date for V2 SDKs dropping the legacy-handshake compatibility path, but when that happens, servers on this engine become unreachable for those clients — hence the interest in landing negotiation correctness (PRs 1–3) soon and the modern era (PR 4) after design alignment here.
We're happy to split, reshape, or re-scope any of this to fit your preferences.