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:
parent
fee585cf45
commit
5c1b815cd1
5 changed files with 730 additions and 0 deletions
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
236
src/scenarios/client/request-metadata.test.ts
Normal file
236
src/scenarios/client/request-metadata.test.ts
Normal 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'
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
272
src/scenarios/client/request-metadata.ts
Normal file
272
src/scenarios/client/request-metadata.ts
Normal 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 }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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
104
src/seps/sep-2575.yaml
Normal 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
|
||||
Loading…
Reference in a new issue