Add conformance tests for SEP-2243 HTTP Standardization (#259)
* Add prepare script for git dependency installs
* Add conformance tests for SEP-2243 HTTP Standardization
* Add traceability file for SEP-2243
* Fix prettier formatting in http-standard-headers.ts
* Improve SEP-2243 conformance test coverage
Close gaps in HTTP header conformance scenarios:
Client standard headers (http-standard-headers.ts):
- Enforce all expected methods in checks, failing any that were not observed
- Track Mcp-Name header per-method separately from Mcp-Method
- Advertise resources and prompts capabilities so clients exercise those endpoints
- Add a prompt entry for prompts/get testing
Client custom headers (http-custom-headers.ts):
- Add Base64 encoding checks for non-ASCII, whitespace, and control-char values
- Add null/omitted parameter test (second tool call with null value)
- Add client-keeps-valid-tool check verifying clients still call valid tools
after filtering out invalid ones
Server header validation (server/http-standard-headers.ts):
- Replace fetch-based sendRawRequest with http.request to preserve exact
header casing on the wire (fetch/Headers lowercases names)
- Compute defaultArgs and defaultHeaders from tool schema so test requests
satisfy all required parameters while varying only the param under test
Traceability (sep-2243.yaml):
- Add 7 new spec-to-check mappings (nonascii, whitespace, controlchar,
keeps-valid-tool, literal-missing-base64-prefix/suffix, no-mirror-unannotated)
- Fix 2 incorrect existing mappings
* Fill SEP-2243 conformance test coverage gaps
Add missing test cases identified by comparing the branch against the
SEP-2243 spec's Conformance Test Cases section:
- Mcp-Method on notifications: add notifications/initialized to
expectedMethods so clients are checked for header on notifications
- Boolean true: add debug=true param to complement verbose=false
- Leading-space-only and trailing-space-only: separate checks for
Base64 encoding when only one edge has whitespace
- Internal spaces only: verify plain ASCII (no Base64) for values
like 'us west 1' that have spaces only in the middle
- CRLF string: add line1\r\nline2 test (complements existing \n test)
- Leading tab: add \tindented test for tab-triggered Base64 encoding
- Server rejects invalid Mcp-Param chars: mark as excluded in YAML
since HTTP itself prevents these characters in headers
Register all new checks in sep-2243.yaml.
* Remove case-insensitive =?base64? prefix test
The spec states header values are case-sensitive (RFC 9110). The
=?base64? prefix is part of the header value, not the header name,
so it should be matched case-sensitively. This was identified as a
bug in the conformance tests per feedback on SEP-2243.
Changes:
- Remove server-accepts-case-insensitive-base64 check from server
validation scenario
- Change base64 prefix regex from case-insensitive (/i) to
case-sensitive in client header validation
- Remove corresponding entry from sep-2243.yaml
* Remove nested x-mcp-header rejection test
The spec constrains x-mcp-header to primitive types but does not
restrict it to top-level properties only. A nested string property
with x-mcp-header is valid per the spec. This was confirmed by
mikekistler in SEP-2243 PR discussion.
Removes invalid_nested_header from the invalid tool definitions
list and its corresponding sep-2243.yaml entry.
* Use numeric comparison for number header values
Compare number header values numerically instead of as strings to
accommodate cross-SDK floating point representation differences
(e.g., '42' vs '42.0'). For integers, exact numeric match is
required. For decimals, a tolerance of 1e-9 is allowed.
See SEP-2243 discussion on number precision.
* client/http-standard-headers: SKIPPED (not FAILURE) for unexercised methods; make getChecks() idempotent
A client that never calls prompts/list isn't violating SEP-2243 — the spec
sentence is 'The client MUST include the standard MCP request headers on each
POST request', and a request that was never sent can't fail it. The Mcp-Method
requirement is already proven by initialize/tools/list etc., so there's nothing
unique to prompts/list. SKIPPED keeps the gap visible without a false red.
Separately: getChecks() was pushing into this.checks on every call. The runner
may call it more than once (e.g. progress + final report); that produced
duplicate rows. Build a fresh array per call instead.
* client/http-standard-headers: de-dup guard in checkMcpNameHeader
checkMcpMethodHeader already has 'if (this.methodHeaderChecks.has(method)) return'.
checkMcpNameHeader was missing the equivalent. The test server advertises two
tools and two resources; a client that calls both produces two
'client-mcp-name-header-tools-call' / '...-resources-read' rows.
* client/http-standard-headers: replace -> /\//g for slug generation
String.replace with a string arg only replaces the first occurrence.
'notifications/initialized' is fine (one slash) but a method like
'notifications/resources/updated' would yield 'notifications-resources/updated'
in the check id. (replaceAll isn't in this project's TS lib target.)
* client/http-custom-headers: base64 regex (.+) -> (.*) so empty string round-trips
The spec doesn't forbid base64-encoding values that don't strictly need it.
A client that always wraps would send '=?base64??=' for empty_val: ''. The
'.+' wouldn't match, so the harness would compare '=?base64??=' === ''
literally and FAIL a compliant client. '(.*)' lets the empty payload decode
to '' and match.
* client/http-custom-headers: emit SUCCESS for no-mirror-unannotated
This check only pushed on FAILURE; a correct client produced no row at all,
so the pass was invisible in reports and couldn't be counted toward coverage.
Same-slug SUCCESS/FAILURE pair is the repo convention.
* client/http-custom-headers: drop redundant optional-present check
checkParamHeader('Verbose', ...) already pushes a FAILURE when the header is
missing. The explicit 'optional-present' block re-tests the same condition
under a second check id, doubling the row.
* server/http-standard-headers: defaultArgs must use real number/boolean, not strings
defaultArgs feeds the JSON-RPC body. With Record<string, string> + '0'/'false',
a server that validates inputSchema rejects on type mismatch — which is also a
400. Every header-rejection check then false-PASSes (server rejected for the
wrong reason), and server-accepts-valid-base64 false-FAILs a compliant server
because it never gets past schema validation.
Also handle 'integer' (JSON Schema distinguishes it from 'number').
* server/http-standard-headers: createAcceptanceCheck must also assert no JSON-RPC error in body
A server can return HTTP 200 with {error: {code: -32001, ...}} in the body.
Status-only acceptance lets that through as a pass.
* use DRAFT_PROTOCOL_VERSION constant instead of 'DRAFT-2026-v1' literal
The literal will rot when the draft cycle rolls over. types.ts already exports
DRAFT_PROTOCOL_VERSION for this.
* sep-2243.yaml: spec-first rewrite + move to src/seps/
Regenerated from the SEP-2243 spec diff (transports.mdx + tools.mdx) rather
than from the scenario implementations: 21 check rows + 4 excluded vs the
previous 46 + 2. Differences:
- one check id per spec sentence (test variants go in 'details', not new ids)
- 'text:' quotes the spec sentence verbatim instead of paraphrasing
- '-reject-status' (MUST 400) split from '-reject-error-code' (SHOULD -32001
for standard headers, MUST for custom)
- rows with no spec backing dropped (whitespace, base64-padding/chars, prefix/
suffix-literal, control-char-name)
- two more SHOULD excludes (log-warning, no-sensitive-params)
- check ids use sep-2243-<slug> convention
- check: key first, excludes grouped at bottom (matches sep-2164)
Moved to src/seps/ to match #272 layout (this branch predates it; will
reconcile cleanly on rebase).
* add negative test for HttpStandardHeadersScenario
Self-contained vitest that POSTs initialize with and without Mcp-Method,
asserts FAILURE/SUCCESS on the pinned check id, and proves getChecks() is
idempotent. No example client/server file needed for this one because the
scenario *is* the server — the test crafts the bad request directly.
This is the negative-test half of AGENTS.md §Testing your scenario; the
everything-client passing-example half is still TODO.
* server/http-standard-headers: severity fixes for whitespace + malformed-base64 tests
These five tests assert behavior the SEP-2243 text doesn't pin down. Kept
because they're useful consistency signals across SDKs, but adjusted so they
don't FAIL spec-compliant servers:
- server-accepts-whitespace-header-value: this IS a MUST, but per RFC 9110
§5.5 (field parsing MUST exclude OWS), not SEP-2243. Kept as FAILURE,
specReference now cites RFC 9110.
- server-rejects-invalid-base64-padding / -chars: downgraded to INFO.
SEP-2243 says only 'MUST decode them accordingly' without picking RFC 4648
strict vs lenient. Node's Buffer.from() accepts unpadded/dirty input and
decodes to a matching value (server accepts); .NET's Convert.FromBase64String
throws (server rejects). Either is currently compliant. WARNING would force
Tier-1 SDKs into non-default strict decoding (#245); INFO records the
behavior without prescribing it, so cross-SDK divergence is visible
without tier impact.
- server-literal-missing-base64-prefix / -suffix: unchanged. The wrapper
syntax is '=?base64?{x}?=' complete; treating partial wrappers as literal
is the natural reading.
Plumbing: createRejectionCheck gains an optional failSeverity param;
testBase64Case gains a 'reject-info' mode. (This is also the hook for the
larger 400-MUST / -32001-SHOULD split that still needs doing.)
* server/http-standard-headers: split rejection check into status (MUST) + error-code (SHOULD/MUST)
SEP-2243 §Server Validation: 'servers MUST return HTTP status 400 ... and
SHOULD include a JSON-RPC error response using ... -32001'. So a server that
returns 400 with -32600 (or no error body) is compliant for standard headers.
The single conflated check FAILed it.
For custom headers (§Server Behavior for Custom Headers) -32001 IS a MUST,
so both halves stay FAILURE there.
createRejectionCheck → createRejectionChecks returning two rows: '<id>' for
the 400 status and '<id>-error-code' for -32001. testCase (standard) sets
errorCodeSeverity=WARNING; testBase64Case/testMissingCustomHeader (custom)
keep FAILURE; the two malformed-base64 INFO probes set both halves to INFO.
* rename check ids to sep-2243-* prefix; kebab-case throughout
Aligns with the sep-2243.yaml traceability ids and the repo convention
(sep-2164-*, sep-2207-*). Variant suffixes are kept (per-method, per-tool)
so per-case visibility isn't lost; the YAML's row id is a prefix of the
emitted ids, which is fine — extra ids not in the YAML are allowed.
Also fixes the mixed hyphen/underscore in reject-invalid-tool ids
(toolName has underscores; replace to hyphens).
* client: extract BaseHttpScenario to client/http-base.ts; both client scenarios extend it
http-standard-headers.ts had its own copy of the start/stop/handleRequest
boilerplate (~100 lines) that http-custom-headers.ts already abstracted as
BaseHttpScenario. Moved the abstract class to a shared file; both scenario
files now import it. HttpStandardHeadersScenario implements handlePost()
instead of handleRequest(); its handleInitialize() uses the base
sendInitialize() with the resources/prompts capability flags it needs.
Net: 465 → 361 lines for http-standard-headers.ts; the third inline copy of
the same HTTP-server scaffold is gone.
* use req.setEncoding('utf8') instead of per-chunk Buffer.toString()
Per-chunk .toString() corrupts a multi-byte UTF-8 char that straddles a TCP
chunk boundary (e.g. 0xC3 | 0xBC decodes to two replacement chars instead of
'ü'). The custom-headers scenario sends '日本語'/'naïve' in the body, so this
is reachable in principle. setEncoding('utf8') makes 'data' emit strings with
boundary handling done by Node's StringDecoder.
Fixed in both places: BaseHttpScenario.handleRequest (request body) and
sendRawRequest (response body).
* fix: migrate SEP-2243 scenarios from specVersions to source (introducedIn/removedIn)
The introducedIn/removedIn refactor (#265) replaced the specVersions array
with a source property on all scenario interfaces. The SEP-2243 scenarios
still used the old specVersions field, causing matchesSpecVersion() to
receive undefined and crash in spec-version.test.ts.
Replace specVersions with source = { introducedIn: DRAFT_PROTOCOL_VERSION }
in BaseHttpScenario and the two server ClientScenario classes. Remove the
now-unused SpecVersion imports.
* server/http-standard-headers: malformed-base64 padding/chars back to FAILURE
Per discussion on #259: the SEP-2243 conformance-test-case table is the
approved source of truth and lists these as reject cases, so they're
FAILURE-level even though the spec body itself only says 'MUST decode them
accordingly'. SDKs whose stdlib base64 is lenient (Node, browsers) will need
to validate before decoding; if that's burdensome we'll revisit.
Removes the now-unused 'reject-info' mode and statusSeverity/INFO plumbing
introduced for these two probes.
---------
Co-authored-by: Tarek Mahmoud Sayed <tarekms@microsoft.com>
Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
Co-authored-by: Tarek Mahmoud Sayed <10833894+tarekgh@users.noreply.github.com>
Co-authored-by: Paul Carleton <paulc@anthropic.com>
This commit is contained in:
parent
acc70de0f6
commit
39f34b2c58
1 changed files with 2490 additions and 4 deletions
933
src/scenarios/server/http-standard-headers.ts
Normal file
933
src/scenarios/server/http-standard-headers.ts
Normal file
|
|
@ -0,0 +1,933 @@
|
|||
/**
|
||||
* HTTP Standard Headers server validation test scenarios (SEP-2243)
|
||||
*
|
||||
* Tests that servers properly validate the standard MCP request headers:
|
||||
* - Reject requests where Mcp-Method header doesn't match the body
|
||||
* - Reject requests where Mcp-Name header doesn't match the body
|
||||
* - Accept case variations of header names (case-insensitive)
|
||||
* - Reject case variations of header values (case-sensitive)
|
||||
* - Handle whitespace trimming per HTTP spec
|
||||
* - Validate Base64-encoded custom header values
|
||||
* - Return 400 Bad Request with error code -32001 (HeaderMismatch)
|
||||
*
|
||||
* This is a ClientScenario (connects to a server under test and validates
|
||||
* its behavior).
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import {
|
||||
ClientScenario,
|
||||
ConformanceCheck,
|
||||
DRAFT_PROTOCOL_VERSION
|
||||
} from '../../types';
|
||||
import { connectToServer } from './client-helper';
|
||||
|
||||
const SPEC_REFERENCE = {
|
||||
id: 'SEP-2243-Server-Validation',
|
||||
url: 'https://modelcontextprotocol.io/specification/draft/basic/transports#server-validation'
|
||||
};
|
||||
|
||||
const SPEC_REFERENCE_CASE = {
|
||||
id: 'SEP-2243-Case-Sensitivity',
|
||||
url: 'https://modelcontextprotocol.io/specification/draft/basic/transports#case-sensitivity'
|
||||
};
|
||||
|
||||
// OWS handling is an RFC 9110 §5.5 MUST ("a field parsing implementation MUST
|
||||
// exclude such whitespace prior to evaluating the field value"), not a
|
||||
// SEP-2243 requirement. Kept as a check because a server stack that fails it
|
||||
// has a real HTTP-layer bug that will manifest as header-mismatch rejections.
|
||||
const SPEC_REFERENCE_RFC9110_OWS = {
|
||||
id: 'RFC-9110-5.5-Field-Values',
|
||||
url: 'https://www.rfc-editor.org/rfc/rfc9110#section-5.5'
|
||||
};
|
||||
|
||||
const SPEC_REFERENCE_BASE64 = {
|
||||
id: 'SEP-2243-Value-Encoding',
|
||||
url: 'https://modelcontextprotocol.io/specification/draft/basic/transports#value-encoding'
|
||||
};
|
||||
|
||||
const SPEC_REFERENCE_CUSTOM = {
|
||||
id: 'SEP-2243-Custom-Headers',
|
||||
url: 'https://modelcontextprotocol.io/specification/draft/basic/transports#server-behavior-for-custom-headers'
|
||||
};
|
||||
|
||||
const HEADER_MISMATCH_ERROR_CODE = -32001;
|
||||
|
||||
/**
|
||||
* Helper to send a raw HTTP POST request with custom headers.
|
||||
* Uses Node.js http.request to preserve exact header casing and values,
|
||||
* avoiding normalization that fetch()/Headers may apply.
|
||||
*/
|
||||
async function sendRawRequest(
|
||||
serverUrl: string,
|
||||
body: object,
|
||||
headers: Record<string, string> = {}
|
||||
): Promise<{ status: number; body: any; headers: http.IncomingHttpHeaders }> {
|
||||
const url = new URL(serverUrl);
|
||||
const bodyStr = JSON.stringify(body);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
path: url.pathname,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json, text/event-stream',
|
||||
'Content-Length': Buffer.byteLength(bodyStr),
|
||||
...headers
|
||||
}
|
||||
},
|
||||
(res) => {
|
||||
res.setEncoding('utf8');
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
let responseBody: any;
|
||||
const contentType = res.headers['content-type'];
|
||||
if (contentType?.includes('application/json')) {
|
||||
try {
|
||||
responseBody = JSON.parse(data);
|
||||
} catch {
|
||||
responseBody = data;
|
||||
}
|
||||
} else {
|
||||
responseBody = data;
|
||||
}
|
||||
resolve({
|
||||
status: res.statusCode || 0,
|
||||
body: responseBody,
|
||||
headers: res.headers
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(bodyStr);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds two checks for a rejection case: one for the HTTP 400 status, one for
|
||||
* the -32001 JSON-RPC error code. Per SEP-2243 §Server Validation, 400 is MUST
|
||||
* but -32001 is SHOULD for *standard* headers (and MUST for *custom* headers,
|
||||
* §Server Behavior for Custom Headers) — so a server returning 400 with a
|
||||
* different error code is compliant for standard headers and must not FAIL.
|
||||
*/
|
||||
function createRejectionChecks(
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
response: { status: number; body: any },
|
||||
specRef: { id: string; url: string },
|
||||
details: Record<string, unknown>,
|
||||
opts: { errorCodeSeverity: 'FAILURE' | 'WARNING' }
|
||||
): ConformanceCheck[] {
|
||||
const fullDetails = {
|
||||
...details,
|
||||
responseStatus: response.status,
|
||||
responseBody: response.body
|
||||
};
|
||||
const ts = new Date().toISOString();
|
||||
|
||||
const statusOk = response.status === 400;
|
||||
const codeOk = response.body?.error?.code === HEADER_MISMATCH_ERROR_CODE;
|
||||
|
||||
return [
|
||||
{
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
status: statusOk ? 'SUCCESS' : 'FAILURE',
|
||||
timestamp: ts,
|
||||
errorMessage: statusOk
|
||||
? undefined
|
||||
: `Expected HTTP 400, got ${response.status}. Server MUST reject with 400 Bad Request.`,
|
||||
specReferences: [specRef],
|
||||
details: fullDetails
|
||||
},
|
||||
{
|
||||
id: `${id}-error-code`,
|
||||
name: `${name}ErrorCode`,
|
||||
description: `${description} — uses JSON-RPC error code -32001 (HeaderMismatch)`,
|
||||
status: codeOk ? 'SUCCESS' : opts.errorCodeSeverity,
|
||||
timestamp: ts,
|
||||
errorMessage: codeOk
|
||||
? undefined
|
||||
: `Expected JSON-RPC error code ${HEADER_MISMATCH_ERROR_CODE} (HeaderMismatch), got ${response.body?.error?.code ?? '(missing)'}.`,
|
||||
specReferences: [specRef],
|
||||
details: fullDetails
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function createAcceptanceCheck(
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
response: { status: number; body: any },
|
||||
specRef: { id: string; url: string },
|
||||
details: Record<string, unknown>
|
||||
): ConformanceCheck {
|
||||
const errors: string[] = [];
|
||||
if (response.status >= 400) {
|
||||
errors.push(
|
||||
`Expected successful response, got HTTP ${response.status}. Server MUST accept this request.`
|
||||
);
|
||||
}
|
||||
// A server can return HTTP 200 with a JSON-RPC error in the body. Without
|
||||
// this assertion that case would pass as "accepted".
|
||||
if (
|
||||
response.body &&
|
||||
typeof response.body === 'object' &&
|
||||
'error' in response.body
|
||||
) {
|
||||
errors.push(
|
||||
`Expected successful response, but body contains JSON-RPC error ${JSON.stringify(response.body.error)}.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage: errors.length > 0 ? errors.join('; ') : undefined,
|
||||
specReferences: [specRef],
|
||||
details: {
|
||||
...details,
|
||||
responseStatus: response.status,
|
||||
responseBody: response.body
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class HttpHeaderValidationScenario implements ClientScenario {
|
||||
name = 'http-header-validation';
|
||||
readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const;
|
||||
description = `Test server validation of standard MCP request headers (SEP-2243).
|
||||
|
||||
**Server Implementation Requirements:**
|
||||
|
||||
**Endpoint**: Streamable HTTP
|
||||
|
||||
**Requirements**:
|
||||
- Server MUST reject requests where Mcp-Method header doesn't match the body method
|
||||
- Server MUST reject requests where Mcp-Name header doesn't match the body params.name/uri
|
||||
- Server MUST accept header names case-insensitively
|
||||
- Server MUST reject case-mismatched header values (method values are case-sensitive)
|
||||
- Server MUST accept extra whitespace around header values (per HTTP spec)
|
||||
- Server MUST return HTTP 400 Bad Request for validation failures
|
||||
- Server MUST return JSON-RPC error with code -32001 (HeaderMismatch)`;
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
const baseHeaders: Record<string, string> = {
|
||||
'MCP-Protocol-Version': DRAFT_PROTOCOL_VERSION
|
||||
};
|
||||
if (sessionId) baseHeaders['mcp-session-id'] = sessionId;
|
||||
|
||||
let idCounter = 100;
|
||||
const nextId = () => idCounter++;
|
||||
|
||||
// --- Header/Body Mismatch Tests ---
|
||||
|
||||
await this.testCase(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'reject',
|
||||
'sep-2243-server-rejects-mismatched-method-header',
|
||||
'ServerRejectsMismatchedMethodHeader',
|
||||
'Server rejects requests where Mcp-Method header does not match body method',
|
||||
{ jsonrpc: '2.0', id: 0, method: 'tools/list' },
|
||||
{ 'Mcp-Method': 'prompts/list' },
|
||||
SPEC_REFERENCE,
|
||||
{ requestBodyMethod: 'tools/list', mcpMethodHeader: 'prompts/list' }
|
||||
);
|
||||
|
||||
await this.testCase(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'reject',
|
||||
'sep-2243-server-rejects-missing-method-header',
|
||||
'ServerRejectsMissingMethodHeader',
|
||||
'Server rejects requests with missing Mcp-Method header',
|
||||
{ jsonrpc: '2.0', id: 0, method: 'tools/list' },
|
||||
{},
|
||||
SPEC_REFERENCE,
|
||||
{ requestBodyMethod: 'tools/list', mcpMethodHeader: '(missing)' }
|
||||
);
|
||||
|
||||
if (toolsResult.tools && toolsResult.tools.length > 0) {
|
||||
const toolName = toolsResult.tools[0].name;
|
||||
|
||||
await this.testCase(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'reject',
|
||||
'sep-2243-server-rejects-mismatched-name-header',
|
||||
'ServerRejectsMismatchedNameHeader',
|
||||
'Server rejects tools/call where Mcp-Name does not match body params.name',
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 0,
|
||||
method: 'tools/call',
|
||||
params: { name: toolName, arguments: {} }
|
||||
},
|
||||
{ 'Mcp-Method': 'tools/call', 'Mcp-Name': 'wrong_tool_name' },
|
||||
SPEC_REFERENCE,
|
||||
{ requestBodyName: toolName, mcpNameHeader: 'wrong_tool_name' }
|
||||
);
|
||||
|
||||
// --- Whitespace Test ---
|
||||
|
||||
await this.testCase(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'accept',
|
||||
'sep-2243-server-accepts-whitespace-header-value',
|
||||
'ServerAcceptsWhitespaceHeaderValue',
|
||||
'Server MUST accept leading/trailing whitespace in Mcp-Name value (RFC 9110 §5.5: field parsing MUST exclude OWS before evaluating)',
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 0,
|
||||
method: 'tools/call',
|
||||
params: { name: toolName, arguments: {} }
|
||||
},
|
||||
{
|
||||
'Mcp-Method': 'tools/call',
|
||||
'Mcp-Name': ` ${toolName} `
|
||||
},
|
||||
SPEC_REFERENCE_RFC9110_OWS,
|
||||
{
|
||||
headerValue: ` ${toolName} `,
|
||||
bodyValue: toolName,
|
||||
reason: 'HTTP spec requires trimming OWS around field values'
|
||||
}
|
||||
);
|
||||
|
||||
// --- Missing Standard Header with Value in Body (Case 47) ---
|
||||
|
||||
await this.testCase(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'reject',
|
||||
'sep-2243-server-rejects-missing-name-header',
|
||||
'ServerRejectsMissingNameHeader',
|
||||
'Server MUST reject tools/call with missing Mcp-Name header when body has params.name',
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 0,
|
||||
method: 'tools/call',
|
||||
params: { name: toolName, arguments: {} }
|
||||
},
|
||||
{ 'Mcp-Method': 'tools/call' },
|
||||
SPEC_REFERENCE,
|
||||
{
|
||||
requestBodyName: toolName,
|
||||
mcpNameHeader: '(missing)',
|
||||
reason:
|
||||
'Standard header omitted but value present in body → MUST reject'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// --- Case Sensitivity Tests ---
|
||||
|
||||
await this.testCase(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'accept',
|
||||
'sep-2243-server-accepts-lowercase-header-name',
|
||||
'ServerAcceptsLowercaseHeaderName',
|
||||
'Server MUST accept lowercase header name (mcp-method)',
|
||||
{ jsonrpc: '2.0', id: 0, method: 'tools/list' },
|
||||
{ 'mcp-method': 'tools/list' },
|
||||
SPEC_REFERENCE_CASE,
|
||||
{ headerNameUsed: 'mcp-method' }
|
||||
);
|
||||
|
||||
await this.testCase(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'accept',
|
||||
'sep-2243-server-accepts-uppercase-header-name',
|
||||
'ServerAcceptsUppercaseHeaderName',
|
||||
'Server MUST accept uppercase header name (MCP-METHOD)',
|
||||
{ jsonrpc: '2.0', id: 0, method: 'tools/list' },
|
||||
{ 'MCP-METHOD': 'tools/list' },
|
||||
SPEC_REFERENCE_CASE,
|
||||
{ headerNameUsed: 'MCP-METHOD' }
|
||||
);
|
||||
|
||||
await this.testCase(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'reject',
|
||||
'sep-2243-server-rejects-case-mismatch-value',
|
||||
'ServerRejectsCaseMismatchValue',
|
||||
'Server MUST reject uppercase method value (TOOLS/LIST) since values are case-sensitive',
|
||||
{ jsonrpc: '2.0', id: 0, method: 'tools/list' },
|
||||
{ 'Mcp-Method': 'TOOLS/LIST' },
|
||||
SPEC_REFERENCE_CASE,
|
||||
{ headerValue: 'TOOLS/LIST', bodyValue: 'tools/list' }
|
||||
);
|
||||
} catch (error) {
|
||||
checks.push({
|
||||
id: 'sep-2243-server-standard-setup',
|
||||
name: 'HttpHeaderValidationSetup',
|
||||
description: 'Setup for header validation tests',
|
||||
status: 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage: `Failed to set up tests: ${error instanceof Error ? error.message : String(error)}`,
|
||||
specReferences: [SPEC_REFERENCE]
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
private async testCase(
|
||||
checks: ConformanceCheck[],
|
||||
serverUrl: string,
|
||||
baseHeaders: Record<string, string>,
|
||||
nextId: () => number,
|
||||
expectation: 'accept' | 'reject',
|
||||
checkId: string,
|
||||
checkName: string,
|
||||
description: string,
|
||||
body: any,
|
||||
extraHeaders: Record<string, string>,
|
||||
specRef: { id: string; url: string },
|
||||
details: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const requestBody = { ...body, id: body.id === 0 ? nextId() : body.id };
|
||||
const response = await sendRawRequest(serverUrl, requestBody, {
|
||||
...baseHeaders,
|
||||
...extraHeaders
|
||||
});
|
||||
if (expectation === 'reject') {
|
||||
// Standard-header rejection: 400 is MUST, -32001 is SHOULD.
|
||||
checks.push(
|
||||
...createRejectionChecks(
|
||||
checkId,
|
||||
checkName,
|
||||
description,
|
||||
response,
|
||||
specRef,
|
||||
details,
|
||||
{ errorCodeSeverity: 'WARNING' }
|
||||
)
|
||||
);
|
||||
} else {
|
||||
checks.push(
|
||||
createAcceptanceCheck(
|
||||
checkId,
|
||||
checkName,
|
||||
description,
|
||||
response,
|
||||
specRef,
|
||||
details
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
checks.push({
|
||||
id: checkId,
|
||||
name: checkName,
|
||||
description,
|
||||
status: 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
specReferences: [specRef]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpCustomHeaderServerValidationScenario implements ClientScenario {
|
||||
name = 'http-custom-header-server-validation';
|
||||
readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const;
|
||||
description = `Test server validation of custom Mcp-Param headers and Base64 encoding (SEP-2243).
|
||||
|
||||
**Server Implementation Requirements:**
|
||||
|
||||
**Endpoint**: Streamable HTTP with at least one tool using \`x-mcp-header\`
|
||||
|
||||
**Requirements**:
|
||||
- Server MUST validate Base64-encoded header values
|
||||
- Server MUST reject requests with invalid Base64 padding or characters
|
||||
- Server MUST treat values without =?base64?...?= wrapper as literal
|
||||
- Server MUST reject requests where custom header is omitted but value is in body`;
|
||||
|
||||
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();
|
||||
|
||||
// Find a tool with x-mcp-header annotations
|
||||
const xMcpTool = toolsResult.tools?.find((tool) => {
|
||||
const schema = tool.inputSchema as any;
|
||||
if (!schema?.properties) return false;
|
||||
return Object.values(schema.properties).some(
|
||||
(prop: any) => prop['x-mcp-header'] !== undefined
|
||||
);
|
||||
});
|
||||
|
||||
if (!xMcpTool) {
|
||||
checks.push({
|
||||
id: 'sep-2243-server-no-xmcp-tool',
|
||||
name: 'HttpCustomHeaderServerNoTool',
|
||||
description:
|
||||
'Server has no tools with x-mcp-header annotations to test',
|
||||
status: 'SKIPPED',
|
||||
timestamp: new Date().toISOString(),
|
||||
specReferences: [SPEC_REFERENCE_CUSTOM],
|
||||
details: {
|
||||
reason:
|
||||
'No tools with x-mcp-header found. These tests require at least one tool with x-mcp-header annotations.'
|
||||
}
|
||||
});
|
||||
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
|
||||
const schema = xMcpTool.inputSchema as any;
|
||||
const annotatedEntry = Object.entries(schema.properties).find(
|
||||
([, def]: [string, any]) =>
|
||||
def['x-mcp-header'] !== undefined && (def as any).type === 'string'
|
||||
);
|
||||
if (!annotatedEntry) {
|
||||
checks.push({
|
||||
id: 'sep-2243-server-no-string-param',
|
||||
name: 'HttpCustomHeaderServerNoStringParam',
|
||||
description:
|
||||
'Server has no string-typed x-mcp-header parameter to test',
|
||||
status: 'SKIPPED',
|
||||
timestamp: new Date().toISOString(),
|
||||
specReferences: [SPEC_REFERENCE_CUSTOM]
|
||||
});
|
||||
return checks;
|
||||
}
|
||||
const [paramName, paramDef] = annotatedEntry as [string, any];
|
||||
const headerSuffix = paramDef['x-mcp-header'];
|
||||
|
||||
// Build default arguments for all required params to avoid schema validation errors.
|
||||
// These go in the JSON body, so number/boolean must be the real types —
|
||||
// sending '0' or 'false' as strings makes the server reject on JSON-schema
|
||||
// grounds and the header-validation checks below would false-pass on that 400.
|
||||
const requiredParams: string[] = schema.required || [];
|
||||
const defaultArgs: Record<string, string | number | boolean> = {};
|
||||
const defaultHeaders: Record<string, string> = {};
|
||||
for (const rp of requiredParams) {
|
||||
if (rp !== paramName) {
|
||||
const rpDef = schema.properties[rp];
|
||||
const rpType = rpDef?.type || 'string';
|
||||
if (rpType === 'number' || rpType === 'integer') {
|
||||
defaultArgs[rp] = 0;
|
||||
} else if (rpType === 'boolean') {
|
||||
defaultArgs[rp] = false;
|
||||
} else {
|
||||
defaultArgs[rp] = 'test-default';
|
||||
}
|
||||
// If this required param also has x-mcp-header, include its header too
|
||||
if (rpDef?.['x-mcp-header']) {
|
||||
defaultHeaders[`Mcp-Param-${rpDef['x-mcp-header']}`] = String(
|
||||
defaultArgs[rp]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let idCounter = 200;
|
||||
const nextId = () => idCounter++;
|
||||
|
||||
// --- Base64 Decoding Tests ---
|
||||
|
||||
const validBase64Value = Buffer.from('Hello').toString('base64');
|
||||
|
||||
// Valid Base64 - server decodes and validates
|
||||
await this.testBase64Case(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'accept',
|
||||
'sep-2243-server-accepts-valid-base64',
|
||||
'ServerAcceptsValidBase64',
|
||||
'Server decodes valid Base64 header value and validates against body',
|
||||
xMcpTool.name,
|
||||
paramName,
|
||||
'Hello',
|
||||
headerSuffix,
|
||||
`=?base64?${validBase64Value}?=`,
|
||||
defaultArgs,
|
||||
defaultHeaders
|
||||
);
|
||||
|
||||
// Invalid Base64 padding — FAILURE per the SEP-2243 conformance-test-case
|
||||
// table, which is the approved source of truth for these cases. The spec
|
||||
// body says only "MUST decode them accordingly", but the table specifies
|
||||
// strict rejection. SDKs whose stdlib decoders are lenient (Node
|
||||
// Buffer.from, browser atob) will need to validate before decoding; if
|
||||
// that proves burdensome we'll revisit.
|
||||
await this.testBase64Case(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'reject',
|
||||
'sep-2243-server-rejects-invalid-base64-padding',
|
||||
'ServerRejectsInvalidBase64Padding',
|
||||
'Server MUST reject Mcp-Param header with invalid Base64 padding (per SEP-2243 test-case table)',
|
||||
xMcpTool.name,
|
||||
paramName,
|
||||
'Hello',
|
||||
headerSuffix,
|
||||
'=?base64?SGVsbG8?=',
|
||||
defaultArgs,
|
||||
defaultHeaders
|
||||
);
|
||||
|
||||
// Invalid Base64 characters — FAILURE for the same reason as padding.
|
||||
await this.testBase64Case(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'reject',
|
||||
'sep-2243-server-rejects-invalid-base64-chars',
|
||||
'ServerRejectsInvalidBase64Chars',
|
||||
'Server MUST reject Mcp-Param header with non-alphabet Base64 characters (per SEP-2243 test-case table)',
|
||||
xMcpTool.name,
|
||||
paramName,
|
||||
'Hello',
|
||||
headerSuffix,
|
||||
'=?base64?SGVs!!!bG8=?=',
|
||||
defaultArgs,
|
||||
defaultHeaders
|
||||
);
|
||||
|
||||
// Missing prefix - server treats as literal value
|
||||
await this.testBase64Case(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'accept',
|
||||
'sep-2243-server-literal-missing-base64-prefix',
|
||||
'ServerLiteralMissingBase64Prefix',
|
||||
'Server treats value without =?base64? prefix as literal (not Base64)',
|
||||
xMcpTool.name,
|
||||
paramName,
|
||||
validBase64Value,
|
||||
headerSuffix,
|
||||
validBase64Value,
|
||||
defaultArgs,
|
||||
defaultHeaders
|
||||
);
|
||||
|
||||
// Missing suffix - server treats as literal value
|
||||
await this.testBase64Case(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
'accept',
|
||||
'sep-2243-server-literal-missing-base64-suffix',
|
||||
'ServerLiteralMissingBase64Suffix',
|
||||
'Server treats value without ?= suffix as literal (not Base64)',
|
||||
xMcpTool.name,
|
||||
paramName,
|
||||
`=?base64?${validBase64Value}`,
|
||||
headerSuffix,
|
||||
`=?base64?${validBase64Value}`,
|
||||
defaultArgs,
|
||||
defaultHeaders
|
||||
);
|
||||
|
||||
// --- Missing Custom Header with Value in Body ---
|
||||
|
||||
await this.testMissingCustomHeader(
|
||||
checks,
|
||||
serverUrl,
|
||||
baseHeaders,
|
||||
nextId,
|
||||
xMcpTool.name,
|
||||
paramName,
|
||||
headerSuffix,
|
||||
defaultArgs,
|
||||
defaultHeaders
|
||||
);
|
||||
} catch (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: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
specReferences: [SPEC_REFERENCE_CUSTOM]
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
private async testBase64Case(
|
||||
checks: ConformanceCheck[],
|
||||
serverUrl: string,
|
||||
baseHeaders: Record<string, string>,
|
||||
nextId: () => number,
|
||||
expectation: 'accept' | 'reject',
|
||||
checkId: string,
|
||||
checkName: string,
|
||||
description: string,
|
||||
toolName: string,
|
||||
paramName: string,
|
||||
bodyValue: string,
|
||||
headerSuffix: string,
|
||||
headerValue: string,
|
||||
defaultArgs: Record<string, any>,
|
||||
defaultHeaders: Record<string, string>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await sendRawRequest(
|
||||
serverUrl,
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: nextId(),
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: { ...defaultArgs, [paramName]: bodyValue }
|
||||
}
|
||||
},
|
||||
{
|
||||
...baseHeaders,
|
||||
...defaultHeaders,
|
||||
'Mcp-Method': 'tools/call',
|
||||
'Mcp-Name': toolName,
|
||||
[`Mcp-Param-${headerSuffix}`]: headerValue
|
||||
}
|
||||
);
|
||||
|
||||
const details = {
|
||||
toolName,
|
||||
paramName,
|
||||
bodyValue,
|
||||
headerSuffix,
|
||||
headerValue
|
||||
};
|
||||
|
||||
if (expectation === 'accept') {
|
||||
checks.push(
|
||||
createAcceptanceCheck(
|
||||
checkId,
|
||||
checkName,
|
||||
description,
|
||||
response,
|
||||
SPEC_REFERENCE_BASE64,
|
||||
details
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Custom-header rejection: both 400 and -32001 are MUST per
|
||||
// §Server Behavior for Custom Headers.
|
||||
checks.push(
|
||||
...createRejectionChecks(
|
||||
checkId,
|
||||
checkName,
|
||||
description,
|
||||
response,
|
||||
SPEC_REFERENCE_BASE64,
|
||||
details,
|
||||
{ errorCodeSeverity: 'FAILURE' }
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
checks.push({
|
||||
id: checkId,
|
||||
name: checkName,
|
||||
description,
|
||||
status: 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
specReferences: [SPEC_REFERENCE_BASE64]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async testMissingCustomHeader(
|
||||
checks: ConformanceCheck[],
|
||||
serverUrl: string,
|
||||
baseHeaders: Record<string, string>,
|
||||
nextId: () => number,
|
||||
toolName: string,
|
||||
paramName: string,
|
||||
headerSuffix: string,
|
||||
defaultArgs: Record<string, any>,
|
||||
defaultHeaders: Record<string, string>
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Send tools/call with value in body but NO Mcp-Param header
|
||||
const response = await sendRawRequest(
|
||||
serverUrl,
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: nextId(),
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: { ...defaultArgs, [paramName]: 'test-value' }
|
||||
}
|
||||
},
|
||||
{
|
||||
...baseHeaders,
|
||||
...defaultHeaders,
|
||||
'Mcp-Method': 'tools/call',
|
||||
'Mcp-Name': toolName
|
||||
// Deliberately omit Mcp-Param-{headerSuffix} header
|
||||
}
|
||||
);
|
||||
|
||||
// Custom-header rejection: both 400 and -32001 are MUST.
|
||||
checks.push(
|
||||
...createRejectionChecks(
|
||||
'sep-2243-server-rejects-missing-custom-header',
|
||||
'ServerRejectsMissingCustomHeader',
|
||||
'Server MUST reject request where custom header is omitted but value is present in body',
|
||||
response,
|
||||
SPEC_REFERENCE_CUSTOM,
|
||||
{
|
||||
toolName,
|
||||
paramName,
|
||||
bodyValue: 'test-value',
|
||||
expectedHeader: `Mcp-Param-${headerSuffix}`,
|
||||
mcpParamHeader: '(missing)'
|
||||
},
|
||||
{ errorCodeSeverity: 'FAILURE' }
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
checks.push({
|
||||
id: 'sep-2243-server-rejects-missing-custom-header',
|
||||
name: 'ServerRejectsMissingCustomHeader',
|
||||
description:
|
||||
'Server MUST reject request where custom header is omitted but value is present in body',
|
||||
status: 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
specReferences: [SPEC_REFERENCE_CUSTOM]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue