Skip to content

Commit 6870ae4

Browse files
Add wire-level tests pinning the Unsupported protocol version error
The go-sdk client substring-matches 'Unsupported protocol version' on non-2xx response bodies to detect protocol-version fallback (its structured parse path rejects the id:null error body), so the HTTP 400 status and that literal substring are load-bearing interop surface. Assert them from the response bytes; assert the full message shape against a string derived from SUPPORTED_PROTOCOL_VERSIONS so the tests survive additions to the supported-versions list.
1 parent 7dc3f31 commit 6870ae4

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { randomUUID } from 'node:crypto';
2+
3+
import type { JSONRPCMessage } from '@modelcontextprotocol/core';
4+
import { SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/core';
5+
6+
import { McpServer } from '../../src/server/mcp.js';
7+
import { WebStandardStreamableHTTPServerTransport } from '../../src/server/streamableHttp.js';
8+
9+
/**
10+
* Wire-level continuity tests for the "Unsupported protocol version" rejection.
11+
*
12+
* The load-bearing surface is the HTTP 400 status plus the literal substring
13+
* `Unsupported protocol version` in the response body: the go-sdk client
14+
* substring-matches exactly that phrase on non-2xx bodies to drive its
15+
* protocol-version fallback (`streamableClientConn.checkResponse` in
16+
* go-sdk's `mcp/streamable.go`). Its structured JSON-RPC parse path rejects
17+
* this server's `id: null` error body, so the substring is the operative
18+
* interop signal — it must keep appearing verbatim in the wire bytes across
19+
* refactors of the transport internals.
20+
*
21+
* The rest of the message (prefix, echoed version, supported-versions list)
22+
* is asserted against a string derived from `SUPPORTED_PROTOCOL_VERSIONS`
23+
* rather than a frozen byte literal, so these tests survive additions to the
24+
* supported-versions list while still catching any rewording.
25+
*/
26+
27+
const INITIALIZE_MESSAGE = {
28+
jsonrpc: '2.0',
29+
method: 'initialize',
30+
params: {
31+
clientInfo: { name: 'test-client', version: '1.0' },
32+
protocolVersion: '2025-11-25',
33+
capabilities: {}
34+
},
35+
id: 'init-1'
36+
} as JSONRPCMessage;
37+
38+
const TOOLS_LIST_MESSAGE = {
39+
jsonrpc: '2.0',
40+
method: 'tools/list',
41+
params: {},
42+
id: 'tools-1'
43+
} as JSONRPCMessage;
44+
45+
interface JSONRPCErrorBody {
46+
jsonrpc: string;
47+
id: unknown;
48+
error: { code: number; message: string };
49+
}
50+
51+
function postRequest(body: JSONRPCMessage, headers: Record<string, string> = {}): Request {
52+
return new Request('http://localhost/mcp', {
53+
method: 'POST',
54+
headers: {
55+
'Content-Type': 'application/json',
56+
Accept: 'application/json, text/event-stream',
57+
...headers
58+
},
59+
body: JSON.stringify(body)
60+
});
61+
}
62+
63+
async function initializeServer(transport: WebStandardStreamableHTTPServerTransport): Promise<string> {
64+
const response = await transport.handleRequest(postRequest(INITIALIZE_MESSAGE));
65+
expect(response.status).toBe(200);
66+
return response.headers.get('mcp-session-id') as string;
67+
}
68+
69+
async function connectedTransport(supportedProtocolVersions?: string[]): Promise<WebStandardStreamableHTTPServerTransport> {
70+
// `connect()` passes the server's supported protocol versions down to the
71+
// transport, so a custom list is configured on the server options.
72+
const mcpServer = new McpServer(
73+
{ name: 'test-server', version: '1.0.0' },
74+
{ capabilities: {}, ...(supportedProtocolVersions ? { supportedProtocolVersions } : {}) }
75+
);
76+
const transport = new WebStandardStreamableHTTPServerTransport({
77+
sessionIdGenerator: () => randomUUID()
78+
});
79+
await mcpServer.connect(transport);
80+
return transport;
81+
}
82+
83+
describe('Unsupported protocol version - wire literal continuity', () => {
84+
it('rejects an unsupported MCP-Protocol-Version header with HTTP 400, code -32000, and the sniffed literal substring', async () => {
85+
const transport = await connectedTransport();
86+
try {
87+
const sessionId = await initializeServer(transport);
88+
89+
const response = await transport.handleRequest(
90+
postRequest(TOOLS_LIST_MESSAGE, {
91+
'mcp-session-id': sessionId,
92+
'mcp-protocol-version': '2099-01-01'
93+
})
94+
);
95+
96+
expect(response.status).toBe(400);
97+
expect(response.headers.get('content-type')).toContain('application/json');
98+
99+
const rawBody = await response.text();
100+
// The substring deployed clients (go-sdk) sniff must appear
101+
// verbatim in the wire bytes.
102+
expect(rawBody).toContain('Unsupported protocol version');
103+
104+
const body = JSON.parse(rawBody) as JSONRPCErrorBody;
105+
expect(body.jsonrpc).toBe('2.0');
106+
expect(body.id).toBeNull();
107+
expect(body.error.code).toBe(-32_000);
108+
expect(body.error.message).toBe(
109+
`Bad Request: Unsupported protocol version: 2099-01-01 (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`
110+
);
111+
} finally {
112+
await transport.close();
113+
}
114+
});
115+
116+
it('derives the supported-versions suffix from the per-instance supportedProtocolVersions', async () => {
117+
const transport = await connectedTransport(['2025-11-25', '2025-06-18']);
118+
try {
119+
const sessionId = await initializeServer(transport);
120+
121+
const response = await transport.handleRequest(
122+
postRequest(TOOLS_LIST_MESSAGE, {
123+
'mcp-session-id': sessionId,
124+
'mcp-protocol-version': '1999-01-01'
125+
})
126+
);
127+
128+
expect(response.status).toBe(400);
129+
130+
const body = (await response.json()) as JSONRPCErrorBody;
131+
expect(body.error.code).toBe(-32_000);
132+
expect(body.error.message).toBe(
133+
'Bad Request: Unsupported protocol version: 1999-01-01 (supported versions: 2025-11-25, 2025-06-18)'
134+
);
135+
} finally {
136+
await transport.close();
137+
}
138+
});
139+
});

0 commit comments

Comments
 (0)