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>
This commit is contained in:
parent
1ca3bc301b
commit
5ad96dbfbc
2 changed files with 58 additions and 1 deletions
|
|
@ -8,6 +8,8 @@ import {
|
|||
} from './auth/test_helpers/testClient';
|
||||
import { JsonSchemaRefDerefScenario } from './json-schema-ref-deref';
|
||||
import { getScenario } from '../index';
|
||||
import { sendStatelessRequest } from '../../connection/stateless';
|
||||
import { DRAFT_PROTOCOL_VERSION } from '../../types';
|
||||
|
||||
/**
|
||||
* SEP-2106: implementations MUST NOT automatically dereference $ref values
|
||||
|
|
@ -71,6 +73,36 @@ async function dereferencingClient(serverUrl: string): Promise<void> {
|
|||
await transport.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* SEP-2575 stateless client: probe `server/discover` first, then honor the
|
||||
* negotiated protocol version on subsequent requests. Regression client for
|
||||
* issue #397 — the scenario's hand-rolled `server/discover` advertises the
|
||||
* draft version, so the pinned SDK transport must not reject that version's
|
||||
* MCP-Protocol-Version header on the follow-up tools/list.
|
||||
*/
|
||||
async function statelessDraftClient(serverUrl: string): Promise<void> {
|
||||
const discover = await sendStatelessRequest(serverUrl, 'server/discover');
|
||||
const supportedVersions = (
|
||||
discover.body?.result as { supportedVersions?: string[] } | undefined
|
||||
)?.supportedVersions;
|
||||
if (!supportedVersions?.includes(DRAFT_PROTOCOL_VERSION)) {
|
||||
throw new Error(
|
||||
`server/discover did not advertise ${DRAFT_PROTOCOL_VERSION}: ` +
|
||||
JSON.stringify(supportedVersions)
|
||||
);
|
||||
}
|
||||
|
||||
// sendStatelessRequest sends MCP-Protocol-Version: <draft> by default —
|
||||
// exactly what a client that honors the negotiated version would do.
|
||||
const tools = await sendStatelessRequest(serverUrl, 'tools/list');
|
||||
if (tools.status !== 200 || tools.body?.result === undefined) {
|
||||
throw new Error(
|
||||
`tools/list with the negotiated draft version failed: HTTP ${tools.status} ` +
|
||||
JSON.stringify(tools.body ?? tools.text)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe('json-schema-ref-no-deref (SEP-2106)', () => {
|
||||
test('scenario is registered', () => {
|
||||
expect(getScenario('json-schema-ref-no-deref')).toBeDefined();
|
||||
|
|
@ -83,6 +115,13 @@ describe('json-schema-ref-no-deref (SEP-2106)', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('stateless draft client passes: negotiated version reaches tools/list (issue #397)', async () => {
|
||||
await runClientAgainstScenario(
|
||||
new InlineClientRunner(statelessDraftClient),
|
||||
'json-schema-ref-no-deref'
|
||||
);
|
||||
});
|
||||
|
||||
test('dereferencing client fails: canary fetch is detected', async () => {
|
||||
await runClientAgainstScenario(
|
||||
new InlineClientRunner(dereferencingClient),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import type { ScenarioContext } from '../../mock-server';
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import {
|
||||
ListToolsRequestSchema,
|
||||
LATEST_PROTOCOL_VERSION as SDK_LATEST_PROTOCOL_VERSION
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import type { Scenario, ConformanceCheck } from '../../types';
|
||||
import express, { Request, Response } from 'express';
|
||||
import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../types';
|
||||
|
|
@ -132,6 +135,21 @@ The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at
|
|||
}
|
||||
});
|
||||
}
|
||||
// Second half of the same workaround: the pinned SDK transport
|
||||
// whitelists MCP-Protocol-Version headers and would reject the draft
|
||||
// version that the server/discover response above advertises with an
|
||||
// HTTP 400. Rewrite it to the newest version the SDK understands so a
|
||||
// client that honors the negotiated version can reach tools/list.
|
||||
if (req.headers['mcp-protocol-version'] === DRAFT_PROTOCOL_VERSION) {
|
||||
req.headers['mcp-protocol-version'] = SDK_LATEST_PROTOCOL_VERSION;
|
||||
// The SDK's Node adapter rebuilds its web-standard Request from
|
||||
// rawHeaders, not the parsed headers object, so patch those too.
|
||||
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
||||
if (req.rawHeaders[i].toLowerCase() === 'mcp-protocol-version') {
|
||||
req.rawHeaders[i + 1] = SDK_LATEST_PROTOCOL_VERSION;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Stateless: fresh server and transport per request
|
||||
const server = createMcpServer(this.canaryUrl(), () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue