fix(http-custom-headers): use spec-permitted x-mcp-header types (#371)

* fix(http-custom-headers): use spec-permitted x-mcp-header types

The http-custom-headers fixture annotated three `number`-typed properties
(priority and float_val) with x-mcp-header. The SEP-2243 spec forbids this:
"MUST only be applied to parameters with primitive types (integer, string,
boolean). Parameters with type `number` are not permitted."

A spec-conformant client MUST reject (exclude from tools/list) any tool whose
x-mcp-header annotations violate the constraints, so it would drop both test
tools and never call them, failing ClientSupportsCustomHeaders through no fault
of its own.

- priority: number -> integer (its value, 42/1, and header are unchanged).
- float_val: drop the x-mcp-header annotation; a floating-point header param is
  exactly what the spec prohibits and has no valid replacement. It stays in the
  body and is now asserted NOT to be mirrored, like the unannotated query param.
- Treat 'integer' like 'number' in header-value validation and check-id
  classification so Priority keeps its cross-SDK numeric tolerance.

* Reword float_val check to the rule actually under test

The new ClientCustomHeaderNoMirrorNumber check asserts absence of a header
for an *unannotated* parameter — the same designated-params-only rule the
query check covers. The previous description stated a 'MUST NOT mirror
number-typed params' requirement that SEP-2243 does not contain (the spec
forbids the annotation on number, with rejection as the consequence; it does
not define a client-side type filter). Reword the description and comment to
the rule actually under test, keeping the number/SEP-2243 context as the
explanatory why. Also trim the float_val schema description to match the
sibling query param style.

---------

Co-authored-by: Paul Carleton <paulc@anthropic.com>
This commit is contained in:
Marcelo Trylesinski 2026-06-26 16:34:38 +02:00 • committed by GitHub
commit b18aa91850
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -127,7 +127,7 @@ function validateEncodedHeader(
return `Value '${bodyValue}' requires Base64 encoding but header was sent as plain: '${rawHeader}'`;
}
const decoded = Buffer.from(base64Match[1], 'base64').toString('utf-8');
if (valueType === 'number') {
if (valueType === 'number' || valueType === 'integer') {
return compareNumericValues(decoded, bodyValue);
}
if (decoded !== bodyValue) {
@ -137,7 +137,7 @@ function validateEncodedHeader(
}
// Plain ASCII - compare directly (after decoding if Base64 was used)
const decoded = decodeHeaderValue(rawHeader);
if (valueType === 'number') {
if (valueType === 'number' || valueType === 'integer') {
return compareNumericValues(decoded, bodyValue);
}
if (decoded !== bodyValue) {
@ -293,7 +293,7 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
'x-mcp-header': 'Region'
},
priority: {
type: 'number',
type: 'integer',
description: 'Integer numeric value',
'x-mcp-header': 'Priority'
},
@ -320,8 +320,8 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
},
float_val: {
type: 'number',
description: 'Floating point numeric value',
'x-mcp-header': 'FloatVal'
description:
'Floating point value — no x-mcp-header annotation, should not be mirrored'
},
non_ascii_val: {
type: 'string',
@ -393,7 +393,7 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
'x-mcp-header': 'Region'
},
priority: {
type: 'number',
type: 'integer',
description: 'Integer numeric value',
'x-mcp-header': 'Priority'
},
@ -449,8 +449,8 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
// Check Mcp-Param-Region header (plain ASCII string)
this.checkParamHeader(req, 'Region', args.region, 'string');
// Check Mcp-Param-Priority header (integer number)
this.checkParamHeader(req, 'Priority', args.priority, 'number');
// Check Mcp-Param-Priority header (integer)
this.checkParamHeader(req, 'Priority', args.priority, 'integer');
// Check Mcp-Param-Verbose header (boolean value)
// checkParamHeader already FAILs on missing header, so this also covers
@ -475,10 +475,25 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
this.checkParamHeader(req, 'Method', args.method_val, 'string');
}
// Check Mcp-Param-FloatVal header (floating point number)
if (args.float_val !== undefined && args.float_val !== null) {
this.checkParamHeader(req, 'FloatVal', args.float_val, 'number');
}
// float_val is intentionally unannotated: SEP-2243 forbids x-mcp-header on
// `number`-typed properties, so it is served without one. Assert no header
// was sent — same "designated params only" rule as the `query` check below.
const floatHeader = req.headers['mcp-param-floatval'] as
| string
| undefined;
this.checks.push({
id: 'sep-2243-client-mirrors-designated-params',
name: 'ClientCustomHeaderNoMirrorNumber',
description:
'Client MUST NOT add Mcp-Param headers for parameters without x-mcp-header (number-typed float_val is served unannotated per SEP-2243)',
status: floatHeader === undefined ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage:
floatHeader !== undefined
? `Found unexpected Mcp-Param-FloatVal header '${floatHeader}' for an unannotated number parameter`
: undefined,
specReferences: [SPEC_REFERENCE_CUSTOM]
});
// Check Mcp-Param-NonAscii header (requires Base64 encoding)
if (args.non_ascii_val !== undefined && args.non_ascii_val !== null) {
@ -652,7 +667,9 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
typeof bodyValue === 'string' && needsBase64Encoding(String(bodyValue));
const checkId = needsBase64
? 'sep-2243-client-base64-unsafe'
: valueType === 'number' || valueType === 'boolean'
: valueType === 'number' ||
valueType === 'integer' ||
valueType === 'boolean'
? 'sep-2243-client-encode-values'
: 'sep-2243-client-mirrors-designated-params';