Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f60e086
feat: vendor spec schema types per-version
felixweinberger May 26, 2026
630ce5e
feat: add Connection abstraction and RunContext
felixweinberger May 26, 2026
276cfc3
refactor: thread RunContext through ClientScenario.run
felixweinberger May 26, 2026
e7c0c09
refactor: migrate server scenarios to ctx.connect() + conn.request()
felixweinberger May 26, 2026
649e81c
fix: normalize Connection error to JsonRpcError; clean up RunContext …
felixweinberger May 26, 2026
afd6278
fix(dns-rebinding): use version-appropriate probe body
felixweinberger May 26, 2026
2b8b15c
feat(everything-server): route stateless carry-forward methods to Mcp…
felixweinberger May 26, 2026
9a5dd63
refactor(connection): drop unused RequestOptions; move sdk-client; ad…
felixweinberger May 26, 2026
9c785f0
fix: address bughunt findings (response.ok check; targetVersion naming)
felixweinberger May 26, 2026
396c055
fix(sse-multiple-streams): keep scenario in draft; version-aware requ…
felixweinberger May 27, 2026
d3ba751
feat: add MockServer abstraction and ScenarioContext
felixweinberger May 26, 2026
64ad717
refactor: thread ScenarioContext through Scenario.start()
felixweinberger May 26, 2026
7add7b3
refactor: migrate tools_call to ctx.createServer; tag 2025-only clien…
felixweinberger May 26, 2026
24b164e
feat(auth): make createServer helper version-aware via ScenarioContext
felixweinberger May 26, 2026
be9a85c
feat(everything-client): pick stateless requester by MCP_CONFORMANCE_…
felixweinberger May 26, 2026
9720252
fix: address review findings on MockServer (dead opts, shared validat…
felixweinberger May 26, 2026
8459baf
fix(mock-server): record stateless requests before validation; docume…
pcarleton Jun 2, 2026
8077d8d
refactor(mock-server): tri-state result for validateStatelessRequest …
pcarleton Jun 2, 2026
3dce0e8
feat(runner): single-source the version→lifecycle mapping; export MCP…
pcarleton Jun 2, 2026
b56fbc2
fix(everything-client): list-only flow for json-schema-ref-no-deref
pcarleton Jun 2, 2026
8f91523
fix(tools_call): make getChecks() idempotent
pcarleton Jun 2, 2026
10712a5
fix: ensure dispatch and transport cleanup runs when handlers throw
pcarleton Jun 2, 2026
b74dca4
fix(everything-client): send Accept header on stateless requests
pcarleton Jun 2, 2026
1f18fec
chore: exclude .claude/ from vitest globs
pcarleton Jun 2, 2026
6334935
fix(request-metadata): use spec error code -32004 for unsupported pro…
pcarleton Jun 2, 2026
2ee17c9
fix(auth): register transport cleanup before handleRequest in createS…
pcarleton Jun 2, 2026
5be5031
fix(everything-server): return JSON-RPC errors from stateless list di…
pcarleton Jun 2, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ dist/
.idea/
.claude/settings.local.json
.sdk-under-test/
.sync-schema-tmp/
10 changes: 10 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@
# repo's `prettier --check .` would reformat the file and fight the generator's
# output (and the refresh workflow's `git diff` check).
src/seps/traceability.json

# Vendored verbatim from modelcontextprotocol/schema/{version}/schema.ts via
# `npm run sync-schema`. Keep byte-identical with upstream so the SOURCE pin
# is meaningful and re-syncing produces a clean diff.
src/spec-types/*.ts

# Local agent workspace state (e.g. .claude/worktrees/ checkouts). Untracked,
# and the root-anchored ignores above don't match their nested copies, so
# `prettier --check .` would otherwise flag them.
.claude/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ npx @modelcontextprotocol/conformance client --command "<client-command>" --scen
- `--timeout` - Timeout in milliseconds (default: 30000)
- `--verbose` - Show verbose output

The framework appends `<server-url>` as an argument to your command and sets the `MCP_CONFORMANCE_SCENARIO` environment variable to the scenario name. For scenarios that require additional context (e.g., client credentials), the `MCP_CONFORMANCE_CONTEXT` environment variable contains a JSON object with scenario-specific data. When `--spec-version` is passed, its resolved value is forwarded to the client process as `MCP_CONFORMANCE_PROTOCOL_VERSION`; example clients can use this value directly as their `protocolVersion`. SDKs that hard-code their protocol version can ignore it.
The framework appends `<server-url>` as an argument to your command and sets the `MCP_CONFORMANCE_SCENARIO` environment variable to the scenario name. For scenarios that require additional context (e.g., client credentials), the `MCP_CONFORMANCE_CONTEXT` environment variable contains a JSON object with scenario-specific data. When `--spec-version` is passed, its resolved value is forwarded to the client process as `MCP_CONFORMANCE_PROTOCOL_VERSION`; example clients can use this value directly as their `protocolVersion`. SDKs that hard-code their protocol version can ignore it. The runner also sets `MCP_CONFORMANCE_LIFECYCLE` to `stateful` or `stateless` based on the resolved spec version, so clients can pick the right lifecycle (initialize handshake vs per-request `_meta`) without maintaining their own version-to-lifecycle mapping.

### Server Testing

Expand Down
153 changes: 132 additions & 21 deletions examples/clients/typescript/everything-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '@modelcontextprotocol/sdk/client/auth-extensions.js';
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { ClientConformanceContextSchema } from '../../../src/schemas/context.js';
import { DRAFT_PROTOCOL_VERSION } from '../../../src/types.js';
import {
auth,
extractWWWAuthenticateParams
Expand Down Expand Up @@ -70,10 +71,96 @@ export function getHandler(scenarioName: string): ScenarioHandler | undefined {
}

// ============================================================================
// Basic scenarios (initialize, tools-call)
// Stateless requester (SEP-2575 / 2026-x lifecycle)
//
// Shim for the fact that the SDK Client doesn't support stateless mode yet.
// Carry-forward handlers below pick this when the runner says the resolved
// spec version is stateless, so the same handler exercises both lifecycles.
// ============================================================================

const PROTOCOL_VERSION = process.env.MCP_CONFORMANCE_PROTOCOL_VERSION;

// Lifecycle decision: trust the runner's MCP_CONFORMANCE_LIFECYCLE when set;
// fall back to comparing against the draft version for older runners that
// only export the protocol version.
const USE_STATELESS_LIFECYCLE = process.env.MCP_CONFORMANCE_LIFECYCLE
? process.env.MCP_CONFORMANCE_LIFECYCLE === 'stateless'
: PROTOCOL_VERSION === DRAFT_PROTOCOL_VERSION;

// Wire protocolVersion for stateless requests: the runner-resolved version
// when available (so a dated stateless release is exercised under its own
// identifier), the current draft otherwise.
const STATELESS_PROTOCOL_VERSION = PROTOCOL_VERSION ?? DRAFT_PROTOCOL_VERSION;

const STATELESS_META_BASE = {
'io.modelcontextprotocol/clientInfo': {
name: 'conformance-test-client',
version: '1.0.0'
},
'io.modelcontextprotocol/clientCapabilities': {
tools: {},
roots: {},
sampling: {},
elicitation: {}
}
};

let _nextStatelessId = 1;
async function statelessRequest(
serverUrl: string,
method: string,
params: Record<string, unknown> = {}
): Promise<any> {
const _meta = {
'io.modelcontextprotocol/protocolVersion': STATELESS_PROTOCOL_VERSION,
...STATELESS_META_BASE,
...((params._meta as object | undefined) ?? {})
};
const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Servers built on the SDK's StreamableHTTPServerTransport reject
// requests that don't accept both JSON and SSE responses.
Accept: 'application/json, text/event-stream',
'MCP-Protocol-Version': STATELESS_PROTOCOL_VERSION
},
body: JSON.stringify({
jsonrpc: '2.0',
id: _nextStatelessId++,
method,
params: { ...params, _meta }
})
});
const body = await response.json();
if (body.error) {
throw new Error(
`${method} failed: ${body.error.code} ${body.error.message}`
);
}
return body.result;
}

// ============================================================================
// Basic scenarios (initialize, tools_call)
// ============================================================================

async function runBasicClient(serverUrl: string): Promise<void> {
if (USE_STATELESS_LIFECYCLE) {
logger.debug('Stateless lifecycle: calling tools/list + tools/call');
const list = await statelessRequest(serverUrl, 'tools/list');
logger.debug('Successfully listed tools:', JSON.stringify(list));
const tool = list?.tools?.[0];
if (tool) {
const result = await statelessRequest(serverUrl, 'tools/call', {
name: tool.name,
arguments: { a: 2, b: 3 }
});
logger.debug('Successfully called tool:', JSON.stringify(result));
}
return;
}

const client = new Client(
{ name: 'test-client', version: '1.0.0' },
{ capabilities: {} }
Expand All @@ -84,20 +171,52 @@ async function runBasicClient(serverUrl: string): Promise<void> {
await client.connect(transport);
logger.debug('Successfully connected to MCP server');

await client.listTools();
const list = await client.listTools();
logger.debug('Successfully listed tools');

const tool = list.tools[0];
if (tool) {
await client.callTool({ name: tool.name, arguments: { a: 2, b: 3 } });
logger.debug('Successfully called tool');
}

await transport.close();
logger.debug('Connection closed successfully');
}

registerScenarios(['initialize', 'tools-call'], runBasicClient);
registerScenarios(['initialize', 'tools_call', 'tools-call'], runBasicClient);

// SEP-2106: json-schema-ref-no-deref advertises a tool whose inputSchema
// contains a network-URI $ref. A conformant client lists tools normally and
// simply never fetches that URI, so the basic connect+listTools flow is the
// correct behavior here.
registerScenario('json-schema-ref-no-deref', runBasicClient);
// simply never fetches that URI. The scenario's mock only serves tools/list,
// so this handler stops after listing instead of reusing runBasicClient
// (whose tools/call would get -32601 and fail the run).
async function runListToolsOnlyClient(serverUrl: string): Promise<void> {
if (USE_STATELESS_LIFECYCLE) {
logger.debug('Stateless lifecycle: calling tools/list');
const list = await statelessRequest(serverUrl, 'tools/list');
logger.debug('Successfully listed tools:', JSON.stringify(list));
return;
}

const client = new Client(
{ name: 'test-client', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl));

await client.connect(transport);
logger.debug('Successfully connected to MCP server');

await client.listTools();
logger.debug('Successfully listed tools');

await transport.close();
logger.debug('Connection closed successfully');
}

registerScenario('json-schema-ref-no-deref', runListToolsOnlyClient);

// ============================================================================
// request-metadata scenario (SEP-2575)
Expand All @@ -106,20 +225,9 @@ registerScenario('json-schema-ref-no-deref', runBasicClient);
async function runRequestMetadataClient(serverUrl: string): Promise<void> {
logger.debug('Starting request-metadata client flow...');

const meta = {
'io.modelcontextprotocol/clientInfo': {
name: 'conformance-test-client',
version: '1.0.0'
},
'io.modelcontextprotocol/clientCapabilities': {
tools: {},
roots: {},
sampling: {},
elicitation: {}
}
};
const meta = STATELESS_META_BASE;

let activeVersion = 'DRAFT-2026-v1';
let activeVersion = STATELESS_PROTOCOL_VERSION;

const sendRequestWithNegotiation = async (
method: string,
Expand Down Expand Up @@ -155,13 +263,16 @@ async function runRequestMetadataClient(serverUrl: string): Promise<void> {
const clone = response.clone();
try {
const errorResult = await clone.json();
if (errorResult.error?.code === -32001) {
// -32004 UnsupportedProtocolVersionError per the draft spec
if (errorResult.error?.code === -32004) {
logger.debug(
'Received UnsupportedProtocolVersionError, starting negotiation...'
);
const serverSupported: string[] =
errorResult.error.data?.supported || [];
const clientSupported = ['DRAFT-2026-v1'];
const clientSupported = [
...new Set([STATELESS_PROTOCOL_VERSION, DRAFT_PROTOCOL_VERSION])
];
const mutuallySupported = clientSupported.filter((v) =>
serverSupported.includes(v)
);
Expand Down
Loading
Loading