feat: add version negotiation conformance tests (closes #102)#224
Open
alexdoroshevich wants to merge 2 commits intomodelcontextprotocol:mainfrom
Open
feat: add version negotiation conformance tests (closes #102)#224alexdoroshevich wants to merge 2 commits intomodelcontextprotocol:mainfrom
alexdoroshevich wants to merge 2 commits intomodelcontextprotocol:mainfrom
Conversation
2e571ae to
c408eca
Compare
added 2 commits
April 13, 2026 20:28
…xtprotocol#102) Adds ServerVersionNegotiationScenario with three independent checks: - version-echo: client sends 2025-11-25, server must echo the same version - version-negotiate: client sends 1999-01-01, server must respond with a supported version (not a JSON-RPC error). Reports WARNING for unrecognised future versions to avoid false failures on servers running newer spec releases. - http-protocol-version-header: subsequent ping carrying the negotiated MCP-Protocol-Version header must be accepted (2xx or -32601). Why raw fetch() instead of connectToServer(): the SDK Client hard-codes the protocol version with no public override API. Why a streaming SSE reader: StreamableHTTP keeps the connection open indefinitely; response.text() blocks forever. readFirstSSEMessage() reads incrementally, extracts the first data: event, and calls reader.cancel() in a finally block to close the connection cleanly. The timeout sentinel is created once before the read loop so a slow server cannot extend the deadline. check1Threw gates check 3 as SKIPPED (not FAILURE) when the server is unreachable. negotiatedVersion captures the server-returned version from check 1 for use as the MCP-Protocol-Version header in check 3. specVersions is restricted to ['2025-11-25']: all checks send/expect that version; listing older versions would cause false FAILUREs on servers that implement only those versions.
13 tests covering all check outcomes without a real server (fetch stubbed with vi.stubGlobal): version-echo: SUCCESS (happy path), FAILURE (wrong version, JSON-RPC error, missing protocolVersion field) version-negotiate: FAILURE (JSON-RPC error, 1999-01-01 echo-back), WARNING (2026-03-15 -- valid format but not a known spec release) http-protocol-version-header: SUCCESS (-32601 method not found), FAILURE (HTTP 4xx), SKIPPED (transport error, fetch called exactly twice), WARNING (unexpected JSON-RPC error, unparseable 2xx body) Regression: check 3 sends the version returned by check 1 as the MCP-Protocol-Version header, not the hardcoded CURRENT_PROTOCOL_VERSION constant.
8a87a66 to
eaffb31
Compare
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
Issue #102 identified three uncovered MUST requirements from the MCP lifecycle spec
and HTTP transport spec. The existing server-initialize scenario only verifies that
initialization succeeds; it does not check which version is echoed back or how the
server handles an unsupported version.
Spec text (2025-11-25):
Sec.3.1 Version Negotiation
Sec.2.3 Transport / Protocol Version Header
What This PR Tests
One
ClientScenario(server-version-negotiation) with three checks:version-echo2025-11-25-- server must echo same versionversion-negotiate1999-01-01-- server must respond with a supported version, not an error{"error": ...}instead of negotiatinghttp-protocol-version-headerMCP-Protocol-Version: <negotiated>header -- server must not reject itspecVersionsis set to['2025-11-25']only. All three checks send/expect2025-11-25; listing older versions would produce false FAILUREs on servers that implement only those versions.Design Decisions
Why raw
fetch()instead ofconnectToServer()The TypeScript SDK's
Clienthard-codes the protocol version in every initializerequest and provides no public API to override it. Version negotiation checks
must control
protocolVersiondirectly, so rawfetch()is the only option.Why a streaming SSE reader
The MCP StreamableHTTP transport responds with
Content-Type: text/event-streamfor all POST requests -- including
initializeandping-- and keeps theconnection open indefinitely. Calling
response.text()on such a body blocksforever.
readFirstSSEMessage()reads incrementally, extracts the firstdata: {...}event, and callsreader.cancel()in afinallyblock to closethe connection cleanly.
The timeout sentinel is created once before the read loop (not per-chunk) so a
server that trickles data cannot extend the deadline indefinitely.
Three-tier version validation in
version-negotiateThe check uses
KNOWN_SPEC_VERSIONS(the three published releases) plus a year floor to distinguish three outcomes:YYYY-MM-DDformat, or year < 2025 (catches echo-back of1999-01-01)KNOWN_SPEC_VERSIONS(server may be running a newer spec release the harness doesn't know about yet -- updateKNOWN_SPEC_VERSIONSto verify fully)This avoids both false FAILUREs on servers running a new spec and false SUCCESS on servers that blindly echo back whatever the client sends.
MCP-Protocol-Versionheader uses the negotiated versionCheck 3 sends the version the server actually returned in check 1 (captured in
negotiatedVersion), not the hardcodedCURRENT_PROTOCOL_VERSIONconstant. This ensures the header probe is honest even when check 1 fails with a version mismatch.Session cleanup
Sessions opened by each check are collected in
sessionsToClearand deleted viaDELETE /mcpin an outerfinallyblock to avoid orphaned sessions in the test server.Files Changed
No changes to
everything-server.ts-- the SDK'sMcpServeralready implementsversion negotiation correctly, so the scenario passes against the reference server.
How Has This Been Tested?
Manual run against the everything-server (passing case):
Unit tests proving failure paths (
version-negotiation.test.ts, 13 tests):The unit tests stub
fetch()and prove the scenario catches broken servers -- not just that it passes on a well-behaved one:version-echoFAILURE: wrong version echoed, JSON-RPC error returned,protocolVersionmissingversion-negotiateFAILURE: JSON-RPC error returned instead of negotiating,1999-01-01echoed back (year < 2025)version-negotiateWARNING: future unknown version2026-03-15(valid format, year >= 2025, not inKNOWN_SPEC_VERSIONS)http-protocol-version-header: FAILURE on HTTP 4xx, SKIPPED when server unreachable, SUCCESS on-32601(ping optional), WARNING on unexpected errorsMCP-Protocol-Versionheader, not the hardcoded constantFull suite:
npm test-- 106/106 passed.Lint/format:
npm run lint-- clean. No non-ASCII characters in either new file.Pre-push hook: both
TestandCode Formattinghooks passed.Breaking Changes
None.
Types of Changes
Checklist
specReferencesincluded on every checknpm run buildpassesnpm run typecheckpassesnpm testpasses (106/106)npm run lintpassesallClientScenariosListunder lifecycle scenariosDELETE /mcpAdditional Context
The
version-negotiatecheck targets the most commonly mis-implemented requirementin this section. Many servers reject an
initializewith an unsupported version viaa JSON-RPC error, whereas the spec requires responding with a supported version instead.
This check catches that pattern in any SDK that gets version negotiation wrong.