fix(sep-2243): collapse check IDs to one-per-requirement (#287)
The sep-2243 scenario check IDs drifted from the requirement-traceability yaml in #259 — the code emitted one ID per test case while the yaml declares one per normative requirement, so the IDs no longer matched. Rename the emitted IDs so a check ID maps to a single MUST/SHOULD and is emitted once per case (the per-case detail moves to name/description), matching the repo's existing 'same id, vary status/message' convention: - mcp-method-header-* / mcp-name-header-* -> client-includes-standard-headers - reject-invalid-tool-* / keep-valid-tool -> client-reject-invalid-tool - server-accepts-{lower,upper}case-name -> header-name-case-insensitive - server reject status checks (mismatch/missing/case x method/name) -> server-reject-invalid-headers - their error-code variants -> server-reject-error-code Merge the yaml's server-reject-mismatch + server-reject-status into one server-reject-invalid-headers MUST (HTTP 400 on a header-validation failure); keep server-reject-error-code as the SHOULD. Base64/custom-header rejection checks keep their own param-validation IDs (out of scope here). Gates and the whitespace-acceptance check keep their scenario-matching names. No behavior change — only check IDs, descriptions, and the yaml.
This commit is contained in:
parent
5c1b815cd1
commit
70f7ba0004
5 changed files with 48 additions and 23 deletions
|
|
@ -626,7 +626,7 @@ export class HttpInvalidToolHeadersScenario extends BaseHttpScenario {
|
||||||
// Check that valid_tool WAS called — proves client kept valid tools
|
// Check that valid_tool WAS called — proves client kept valid tools
|
||||||
const validToolCalled = this.calledTools.has('valid_tool');
|
const validToolCalled = this.calledTools.has('valid_tool');
|
||||||
this.checks.push({
|
this.checks.push({
|
||||||
id: 'sep-2243-keep-valid-tool',
|
id: 'sep-2243-client-reject-invalid-tool',
|
||||||
name: 'ClientKeepsValidTool',
|
name: 'ClientKeepsValidTool',
|
||||||
description: 'Client MUST keep valid tools while excluding invalid ones',
|
description: 'Client MUST keep valid tools while excluding invalid ones',
|
||||||
status: validToolCalled ? 'SUCCESS' : 'FAILURE',
|
status: validToolCalled ? 'SUCCESS' : 'FAILURE',
|
||||||
|
|
@ -654,7 +654,7 @@ export class HttpInvalidToolHeadersScenario extends BaseHttpScenario {
|
||||||
for (const toolName of invalidTools) {
|
for (const toolName of invalidTools) {
|
||||||
const called = this.calledTools.has(toolName);
|
const called = this.calledTools.has(toolName);
|
||||||
this.checks.push({
|
this.checks.push({
|
||||||
id: `sep-2243-reject-invalid-tool-${toolName.replace(/_/g, '-')}`,
|
id: 'sep-2243-client-reject-invalid-tool',
|
||||||
name: `ClientRejectsInvalidTool_${toolName}`,
|
name: `ClientRejectsInvalidTool_${toolName}`,
|
||||||
description: `Client MUST NOT call tool '${toolName}' with invalid x-mcp-header`,
|
description: `Client MUST NOT call tool '${toolName}' with invalid x-mcp-header`,
|
||||||
status: called ? 'FAILURE' : 'SUCCESS',
|
status: called ? 'FAILURE' : 'SUCCESS',
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,19 @@ describe('HttpStandardHeadersScenario (SEP-2243) — negative', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it('FAILs sep-2243-mcp-method-header-initialize when Mcp-Method is missing', async () => {
|
// The coarse check id is emitted once per method/name case, so we narrow to
|
||||||
|
// the initialize Mcp-Method emission via its (case-specific) name.
|
||||||
|
const COARSE_ID = 'sep-2243-client-includes-standard-headers';
|
||||||
|
const INIT_METHOD_NAME = 'ClientMcpMethodHeader_initialize';
|
||||||
|
|
||||||
|
it('FAILs the initialize Mcp-Method emission when Mcp-Method is missing', async () => {
|
||||||
const scenario = new HttpStandardHeadersScenario();
|
const scenario = new HttpStandardHeadersScenario();
|
||||||
const { serverUrl } = await scenario.start();
|
const { serverUrl } = await scenario.start();
|
||||||
try {
|
try {
|
||||||
await postInitialize(serverUrl, {}); // no Mcp-Method header
|
await postInitialize(serverUrl, {}); // no Mcp-Method header
|
||||||
const checks = scenario.getChecks();
|
const checks = scenario.getChecks();
|
||||||
const check = checks.find(
|
const check = checks.find(
|
||||||
(c) => c.id === 'sep-2243-mcp-method-header-initialize'
|
(c) => c.id === COARSE_ID && c.name === INIT_METHOD_NAME
|
||||||
);
|
);
|
||||||
expect(check?.status).toBe('FAILURE');
|
expect(check?.status).toBe('FAILURE');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -46,14 +51,14 @@ describe('HttpStandardHeadersScenario (SEP-2243) — negative', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('SUCCEEDs sep-2243-mcp-method-header-initialize when Mcp-Method matches', async () => {
|
it('SUCCEEDs the initialize Mcp-Method emission when Mcp-Method matches', async () => {
|
||||||
const scenario = new HttpStandardHeadersScenario();
|
const scenario = new HttpStandardHeadersScenario();
|
||||||
const { serverUrl } = await scenario.start();
|
const { serverUrl } = await scenario.start();
|
||||||
try {
|
try {
|
||||||
await postInitialize(serverUrl, { 'Mcp-Method': 'initialize' });
|
await postInitialize(serverUrl, { 'Mcp-Method': 'initialize' });
|
||||||
const checks = scenario.getChecks();
|
const checks = scenario.getChecks();
|
||||||
const check = checks.find(
|
const check = checks.find(
|
||||||
(c) => c.id === 'sep-2243-mcp-method-header-initialize'
|
(c) => c.id === COARSE_ID && c.name === INIT_METHOD_NAME
|
||||||
);
|
);
|
||||||
expect(check?.status).toBe('SUCCESS');
|
expect(check?.status).toBe('SUCCESS');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
|
||||||
for (const method of expectedMethods) {
|
for (const method of expectedMethods) {
|
||||||
if (!this.methodHeaderChecks.has(method)) {
|
if (!this.methodHeaderChecks.has(method)) {
|
||||||
result.push({
|
result.push({
|
||||||
id: `sep-2243-mcp-method-header-${method.replace(/\//g, '-')}`,
|
id: 'sep-2243-client-includes-standard-headers',
|
||||||
name: `ClientMcpMethodHeader_${method.replace(/\//g, '_')}`,
|
name: `ClientMcpMethodHeader_${method.replace(/\//g, '_')}`,
|
||||||
description: `Client sends correct Mcp-Method header on ${method} request`,
|
description: `Client sends correct Mcp-Method header on ${method} request`,
|
||||||
status: 'SKIPPED',
|
status: 'SKIPPED',
|
||||||
|
|
@ -68,7 +68,7 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
|
||||||
for (const method of expectedNameMethods) {
|
for (const method of expectedNameMethods) {
|
||||||
if (!this.nameHeaderChecks.has(method)) {
|
if (!this.nameHeaderChecks.has(method)) {
|
||||||
result.push({
|
result.push({
|
||||||
id: `sep-2243-mcp-name-header-${method.replace(/\//g, '-')}`,
|
id: 'sep-2243-client-includes-standard-headers',
|
||||||
name: `ClientMcpNameHeader_${method.replace(/\//g, '_')}`,
|
name: `ClientMcpNameHeader_${method.replace(/\//g, '_')}`,
|
||||||
description: `Client sends correct Mcp-Name header on ${method} request`,
|
description: `Client sends correct Mcp-Name header on ${method} request`,
|
||||||
status: 'SKIPPED',
|
status: 'SKIPPED',
|
||||||
|
|
@ -141,7 +141,7 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
|
||||||
this.methodHeaderChecks.set(method, errors.length === 0);
|
this.methodHeaderChecks.set(method, errors.length === 0);
|
||||||
|
|
||||||
this.checks.push({
|
this.checks.push({
|
||||||
id: `sep-2243-mcp-method-header-${method.replace(/\//g, '-')}`,
|
id: 'sep-2243-client-includes-standard-headers',
|
||||||
name: `ClientMcpMethodHeader_${method.replace(/\//g, '_')}`,
|
name: `ClientMcpMethodHeader_${method.replace(/\//g, '_')}`,
|
||||||
description: `Client sends correct Mcp-Method header on ${method} request`,
|
description: `Client sends correct Mcp-Method header on ${method} request`,
|
||||||
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
||||||
|
|
@ -186,7 +186,7 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
|
||||||
this.nameHeaderChecks.set(method, errors.length === 0);
|
this.nameHeaderChecks.set(method, errors.length === 0);
|
||||||
|
|
||||||
this.checks.push({
|
this.checks.push({
|
||||||
id: `sep-2243-mcp-name-header-${method.replace(/\//g, '-')}`,
|
id: 'sep-2243-client-includes-standard-headers',
|
||||||
name: `ClientMcpNameHeader_${method.replace(/\//g, '_')}`,
|
name: `ClientMcpNameHeader_${method.replace(/\//g, '_')}`,
|
||||||
description: `Client sends correct Mcp-Name header on ${method} request`,
|
description: `Client sends correct Mcp-Name header on ${method} request`,
|
||||||
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,14 @@ const SPEC_REFERENCE_CUSTOM = {
|
||||||
|
|
||||||
const HEADER_MISMATCH_ERROR_CODE = -32001;
|
const HEADER_MISMATCH_ERROR_CODE = -32001;
|
||||||
|
|
||||||
|
// Coarse, requirement-level check IDs (SEP-2243) for STANDARD-header
|
||||||
|
// rejections. Every standard-header rejection case emits this same pair of IDs;
|
||||||
|
// the per-case name/description carry the detail of which case was exercised,
|
||||||
|
// matching the repo's "same id, vary status/message" convention. (Custom-header
|
||||||
|
// /Base64 rejections map to different requirements and keep their own IDs.)
|
||||||
|
const REJECT_STATUS_CHECK_ID = 'sep-2243-server-reject-invalid-headers';
|
||||||
|
const REJECT_ERROR_CODE_CHECK_ID = 'sep-2243-server-reject-error-code';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to send a raw HTTP POST request with custom headers.
|
* Helper to send a raw HTTP POST request with custom headers.
|
||||||
* Uses Node.js http.request to preserve exact header casing and values,
|
* Uses Node.js http.request to preserve exact header casing and values,
|
||||||
|
|
@ -119,9 +127,16 @@ async function sendRawRequest(
|
||||||
* but -32001 is SHOULD for *standard* headers (and MUST for *custom* headers,
|
* but -32001 is SHOULD for *standard* headers (and MUST for *custom* headers,
|
||||||
* §Server Behavior for Custom Headers) — so a server returning 400 with a
|
* §Server Behavior for Custom Headers) — so a server returning 400 with a
|
||||||
* different error code is compliant for standard headers and must not FAIL.
|
* different error code is compliant for standard headers and must not FAIL.
|
||||||
|
*
|
||||||
|
* The two emitted check IDs are supplied explicitly by the caller: standard-
|
||||||
|
* header callers pass the coarse REJECT_STATUS_CHECK_ID/REJECT_ERROR_CODE_CHECK_ID
|
||||||
|
* so all standard-header cases collapse onto one requirement, while custom-header
|
||||||
|
* /Base64 callers pass their own per-case ids. The per-case `name`/`description`
|
||||||
|
* distinguish which rejection case was exercised.
|
||||||
*/
|
*/
|
||||||
function createRejectionChecks(
|
function createRejectionChecks(
|
||||||
id: string,
|
statusId: string,
|
||||||
|
errorCodeId: string,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
response: { status: number; body: any },
|
response: { status: number; body: any },
|
||||||
|
|
@ -141,7 +156,7 @@ function createRejectionChecks(
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id,
|
id: statusId,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status: statusOk ? 'SUCCESS' : 'FAILURE',
|
status: statusOk ? 'SUCCESS' : 'FAILURE',
|
||||||
|
|
@ -153,7 +168,7 @@ function createRejectionChecks(
|
||||||
details: fullDetails
|
details: fullDetails
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `${id}-error-code`,
|
id: errorCodeId,
|
||||||
name: `${name}ErrorCode`,
|
name: `${name}ErrorCode`,
|
||||||
description: `${description} — uses JSON-RPC error code -32001 (HeaderMismatch)`,
|
description: `${description} — uses JSON-RPC error code -32001 (HeaderMismatch)`,
|
||||||
status: codeOk ? 'SUCCESS' : opts.errorCodeSeverity,
|
status: codeOk ? 'SUCCESS' : opts.errorCodeSeverity,
|
||||||
|
|
@ -397,7 +412,7 @@ export class HttpHeaderValidationScenario implements ClientScenario {
|
||||||
baseHeaders,
|
baseHeaders,
|
||||||
nextId,
|
nextId,
|
||||||
'accept',
|
'accept',
|
||||||
'sep-2243-server-accepts-lowercase-header-name',
|
'sep-2243-header-name-case-insensitive',
|
||||||
'ServerAcceptsLowercaseHeaderName',
|
'ServerAcceptsLowercaseHeaderName',
|
||||||
'Server MUST accept lowercase header name (mcp-method)',
|
'Server MUST accept lowercase header name (mcp-method)',
|
||||||
{ jsonrpc: '2.0', id: 0, method: 'tools/list' },
|
{ jsonrpc: '2.0', id: 0, method: 'tools/list' },
|
||||||
|
|
@ -412,7 +427,7 @@ export class HttpHeaderValidationScenario implements ClientScenario {
|
||||||
baseHeaders,
|
baseHeaders,
|
||||||
nextId,
|
nextId,
|
||||||
'accept',
|
'accept',
|
||||||
'sep-2243-server-accepts-uppercase-header-name',
|
'sep-2243-header-name-case-insensitive',
|
||||||
'ServerAcceptsUppercaseHeaderName',
|
'ServerAcceptsUppercaseHeaderName',
|
||||||
'Server MUST accept uppercase header name (MCP-METHOD)',
|
'Server MUST accept uppercase header name (MCP-METHOD)',
|
||||||
{ jsonrpc: '2.0', id: 0, method: 'tools/list' },
|
{ jsonrpc: '2.0', id: 0, method: 'tools/list' },
|
||||||
|
|
@ -471,10 +486,13 @@ export class HttpHeaderValidationScenario implements ClientScenario {
|
||||||
...extraHeaders
|
...extraHeaders
|
||||||
});
|
});
|
||||||
if (expectation === 'reject') {
|
if (expectation === 'reject') {
|
||||||
// Standard-header rejection: 400 is MUST, -32001 is SHOULD.
|
// Standard-header rejection: 400 is MUST, -32001 is SHOULD. All
|
||||||
|
// standard-header rejection cases collapse onto the coarse requirement
|
||||||
|
// ids; checkId/checkName still distinguish the case via name/details.
|
||||||
checks.push(
|
checks.push(
|
||||||
...createRejectionChecks(
|
...createRejectionChecks(
|
||||||
checkId,
|
REJECT_STATUS_CHECK_ID,
|
||||||
|
REJECT_ERROR_CODE_CHECK_ID,
|
||||||
checkName,
|
checkName,
|
||||||
description,
|
description,
|
||||||
response,
|
response,
|
||||||
|
|
@ -840,10 +858,12 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Custom-header rejection: both 400 and -32001 are MUST per
|
// Custom-header rejection: both 400 and -32001 are MUST per
|
||||||
// §Server Behavior for Custom Headers.
|
// §Server Behavior for Custom Headers. These map to param-validation
|
||||||
|
// requirements, so they keep their per-case ids (status + -error-code).
|
||||||
checks.push(
|
checks.push(
|
||||||
...createRejectionChecks(
|
...createRejectionChecks(
|
||||||
checkId,
|
checkId,
|
||||||
|
`${checkId}-error-code`,
|
||||||
checkName,
|
checkName,
|
||||||
description,
|
description,
|
||||||
response,
|
response,
|
||||||
|
|
@ -899,10 +919,12 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Custom-header rejection: both 400 and -32001 are MUST.
|
// Custom-header rejection: both 400 and -32001 are MUST. Keeps its own
|
||||||
|
// per-case ids (status + -error-code).
|
||||||
checks.push(
|
checks.push(
|
||||||
...createRejectionChecks(
|
...createRejectionChecks(
|
||||||
'sep-2243-server-rejects-missing-custom-header',
|
'sep-2243-server-rejects-missing-custom-header',
|
||||||
|
'sep-2243-server-rejects-missing-custom-header-error-code',
|
||||||
'ServerRejectsMissingCustomHeader',
|
'ServerRejectsMissingCustomHeader',
|
||||||
'Server MUST reject request where custom header is omitted but value is present in body',
|
'Server MUST reject request where custom header is omitted but value is present in body',
|
||||||
response,
|
response,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@ requirements:
|
||||||
text: 'The client MUST include the standard MCP request headers on each POST request. These headers are REQUIRED for compliance.'
|
text: 'The client MUST include the standard MCP request headers on each POST request. These headers are REQUIRED for compliance.'
|
||||||
- check: sep-2243-header-name-case-insensitive
|
- check: sep-2243-header-name-case-insensitive
|
||||||
text: 'Clients and servers MUST use case-insensitive comparisons for header names.'
|
text: 'Clients and servers MUST use case-insensitive comparisons for header names.'
|
||||||
- check: sep-2243-server-reject-mismatch
|
- check: sep-2243-server-reject-invalid-headers
|
||||||
text: 'Servers that process the request body MUST reject requests where the values specified in the headers do not match the corresponding values in the request body.'
|
text: 'Servers that process the request body MUST reject requests with mismatched or missing standard-header values, returning HTTP 400 Bad Request.'
|
||||||
- check: sep-2243-server-reject-status
|
|
||||||
text: 'When rejecting a request due to header validation failure, servers MUST return HTTP status 400 Bad Request.'
|
|
||||||
- check: sep-2243-server-reject-error-code
|
- check: sep-2243-server-reject-error-code
|
||||||
text: 'When rejecting a request due to header validation failure, servers SHOULD include a JSON-RPC error response using error code -32001.'
|
text: 'When rejecting a request due to header validation failure, servers SHOULD include a JSON-RPC error response using error code -32001.'
|
||||||
- check: sep-2243-client-supports-custom-headers
|
- check: sep-2243-client-supports-custom-headers
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue