Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/scenarios/client/json-schema-ref-deref.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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),
Expand Down
20 changes: 19 additions & 1 deletion src/scenarios/client/json-schema-ref-deref.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(), () => {
Expand Down
Loading