* feat(everything-server): serve resources and caching hints on the stateless draft path The stateless (SEP-2575) request path now answers resources/list, resources/templates/list and resources/read — including the SEP-2164 -32602 + data.uri error for unknown URIs — and includes the SEP-2549 ttlMs/cacheScope hints on its cacheable list results, matching what the draft-spec scenarios exercise. * feat: make the harness's own draft-spec traffic conformant Fixes the class of issues where a strictly-conformant SDK rejects the harness's requests for reasons unrelated to the behaviour under test (#311, #312, #315). - add a shared stateless draft request helper (draft-client.ts) that puts the cross-cutting SEP-2243 headers (Mcp-Method, Mcp-Name, MCP-Protocol-Version, Accept) and SEP-2575 _meta (protocolVersion, clientInfo, clientCapabilities) on every request, with overrides for negative cases and the SHOULD-level retry on UnsupportedProtocolVersionError - migrate the draft scenarios onto it: the SEP-2322 helpers and server-stateless now send Mcp-Method/Mcp-Name (#312); the SEP-2243 server scenarios drop the legacy initialize handshake and carry _meta on every raw request (#311); caching and sep-2164-resource-not-found are stateless DRAFT-2026-v1 requests instead of an SDK 2025-11-25 session (#315) - add a draft self-conformance gate (draft-self-conformance.test.ts) that points the harness's own draft drivers at the existing client-testing judge scenarios (request-metadata, http-standard-headers) and requires zero failures or warnings - align the request-metadata mock's unsupported-version rejection with the draft schema (-32004) and teach the everything-client to recognize it - rewrite the SEP-2549/SEP-2164 negative example servers as stateless draft servers so they keep exercising the violation, not the transport * refactor: rename the stateless request helpers and harden them per review - rename draft-client.ts to stateless-client.ts and name the helpers by feature (sendStatelessRequest, buildStandardHeaders, buildRequestMeta, withRequestMeta, CONFORMANCE_CLIENT_INFO); 'draft' now only appears in DRAFT_PROTOCOL_VERSION, the one name that tracks the moving target - retry only genuine UnsupportedProtocolVersionError rejections (-32004, tolerating -32001/-32602) and only with recognized versions; record the retry on the response - parse text/event-stream responses incrementally and resolve on the matching event instead of blocking until the timeout - keep the MCP-Protocol-Version header and _meta protocolVersion in sync when the version is overridden via meta overrides - make server-stateless's sendRpc SSE-tolerant - emit explicit setup failures in the SEP-2243 server scenarios when the tools/list discovery fails instead of misleading per-case results - latch the worst status per check in the request-metadata judge so an early violation cannot be masked by a later conformant request - rename the gate to harness-traffic-conformance.test.ts and derive its driver list from the draft suite with documented per-judge exclusions (35 pairings) - everything-server: advertise resources in stateless server/discover and use -32004 for unsupported protocol version - add unit tests for the helper (retry guard, SSE handling, header/_meta version sync, default headers) * refactor: slim the stateless client helper for the initial merge
This commit is contained in:
parent
e2e65e5595
commit
076b417b23
1 changed files with 894 additions and 596 deletions
|
|
@ -20,7 +20,7 @@ import {
|
|||
ConformanceCheck,
|
||||
DRAFT_PROTOCOL_VERSION
|
||||
} from '../../types';
|
||||
import { connectToServer } from './client-helper';
|
||||
import { withRequestMeta, sendStatelessRequest } from './stateless-client';
|
||||
|
||||
const SPEC_REFERENCE = {
|
||||
id: 'SEP-2243-Server-Validation',
|
||||
|
|
@ -264,51 +264,39 @@ export class HttpHeaderValidationScenario implements ClientScenario {
|
|||
|
||||
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
||||
const checks: ConformanceCheck[] = [];
|
||||
let sessionId: string | null = null;
|
||||
|
||||
try {
|
||||
// Establish a session via normal SDK initialization
|
||||
const connection = await connectToServer(serverUrl);
|
||||
const toolsResult = await connection.client.listTools();
|
||||
await connection.close();
|
||||
|
||||
// Get a fresh session for raw requests
|
||||
const initResponse = await sendRawRequest(
|
||||
serverUrl,
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: DRAFT_PROTOCOL_VERSION,
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: 'conformance-test-raw-client',
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
},
|
||||
{ 'Mcp-Method': 'initialize' }
|
||||
);
|
||||
|
||||
if (initResponse.status === 200) {
|
||||
const rawSid = initResponse.headers['mcp-session-id'];
|
||||
sessionId = (Array.isArray(rawSid) ? rawSid[0] : rawSid) || null;
|
||||
const notifHeaders: Record<string, string> = {
|
||||
'Mcp-Method': 'notifications/initialized'
|
||||
};
|
||||
if (sessionId) notifHeaders['mcp-session-id'] = sessionId;
|
||||
await sendRawRequest(
|
||||
serverUrl,
|
||||
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
||||
notifHeaders
|
||||
);
|
||||
// Discover the server's tools with a fully-conformant stateless request
|
||||
// (SEP-2575) — that wire protocol has no initialize handshake or sessions.
|
||||
const toolsResponse = await sendStatelessRequest(serverUrl, 'tools/list');
|
||||
if (!toolsResponse.body?.result) {
|
||||
// The server under test could not even answer a conformant tools/list:
|
||||
// report a single explicit setup failure instead of misleading
|
||||
// per-case results against a broken server.
|
||||
const rpcError = toolsResponse.body?.error;
|
||||
checks.push({
|
||||
id: 'sep-2243-server-standard-setup',
|
||||
name: 'HttpHeaderValidationSetup',
|
||||
description: 'Setup for header validation tests',
|
||||
status: 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage:
|
||||
`tools/list discovery failed: HTTP ${toolsResponse.status}` +
|
||||
(rpcError
|
||||
? `, JSON-RPC error ${rpcError.code}: ${rpcError.message}`
|
||||
: ', no result in response body'),
|
||||
specReferences: [SPEC_REFERENCE],
|
||||
details: { httpStatus: toolsResponse.status, error: rpcError }
|
||||
});
|
||||
return checks;
|
||||
}
|
||||
const toolsResult = toolsResponse.body.result as {
|
||||
tools?: Array<{ name: string; inputSchema?: unknown }>;
|
||||
};
|
||||
|
||||
const baseHeaders: Record<string, string> = {
|
||||
'MCP-Protocol-Version': DRAFT_PROTOCOL_VERSION
|
||||
};
|
||||
if (sessionId) baseHeaders['mcp-session-id'] = sessionId;
|
||||
|
||||
let idCounter = 100;
|
||||
const nextId = () => idCounter++;
|
||||
|
|
@ -501,7 +489,13 @@ export class HttpHeaderValidationScenario implements ClientScenario {
|
|||
details: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const requestBody = { ...body, id: body.id === 0 ? nextId() : body.id };
|
||||
// Issue #311: every raw request carries the SEP-2575 _meta fields — the
|
||||
// header-validation cases only mangle headers, never the body metadata.
|
||||
const requestBody = {
|
||||
...body,
|
||||
id: body.id === 0 ? nextId() : body.id,
|
||||
params: withRequestMeta(body.params)
|
||||
};
|
||||
const response = await sendRawRequest(serverUrl, requestBody, {
|
||||
...baseHeaders,
|
||||
...extraHeaders
|
||||
|
|
@ -565,12 +559,37 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario
|
|||
|
||||
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
||||
const checks: ConformanceCheck[] = [];
|
||||
let sessionId: string | null = null;
|
||||
|
||||
try {
|
||||
const connection = await connectToServer(serverUrl);
|
||||
const toolsResult = await connection.client.listTools();
|
||||
await connection.close();
|
||||
// Discover the server's tools with a fully-conformant stateless request
|
||||
// (SEP-2575) — that wire protocol has no initialize handshake or sessions.
|
||||
const toolsResponse = await sendStatelessRequest(serverUrl, 'tools/list');
|
||||
if (!toolsResponse.body?.result) {
|
||||
// The server under test could not even answer a conformant tools/list:
|
||||
// report a single explicit setup failure (and backfill the declared
|
||||
// checks, mirroring the catch path) instead of pretending the
|
||||
// requirements are not applicable to a broken server.
|
||||
const rpcError = toolsResponse.body?.error;
|
||||
checks.push({
|
||||
id: 'sep-2243-server-custom-setup',
|
||||
name: 'HttpCustomHeaderServerValidationSetup',
|
||||
description: 'Setup for custom header server validation tests',
|
||||
status: 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage:
|
||||
`tools/list discovery failed: HTTP ${toolsResponse.status}` +
|
||||
(rpcError
|
||||
? `, JSON-RPC error ${rpcError.code}: ${rpcError.message}`
|
||||
: ', no result in response body'),
|
||||
specReferences: [SPEC_REFERENCE_CUSTOM],
|
||||
details: { httpStatus: toolsResponse.status, error: rpcError }
|
||||
});
|
||||
this.failDeclaredChecks(checks);
|
||||
return checks;
|
||||
}
|
||||
const toolsResult = toolsResponse.body.result as {
|
||||
tools?: Array<{ name: string; inputSchema?: unknown }>;
|
||||
};
|
||||
|
||||
// Find a tool with x-mcp-header annotations
|
||||
const xMcpTool = toolsResult.tools?.find((tool) => {
|
||||
|
|
@ -602,43 +621,9 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario
|
|||
return checks;
|
||||
}
|
||||
|
||||
// Get a fresh session for raw requests
|
||||
const initResponse = await sendRawRequest(
|
||||
serverUrl,
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: DRAFT_PROTOCOL_VERSION,
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: 'conformance-test-base64-client',
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
},
|
||||
{ 'Mcp-Method': 'initialize' }
|
||||
);
|
||||
|
||||
if (initResponse.status === 200) {
|
||||
const rawSid2 = initResponse.headers['mcp-session-id'];
|
||||
sessionId = (Array.isArray(rawSid2) ? rawSid2[0] : rawSid2) || null;
|
||||
const notifHeaders: Record<string, string> = {
|
||||
'Mcp-Method': 'notifications/initialized'
|
||||
};
|
||||
if (sessionId) notifHeaders['mcp-session-id'] = sessionId;
|
||||
await sendRawRequest(
|
||||
serverUrl,
|
||||
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
||||
notifHeaders
|
||||
);
|
||||
}
|
||||
|
||||
const baseHeaders: Record<string, string> = {
|
||||
'MCP-Protocol-Version': DRAFT_PROTOCOL_VERSION
|
||||
};
|
||||
if (sessionId) baseHeaders['mcp-session-id'] = sessionId;
|
||||
|
||||
// Find the first x-mcp-header annotated STRING property
|
||||
// that is callable with minimal arguments to avoid schema validation failures
|
||||
|
|
@ -828,6 +813,16 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario
|
|||
// Declared-but-unemitted -> FAILURE. Reached only when setup threw partway
|
||||
// through (the gate-out paths emit SKIPPED rows and the happy path emits
|
||||
// every declared ID).
|
||||
this.failDeclaredChecks(checks);
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfill every declared-but-unemitted check ID as FAILURE when setup
|
||||
* failed before the cases could run, keeping the emitted ID set stable.
|
||||
*/
|
||||
private failDeclaredChecks(checks: ConformanceCheck[]): void {
|
||||
for (const id of CUSTOM_HEADER_SERVER_DECLARED_CHECK_IDS) {
|
||||
if (checks.some((c) => c.id === id)) continue;
|
||||
checks.push({
|
||||
|
|
@ -841,8 +836,6 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario
|
|||
specReferences: [SPEC_REFERENCE_CUSTOM]
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -890,10 +883,12 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario
|
|||
jsonrpc: '2.0',
|
||||
id: nextId(),
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
// Issue #311: the body always carries the SEP-2575 _meta fields —
|
||||
// these cases only vary the Mcp-Param header value.
|
||||
params: withRequestMeta({
|
||||
name: toolName,
|
||||
arguments: { ...defaultArgs, [paramName]: bodyValue }
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
...baseHeaders,
|
||||
|
|
@ -974,10 +969,12 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario
|
|||
jsonrpc: '2.0',
|
||||
id: nextId(),
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
// Issue #311: the body always carries the SEP-2575 _meta fields —
|
||||
// this case only omits the Mcp-Param header.
|
||||
params: withRequestMeta({
|
||||
name: toolName,
|
||||
arguments: { ...defaultArgs, [paramName]: 'test-value' }
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
...baseHeaders,
|
||||
|
|
|
|||
Loading…
Reference in a new issue