Skip to content

Commit 5ad96db

Browse files
claude[bot]claude
andauthored
fix(client): accept the draft protocol version the json-schema-ref-no-deref mock advertises (#398)
The scenario's hand-rolled server/discover handler advertises supportedVersions: ['2026-07-28'], but every other method is delegated to StreamableHTTPServerTransport from the pinned SDK (^1.29.0), whose version whitelist tops out at 2025-11-25. A SEP-2575 client that honors the negotiated version then gets HTTP 400 on tools/list, and the sep-2106-no-network-ref-deref check can never run. Extend the existing server/discover workaround to the version check: rewrite an incoming MCP-Protocol-Version: 2026-07-28 header to the SDK's LATEST_PROTOCOL_VERSION before delegating (patching rawHeaders too, since the SDK's Node adapter rebuilds its web-standard Request from rawHeaders). Add a stateless-draft-client regression test. Fixes #397 Claude-Session: https://claude.ai/code/session_01N4TdyLB13XSsPwNScYHf4C Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1ca3bc3 commit 5ad96db

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

src/scenarios/client/json-schema-ref-deref.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
} from './auth/test_helpers/testClient';
99
import { JsonSchemaRefDerefScenario } from './json-schema-ref-deref';
1010
import { getScenario } from '../index';
11+
import { sendStatelessRequest } from '../../connection/stateless';
12+
import { DRAFT_PROTOCOL_VERSION } from '../../types';
1113

1214
/**
1315
* SEP-2106: implementations MUST NOT automatically dereference $ref values
@@ -71,6 +73,36 @@ async function dereferencingClient(serverUrl: string): Promise<void> {
7173
await transport.close();
7274
}
7375

76+
/**
77+
* SEP-2575 stateless client: probe `server/discover` first, then honor the
78+
* negotiated protocol version on subsequent requests. Regression client for
79+
* issue #397 — the scenario's hand-rolled `server/discover` advertises the
80+
* draft version, so the pinned SDK transport must not reject that version's
81+
* MCP-Protocol-Version header on the follow-up tools/list.
82+
*/
83+
async function statelessDraftClient(serverUrl: string): Promise<void> {
84+
const discover = await sendStatelessRequest(serverUrl, 'server/discover');
85+
const supportedVersions = (
86+
discover.body?.result as { supportedVersions?: string[] } | undefined
87+
)?.supportedVersions;
88+
if (!supportedVersions?.includes(DRAFT_PROTOCOL_VERSION)) {
89+
throw new Error(
90+
`server/discover did not advertise ${DRAFT_PROTOCOL_VERSION}: ` +
91+
JSON.stringify(supportedVersions)
92+
);
93+
}
94+
95+
// sendStatelessRequest sends MCP-Protocol-Version: <draft> by default —
96+
// exactly what a client that honors the negotiated version would do.
97+
const tools = await sendStatelessRequest(serverUrl, 'tools/list');
98+
if (tools.status !== 200 || tools.body?.result === undefined) {
99+
throw new Error(
100+
`tools/list with the negotiated draft version failed: HTTP ${tools.status} ` +
101+
JSON.stringify(tools.body ?? tools.text)
102+
);
103+
}
104+
}
105+
74106
describe('json-schema-ref-no-deref (SEP-2106)', () => {
75107
test('scenario is registered', () => {
76108
expect(getScenario('json-schema-ref-no-deref')).toBeDefined();
@@ -83,6 +115,13 @@ describe('json-schema-ref-no-deref (SEP-2106)', () => {
83115
);
84116
});
85117

118+
test('stateless draft client passes: negotiated version reaches tools/list (issue #397)', async () => {
119+
await runClientAgainstScenario(
120+
new InlineClientRunner(statelessDraftClient),
121+
'json-schema-ref-no-deref'
122+
);
123+
});
124+
86125
test('dereferencing client fails: canary fetch is detected', async () => {
87126
await runClientAgainstScenario(
88127
new InlineClientRunner(dereferencingClient),

src/scenarios/client/json-schema-ref-deref.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { ScenarioContext } from '../../mock-server';
22
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
33
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4-
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4+
import {
5+
ListToolsRequestSchema,
6+
LATEST_PROTOCOL_VERSION as SDK_LATEST_PROTOCOL_VERSION
7+
} from '@modelcontextprotocol/sdk/types.js';
58
import type { Scenario, ConformanceCheck } from '../../types';
69
import express, { Request, Response } from 'express';
710
import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../types';
@@ -132,6 +135,21 @@ The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at
132135
}
133136
});
134137
}
138+
// Second half of the same workaround: the pinned SDK transport
139+
// whitelists MCP-Protocol-Version headers and would reject the draft
140+
// version that the server/discover response above advertises with an
141+
// HTTP 400. Rewrite it to the newest version the SDK understands so a
142+
// client that honors the negotiated version can reach tools/list.
143+
if (req.headers['mcp-protocol-version'] === DRAFT_PROTOCOL_VERSION) {
144+
req.headers['mcp-protocol-version'] = SDK_LATEST_PROTOCOL_VERSION;
145+
// The SDK's Node adapter rebuilds its web-standard Request from
146+
// rawHeaders, not the parsed headers object, so patch those too.
147+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
148+
if (req.rawHeaders[i].toLowerCase() === 'mcp-protocol-version') {
149+
req.rawHeaders[i + 1] = SDK_LATEST_PROTOCOL_VERSION;
150+
}
151+
}
152+
}
135153
try {
136154
// Stateless: fresh server and transport per request
137155
const server = createMcpServer(this.canaryUrl(), () => {

0 commit comments

Comments
 (0)