feat: add client conformance tests for SEP-2575 (#270)

* feat: merge latest request-metadata scenario stacked branch

* test: implement optional capability conditional checks and simulated negotiation retry
This commit is contained in:
Anubhav Dhawan 2026-05-20 01:40:52 +05:30 • committed by GitHub
commit 5c1b815cd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 730 additions and 0 deletions

View file

@ -87,6 +87,122 @@ async function runBasicClient(serverUrl: string): Promise<void> {
registerScenarios(['initialize', 'tools-call'], runBasicClient);
// ============================================================================
// request-metadata scenario (SEP-2575)
// ============================================================================
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: {}
}
};
let activeVersion = 'DRAFT-2026-v1';
const sendRequestWithNegotiation = async (
method: string,
requestId: string | number,
params: any
): Promise<any> => {
const getPayload = (version: string) => ({
jsonrpc: '2.0',
id: requestId,
method,
params: {
...params,
_meta: {
...params?._meta,
'io.modelcontextprotocol/protocolVersion': version
}
}
});
const send = async (version: string) => {
return fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'MCP-Protocol-Version': version
},
body: JSON.stringify(getPayload(version))
});
};
let response = await send(activeVersion);
if (response.status === 400) {
const clone = response.clone();
try {
const errorResult = await clone.json();
if (errorResult.error?.code === -32001) {
logger.debug(
'Received UnsupportedProtocolVersionError, starting negotiation...'
);
const serverSupported: string[] =
errorResult.error.data?.supported || [];
const clientSupported = ['DRAFT-2026-v1'];
const mutuallySupported = clientSupported.filter((v) =>
serverSupported.includes(v)
);
if (mutuallySupported.length > 0) {
activeVersion = mutuallySupported[0];
logger.debug(
`Mutually supported version found: ${activeVersion}. Retrying...`
);
response = await send(activeVersion);
} else {
logger.debug('No mutually supported version found. Aborting.');
}
}
} catch (err) {
logger.debug('Failed to parse error response as JSON:', err);
}
}
if (!response.ok) {
throw new Error(`${method} failed: ${response.status}`);
}
return response.json();
};
// Call server/discover (optional for clients, but every POST still needs
// the header + _meta).
logger.debug('Calling server/discover...');
const discoverResult = await sendRequestWithNegotiation(
'server/discover',
'discover-1',
{ _meta: meta }
);
logger.debug(
'Successfully discovered server capabilities:',
JSON.stringify(discoverResult.result)
);
// Call tools/list with required inline _meta tags and header
logger.debug('Calling tools/list with inline _meta...');
const toolsResult = await sendRequestWithNegotiation('tools/list', 2, {
_meta: meta
});
logger.debug(
'Successfully listed tools statelessly:',
JSON.stringify(toolsResult.result)
);
logger.debug('request-metadata client flow completed successfully');
}
// Register the scenario handler
registerScenario('request-metadata', runRequestMetadataClient);
// ============================================================================
// Auth scenarios - well-behaved client
// ============================================================================

View file

@ -0,0 +1,236 @@
import { describe, test, expect } from 'vitest';
import {
runClientAgainstScenario,
InlineClientRunner
} from './auth/test_helpers/testClient';
import { getHandler } from '../../../examples/clients/typescript/everything-client';
import { getScenario } from '../index';
// A bad client that does not send _meta
async function badClient(serverUrl: string) {
const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {} // Missing _meta
})
});
return response.json();
}
const goodMeta = {
'io.modelcontextprotocol/protocolVersion': 'DRAFT-2026-v1',
'io.modelcontextprotocol/clientInfo': { name: 'test', version: '1.0' },
'io.modelcontextprotocol/clientCapabilities': {}
};
// A client that misses the HTTP header
async function missingHeaderClient(serverUrl: string) {
const response = await fetch(serverUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // Missing MCP-Protocol-Version header
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: { _meta: goodMeta }
})
});
return response.json();
}
// A client whose header disagrees with _meta.protocolVersion
async function mismatchedHeaderClient(serverUrl: string) {
const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'MCP-Protocol-Version': '2025-11-25' // != _meta.protocolVersion
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: { _meta: goodMeta }
})
});
return response.json();
}
// A client that fails to negotiate/retry on a 400 response
async function nonRetryingClient(serverUrl: string) {
const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'MCP-Protocol-Version': 'DRAFT-2026-v1'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: { _meta: goodMeta }
})
});
return response.json();
}
// A client that has empty version intersection and terminates
async function incompatibleVersionClient(serverUrl: string) {
const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'MCP-Protocol-Version': 'UNSUPPORTED-VERSION'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {
_meta: {
...goodMeta,
'io.modelcontextprotocol/protocolVersion': 'UNSUPPORTED-VERSION'
}
}
})
});
if (response.status === 400) {
const body = await response.json();
if (body.error?.code === -32001) {
return body; // Abort cleanly
}
}
return response.json();
}
// A client that sends invalid (non-object) capabilities
async function malformedCapabilitiesClient(serverUrl: string) {
const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'MCP-Protocol-Version': 'DRAFT-2026-v1'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {
_meta: {
...goodMeta,
'io.modelcontextprotocol/clientCapabilities': {
roots: 'malformed-string',
sampling: {},
elicitation: true
}
}
}
})
});
return response.json();
}
describe('request-metadata client scenario — positive test', () => {
test('everything-client passes request-metadata scenario with success status for optional capabilities', async () => {
const clientFn = getHandler('request-metadata');
if (!clientFn) {
throw new Error('No handler registered for scenario: request-metadata');
}
const scenario = getScenario('request-metadata');
if (!scenario) {
throw new Error('Scenario not found');
}
const runner = new InlineClientRunner(clientFn);
await runClientAgainstScenario(runner, 'request-metadata');
// Extract checks directly from the scenario instance
const checks = scenario.getChecks();
// 4-Line Bulk Assertion Loop
for (const check of checks) {
expect(check.status).not.toBe('FAILURE');
expect(check.status).not.toBe('WARNING');
}
// Strategic Targeted Optional Assertions
expect(
checks.find((c) => c.id === 'sep-2575-client-declares-roots-capability')
?.status
).toBe('SUCCESS');
expect(
checks.find(
(c) => c.id === 'sep-2575-client-declares-sampling-capability'
)?.status
).toBe('SUCCESS');
expect(
checks.find(
(c) => c.id === 'sep-2575-client-declares-elicitation-capability'
)?.status
).toBe('SUCCESS');
// Assert version negotiation retry succeeded
expect(
checks.find((c) => c.id === 'sep-2575-client-retry-supported-version')
?.status
).toBe('SUCCESS');
});
});
describe('request-metadata client scenario — negative tests', () => {
test('client fails when omitting _meta', async () => {
const runner = new InlineClientRunner(badClient);
await runClientAgainstScenario(runner, 'request-metadata', {
expectedFailureSlugs: [
'sep-2575-client-populates-meta',
'sep-2575-http-client-sends-version-header'
]
});
});
test('client fails when missing version header', async () => {
const runner = new InlineClientRunner(missingHeaderClient);
await runClientAgainstScenario(runner, 'request-metadata', {
expectedFailureSlugs: ['sep-2575-http-client-sends-version-header']
});
});
test('client fails when header disagrees with _meta', async () => {
const runner = new InlineClientRunner(mismatchedHeaderClient);
await runClientAgainstScenario(runner, 'request-metadata', {
expectedFailureSlugs: ['sep-2575-http-version-header-matches-meta']
});
});
test('client fails retry check when it does not handle 400 rejection', async () => {
const runner = new InlineClientRunner(nonRetryingClient);
await runClientAgainstScenario(runner, 'request-metadata', {
expectedFailureSlugs: ['sep-2575-client-retry-supported-version']
});
});
test('client aborts cleanly without hanging when negotiation has empty version intersection', async () => {
const runner = new InlineClientRunner(incompatibleVersionClient);
await runClientAgainstScenario(runner, 'request-metadata', {
expectedFailureSlugs: ['sep-2575-client-retry-supported-version']
});
});
test('client triggers failures for malformed capabilities', async () => {
const runner = new InlineClientRunner(malformedCapabilitiesClient);
await runClientAgainstScenario(runner, 'request-metadata', {
expectedFailureSlugs: [
'sep-2575-client-declares-roots-capability',
'sep-2575-client-declares-elicitation-capability'
]
});
});
});

View file

@ -0,0 +1,272 @@
import http from 'http';
import {
Scenario,
ScenarioUrls,
ConformanceCheck,
DRAFT_PROTOCOL_VERSION
} from '../../types';
export class RequestMetadataScenario implements Scenario {
name = 'request-metadata';
readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const;
description =
'Per-request _meta and MCP-Protocol-Version header obligations (SEP-2575)';
private server: http.Server | null = null;
private checks: ConformanceCheck[] = [];
private hasSimulatedRejection = false;
async start(): Promise<ScenarioUrls> {
this.hasSimulatedRejection = false;
this.checks = [];
return new Promise((resolve, reject) => {
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
this.server.on('error', reject);
this.server.listen(0, () => {
const address = this.server!.address();
if (address && typeof address === 'object') {
resolve({ serverUrl: `http://localhost:${address.port}` });
}
});
});
}
async stop(): Promise<void> {
return new Promise((resolve) => {
if (this.server) {
this.server.close(() => {
resolve();
});
} else {
resolve();
}
});
}
getChecks(): ConformanceCheck[] {
return this.checks;
}
private addOrUpdateCheck(check: ConformanceCheck): void {
const index = this.checks.findIndex((c) => c.id === check.id);
if (index !== -1) {
this.checks[index] = check;
} else {
this.checks.push(check);
}
}
private handleRequest(
req: http.IncomingMessage,
res: http.ServerResponse
): void {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
const request = JSON.parse(body);
// Extract version and headers
const meta = request.params?._meta;
const metaVersion = meta?.['io.modelcontextprotocol/protocolVersion'];
const headerVersion = req.headers['mcp-protocol-version'];
// 1. "Every POST request to the MCP endpoint MUST include an
// MCP-Protocol-Version header." — unconditional, so this fires for
// server/discover too.
this.addOrUpdateCheck({
id: 'sep-2575-http-client-sends-version-header',
name: 'ClientSendsVersionHeader',
description: 'Client sends MCP-Protocol-Version header on every POST',
status: headerVersion !== undefined ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
specReferences: [
{
id: 'SEP-2575',
url: 'https://modelcontextprotocol.io/specification/draft/basic/transports#protocol-version-header'
}
],
details: { method: request.method, headerVersion }
});
// 2. "Every client request MUST include the following
// io.modelcontextprotocol/* fields in _meta: protocolVersion,
// clientInfo, clientCapabilities."
const hasClientInfo = meta?.['io.modelcontextprotocol/clientInfo'];
const hasCapabilities =
meta?.['io.modelcontextprotocol/clientCapabilities'];
const metaIsValid = metaVersion && hasClientInfo && hasCapabilities;
this.addOrUpdateCheck({
id: 'sep-2575-client-populates-meta',
name: 'ClientPopulatesMeta',
description:
'Client populates _meta on every request with all three required fields',
status: metaIsValid ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
specReferences: [
{
id: 'SEP-2575',
url: 'https://modelcontextprotocol.io/specification/draft/basic/index#meta'
}
],
details: { method: request.method, meta }
});
// 3. "The header value MUST match the io.modelcontextprotocol/protocolVersion
// field carried in the request body's _meta." Only meaningful when both
// are present; absence is already covered by the two checks above.
if (headerVersion !== undefined && metaVersion !== undefined) {
this.addOrUpdateCheck({
id: 'sep-2575-http-version-header-matches-meta',
name: 'ClientVersionHeaderMatchesMeta',
description:
'MCP-Protocol-Version header matches _meta.protocolVersion',
status: headerVersion === metaVersion ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
specReferences: [
{
id: 'SEP-2575',
url: 'https://modelcontextprotocol.io/specification/draft/basic/transports#protocol-version-header'
}
],
details: { headerVersion, metaVersion }
});
}
// 4. Optional client capabilities conditional verification
const capabilities = meta?.['io.modelcontextprotocol/clientCapabilities'];
const checkOptionalCapability = (
capabilityName: string,
checkId: string,
checkName: string
) => {
let status: 'SUCCESS' | 'FAILURE' | 'SKIPPED' = 'SKIPPED';
if (capabilities && capabilityName in capabilities) {
const val = capabilities[capabilityName];
const isValidObject =
typeof val === 'object' && val !== null && !Array.isArray(val);
status = isValidObject ? 'SUCCESS' : 'FAILURE';
}
this.addOrUpdateCheck({
id: checkId,
name: checkName,
description: `Client declares valid ${capabilityName} capability if present`,
status,
timestamp: new Date().toISOString(),
specReferences: [
{
id: 'SEP-2575',
url: 'https://modelcontextprotocol.io/specification/draft/basic/index#capabilities'
}
],
details: { capabilityValue: capabilities?.[capabilityName] }
});
};
checkOptionalCapability(
'roots',
'sep-2575-client-declares-roots-capability',
'ClientDeclaresRootsCapability'
);
checkOptionalCapability(
'sampling',
'sep-2575-client-declares-sampling-capability',
'ClientDeclaresSamplingCapability'
);
checkOptionalCapability(
'elicitation',
'sep-2575-client-declares-elicitation-capability',
'ClientDeclaresElicitationCapability'
);
// 5. Simulated Version Negotiation Retry Check
if (!this.hasSimulatedRejection) {
this.hasSimulatedRejection = true;
this.addOrUpdateCheck({
id: 'sep-2575-client-retry-supported-version',
name: 'ClientRetrySupportedVersion',
description:
'Client retries with a supported version when first choice is rejected',
status: 'WARNING',
timestamp: new Date().toISOString(),
specReferences: [
{
id: 'SEP-2575',
url: 'https://modelcontextprotocol.io/specification/draft/basic/transports#protocol-version-header'
}
],
details: { headerVersion }
});
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
jsonrpc: '2.0',
id: request.id ?? null,
error: {
code: -32001,
message: 'Unsupported protocol version',
data: {
supported: [DRAFT_PROTOCOL_VERSION]
}
}
})
);
return;
}
const retryCheck = this.checks.find(
(c) => c.id === 'sep-2575-client-retry-supported-version'
);
if (retryCheck) {
if (
headerVersion === DRAFT_PROTOCOL_VERSION &&
metaVersion === DRAFT_PROTOCOL_VERSION
) {
retryCheck.status = 'SUCCESS';
} else {
retryCheck.status = 'WARNING';
}
retryCheck.details = {
...retryCheck.details,
retryHeaderVersion: headerVersion,
retryMetaVersion: metaVersion
};
}
// server/discover is optional for clients (spec: "Clients MAY call it"),
// so no check is emitted; we still respond so a client that does call it
// proceeds normally and exercises the per-request _meta/header checks above.
if (request.method === 'server/discover') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
jsonrpc: '2.0',
id: request.id,
result: {
supportedVersions: [DRAFT_PROTOCOL_VERSION],
capabilities: {},
serverInfo: { name: 'test', version: '1.0' }
}
})
);
return;
}
// Return generic response to unblock client
let result: object = {};
if (request.method === 'tools/list') {
result = { tools: [] };
} else if (request.method === 'tools/call') {
result = { content: [] };
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result }));
});
}
}

View file

@ -13,6 +13,7 @@ import { InitializeScenario } from './client/initialize';
import { ToolsCallScenario } from './client/tools_call';
import { ElicitationClientDefaultsScenario } from './client/elicitation-defaults';
import { SSERetryScenario } from './client/sse-retry';
import { RequestMetadataScenario } from './client/request-metadata';
// Import all new server test scenarios
import { ServerInitializeScenario } from './server/lifecycle';
@ -201,6 +202,7 @@ const scenariosList: Scenario[] = [
new ToolsCallScenario(),
new ElicitationClientDefaultsScenario(),
new SSERetryScenario(),
new RequestMetadataScenario(),
...authScenariosList,
...backcompatScenariosList,
...draftScenariosList,

104
src/seps/sep-2575.yaml Normal file
View file

@ -0,0 +1,104 @@
sep: 2575
spec_url: https://modelcontextprotocol.io/specification/draft/basic/lifecycle
requirements:
- check: sep-2575-client-populates-meta
text: 'Every client request MUST include the following io.modelcontextprotocol/* fields in _meta: protocolVersion, clientInfo, clientCapabilities.'
url: https://modelcontextprotocol.io/specification/draft/basic/index#meta
- check: sep-2575-server-rejects-undeclared-capability
text: 'A server MUST NOT rely on capabilities the client has not declared. If processing a request requires a capability the client did not include in io.modelcontextprotocol/clientCapabilities, the server MUST return a MissingRequiredClientCapabilityError (-32003).'
url: https://modelcontextprotocol.io/specification/draft/basic/index#meta
- check: sep-2575-missing-capability-http-400
text: 'On HTTP, the response status MUST be 400 Bad Request [for MissingRequiredClientCapabilityError].'
url: https://modelcontextprotocol.io/specification/draft/basic/index#meta
- check: sep-2575-server-tags-subscription-id
text: 'On notifications delivered via a subscriptions/listen stream, the server MUST include io.modelcontextprotocol/subscriptionId in _meta so the client can correlate the notification with the originating subscription request.'
url: https://modelcontextprotocol.io/specification/draft/basic/index#meta
- check: sep-2575-server-stateless-no-prior-context
text: 'A server MUST NOT treat connection or process identity as a proxy for conversation or session continuity. / Servers MUST NOT rely on prior requests over the same connection to establish context (e.g., capabilities, protocol version, client identity).'
- check: sep-2575-server-stateless-no-connection-reuse-required
text: 'Servers MUST NOT require that a client reuse the same connection to perform related operations.'
- check: sep-2575-server-unsupported-version-error
text: 'If the server does not implement the requested version (whether the version is unknown to the server, or is a known version the server has chosen not to support), it MUST respond with an UnsupportedProtocolVersionError listing the versions it does support.'
url: https://modelcontextprotocol.io/specification/draft/basic/lifecycle#protocol-version-negotiation
- check: sep-2575-client-retry-supported-version
text: 'The client SHOULD select a mutually supported version from the supported list and retry the request, or surface an error to the user if no compatible version exists.'
url: https://modelcontextprotocol.io/specification/draft/basic/lifecycle#protocol-version-negotiation
- check: sep-2575-server-implements-discover
text: 'Servers MUST implement server/discover.'
url: https://modelcontextprotocol.io/specification/draft/server/discover
- check: sep-2575-http-server-no-independent-requests-on-stream
text: 'The server MUST NOT send independent JSON-RPC requests on this stream. Server-to-client interactions are embedded as input requests inside an IncompleteResult.'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#receiving-messages-1
- check: sep-2575-http-server-disconnect-is-cancel
text: 'Closing the SSE response stream MUST be treated by the server as cancellation of that request.'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#cancellation-1
- check: sep-2575-http-server-stops-on-cancel
text: 'The server SHOULD stop work on the cancelled request as soon as practical and MUST NOT send any further messages for it [HTTP].'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#cancellation-1
- check: sep-2575-http-client-sends-version-header
text: 'Every POST request to the MCP endpoint MUST include an MCP-Protocol-Version header.'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#protocol-version-header
- check: sep-2575-http-version-header-matches-meta
text: 'The header value MUST match the io.modelcontextprotocol/protocolVersion field carried in the request body _meta.'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#protocol-version-header
- check: sep-2575-http-server-header-mismatch-400
text: 'If the values do not match, the server MUST reject the request with 400 Bad Request and a HeaderMismatch JSON-RPC error.'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#protocol-version-header
- check: sep-2575-http-server-unsupported-version-400
text: 'If the server does not implement the requested protocol version, it MUST respond with 400 Bad Request and an UnsupportedProtocolVersionError listing its supported versions.'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#protocol-version-header
- check: sep-2575-http-server-method-not-found-404
text: 'If the server does not implement the requested RPC method, it MUST respond with 404 Not Found and a JSON-RPC error with code -32601 (Method not found).'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#protocol-version-header
- check: sep-2575-server-honors-notification-filter
text: 'The server MUST NOT send notification types the client has not explicitly requested.'
url: https://modelcontextprotocol.io/specification/draft/basic/utilities/subscriptions#opening-a-stream
- check: sep-2575-server-sends-subscription-ack
text: 'The server MUST send notifications/subscriptions/acknowledged as the first message on the stream.'
url: https://modelcontextprotocol.io/specification/draft/basic/utilities/subscriptions#acknowledgment
- check: sep-2575-client-declares-elicitation-capability
text: 'Clients that support elicitation MUST declare the elicitation capability in _meta.io.modelcontextprotocol/clientCapabilities on each request.'
url: https://modelcontextprotocol.io/specification/draft/client/elicitation#capabilities
- check: sep-2575-client-declares-roots-capability
text: 'Clients that support roots MUST declare the roots capability in _meta.io.modelcontextprotocol/clientCapabilities on each request.'
url: https://modelcontextprotocol.io/specification/draft/client/roots#capabilities
- check: sep-2575-client-declares-sampling-capability
text: 'Clients that support sampling MUST declare the sampling capability in _meta.io.modelcontextprotocol/clientCapabilities on each request.'
url: https://modelcontextprotocol.io/specification/draft/client/sampling#capabilities
- check: sep-2575-server-declares-prompts-in-discover
text: 'Servers that support prompts MUST declare the prompts capability in their DiscoverResult.'
url: https://modelcontextprotocol.io/specification/draft/server/prompts#capabilities
- check: sep-2575-server-sends-prompts-list-changed-on-subscription
text: '[A server with the listChanged] capability SHOULD send a notification to clients that have opened a subscriptions/listen stream with promptsListChanged: true.'
url: https://modelcontextprotocol.io/specification/draft/server/prompts#list-changed-notification
- check: sep-2575-server-sends-tools-list-changed-on-subscription
text: '[A server with the listChanged] capability SHOULD send a notification to clients that have opened a subscriptions/listen stream with toolsListChanged: true.'
url: https://modelcontextprotocol.io/specification/draft/server/tools#list-changed-notification
- check: sep-2575-server-no-log-without-loglevel
text: 'The server MUST NOT emit notifications/message for a request that does not include [io.modelcontextprotocol/logLevel in _meta].'
url: https://modelcontextprotocol.io/specification/draft/server/utilities/logging#per-request-log-level
- text: 'State that needs to span multiple requests (e.g., long-running tasks, application-level handles) MUST be referenced by an explicit identifier the client passes on each request.'
excluded: 'architectural guidance, observable only via subscriptionId/task-id rows already listed'
- text: 'To distinguish notifications belonging to different concurrent subscriptions, clients MUST correlate notifications using the io.modelcontextprotocol/subscriptionId field carried in _meta.'
excluded: 'client-internal demux; not observable on the wire from the harness'
- text: 'The client SHOULD check the acknowledged filter against what it requested and handle any unsupported types gracefully.'
excluded: 'internal comparison; "gracefully" has no wire-observable definition'
- text: 'Because there is no per-request status code to drive fallback, a client that supports both eras SHOULD probe with server/discover first [stdio backward compatibility].'
excluded: 'stdio client harness not implemented — see https://github.com/modelcontextprotocol/conformance/issues/258'
url: https://modelcontextprotocol.io/specification/draft/basic/lifecycle#backward-compatibility-with-initialization-based-versions
- text: 'To cancel an in-flight request [on stdio], the client MUST send a notifications/cancelled notification referencing the request ID.'
excluded: 'stdio client harness not implemented — see https://github.com/modelcontextprotocol/conformance/issues/258'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#cancellation
- text: 'Servers SHOULD stop work on a cancelled request as soon as practical and MUST NOT send any further messages for it [stdio].'
excluded: 'stdio client harness not implemented — see https://github.com/modelcontextprotocol/conformance/issues/258'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#cancellation
- text: 'If the server process exits unexpectedly, the client SHOULD restart it.'
excluded: 'stdio client harness not implemented — see https://github.com/modelcontextprotocol/conformance/issues/258'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#unexpected-termination
- text: 'If the server returns UnsupportedProtocolVersionError, [the stdio client] SHOULD retry using one of the advertised supportedVersions rather than falling back to initialize.'
excluded: 'stdio client harness not implemented — see https://github.com/modelcontextprotocol/conformance/issues/258'
url: https://modelcontextprotocol.io/specification/draft/basic/transports#backward-compatibility
- text: 'On stdio, if the connection is terminated and then re-established, the client MUST re-send subscriptions/listen to re-establish its subscriptions.'
excluded: 'stdio client harness not implemented — see https://github.com/modelcontextprotocol/conformance/issues/258'
url: https://modelcontextprotocol.io/specification/draft/basic/utilities/subscriptions#cancellation