diff --git a/src/scenarios/client/json-schema-ref-deref.test.ts b/src/scenarios/client/json-schema-ref-deref.test.ts index a3091933..8445f1b4 100644 --- a/src/scenarios/client/json-schema-ref-deref.test.ts +++ b/src/scenarios/client/json-schema-ref-deref.test.ts @@ -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 { 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 { + 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: 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), diff --git a/src/scenarios/client/json-schema-ref-deref.ts b/src/scenarios/client/json-schema-ref-deref.ts index ebf1367c..aff791d3 100644 --- a/src/scenarios/client/json-schema-ref-deref.ts +++ b/src/scenarios/client/json-schema-ref-deref.ts @@ -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(), () => {