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';
|
} from './auth/test_helpers/testClient';
|
||||||
import { JsonSchemaRefDerefScenario } from './json-schema-ref-deref';
|
import { JsonSchemaRefDerefScenario } from './json-schema-ref-deref';
|
||||||
import { getScenario } from '../index';
|
import { getScenario } from '../index';
|
||||||
|
import { sendStatelessRequest } from '../../connection/stateless';
|
||||||
|
import { DRAFT_PROTOCOL_VERSION } from '../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SEP-2106: implementations MUST NOT automatically dereference $ref values
|
* SEP-2106: implementations MUST NOT automatically dereference $ref values
|
||||||
|
|
@ -71,6 +73,36 @@ async function dereferencingClient(serverUrl: string): Promise<void> {
|
||||||
await transport.close();
|
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)', () => {
|
describe('json-schema-ref-no-deref (SEP-2106)', () => {
|
||||||
test('scenario is registered', () => {
|
test('scenario is registered', () => {
|
||||||
expect(getScenario('json-schema-ref-no-deref')).toBeDefined();
|
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 () => {
|
test('dereferencing client fails: canary fetch is detected', async () => {
|
||||||
await runClientAgainstScenario(
|
await runClientAgainstScenario(
|
||||||
new InlineClientRunner(dereferencingClient),
|
new InlineClientRunner(dereferencingClient),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import type { ScenarioContext } from '../../mock-server';
|
import type { ScenarioContext } from '../../mock-server';
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.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 type { Scenario, ConformanceCheck } from '../../types';
|
||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../types';
|
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 {
|
try {
|
||||||
// Stateless: fresh server and transport per request
|
// Stateless: fresh server and transport per request
|
||||||
const server = createMcpServer(this.canaryUrl(), () => {
|
const server = createMcpServer(this.canaryUrl(), () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue