fix(sep-2243): reject x-mcp-header on number-typed params (#444)

SEP-2243 permits `x-mcp-header` only on integer, string and boolean
parameters — "Parameters with type `number` are not permitted" — but the
harness never verified the `number` case and its requirement text still
described `number` as permitted.

PR #371 fixed the positive `http-custom-headers` fixture (priority is now
`integer`, `float_val` is served unannotated) but left the two remaining
items from #344:

- `HttpInvalidToolHeadersScenario` serves invalid tools for every other
  x-mcp-header constraint but had no number-typed case, so nothing checked
  that a client excludes such a tool.
- `sep-2243.yaml` claimed primitive types are "(number, string, or
  boolean)" and that "number values" get decimal-string encoding, both of
  which contradict the released 2026-07-28 spec.

Add an `invalid_number_header` tool mapped to the existing
`sep-2243-x-mcp-header-primitive-only` requirement (no new check IDs) and
correct both requirement texts. traceability.json is updated in place
since it embeds the yaml text verbatim; check IDs and statuses are
unchanged.

Verified against typescript-sdk@main: the new check fails there because
the SDK deliberately allow-lists `number` to satisfy the old fixture — a
workaround its own source notes as "tracked upstream" and can now drop.
The positive scenario still passes 18/18.

Closes #344

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Stephen Halter 2026-09-06 13:23:38 -07:00 • committed by GitHub
commit 3507f94150
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 72 additions and 4 deletions

View file

@ -30,6 +30,18 @@ async function post(
}); });
} }
async function postJson(serverUrl: string, body: object): Promise<any> {
const res = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream'
},
body: JSON.stringify(body)
});
return res.json();
}
function idsOf(checks: { id: string }[]): Set<string> { function idsOf(checks: { id: string }[]): Set<string> {
return new Set(checks.map((c) => c.id)); return new Set(checks.map((c) => c.id));
} }
@ -198,4 +210,42 @@ describe('HttpInvalidToolHeadersScenario (SEP-2243) check IDs', () => {
await scenario.stop(); await scenario.stop();
} }
}); });
it('FAILs primitive-only when the client calls the number-typed tool', async () => {
const scenario = new HttpInvalidToolHeadersScenario();
const { serverUrl } = await scenario.start(testScenarioContext());
try {
const listed = await postJson(serverUrl, {
jsonrpc: '2.0',
id: 1,
method: 'tools/list'
});
// SEP-2243 permits x-mcp-header only on integer/string/boolean, so the
// number-typed tool must be served for the client to reject it.
const numberTool = listed.result.tools.find(
(t: { name: string }) => t.name === 'invalid_number_header'
);
expect(numberTool?.inputSchema.properties.score).toEqual({
type: 'number',
'x-mcp-header': 'Score'
});
await post(serverUrl, {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { name: 'invalid_number_header', arguments: { score: 1.5 } }
});
const checks = scenario.getChecks();
expect(
statusesFor(checks, 'sep-2243-x-mcp-header-primitive-only')
).toContain('FAILURE');
// The other constraints were not violated.
expect(
statusesFor(checks, 'sep-2243-x-mcp-header-not-empty')
).not.toContain('FAILURE');
} finally {
await scenario.stop();
}
});
}); });

View file

@ -68,6 +68,7 @@ const INVALID_TOOL_CONSTRAINT_IDS: Record<string, string> = {
invalid_object_header: 'sep-2243-x-mcp-header-primitive-only', invalid_object_header: 'sep-2243-x-mcp-header-primitive-only',
invalid_array_header: 'sep-2243-x-mcp-header-primitive-only', invalid_array_header: 'sep-2243-x-mcp-header-primitive-only',
invalid_null_header: 'sep-2243-x-mcp-header-primitive-only', invalid_null_header: 'sep-2243-x-mcp-header-primitive-only',
invalid_number_header: 'sep-2243-x-mcp-header-primitive-only',
invalid_duplicate_same_case: 'sep-2243-x-mcp-header-unique', invalid_duplicate_same_case: 'sep-2243-x-mcp-header-unique',
invalid_duplicate_diff_case: 'sep-2243-x-mcp-header-unique', invalid_duplicate_diff_case: 'sep-2243-x-mcp-header-unique',
invalid_space_in_name: 'sep-2243-x-mcp-header-charset', invalid_space_in_name: 'sep-2243-x-mcp-header-charset',
@ -861,6 +862,23 @@ export class HttpInvalidToolHeadersScenario extends BaseHttpScenario {
} }
}, },
// ── Invalid: x-mcp-header on number type ──
// `number` is a JSON Schema primitive but SEP-2243 excludes it from
// the permitted set: "Parameters with type `number` are not
// permitted." Only integer, string and boolean may be annotated.
{
name: 'invalid_number_header',
description:
'x-mcp-header MUST NOT be on number type (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
score: { type: 'number', 'x-mcp-header': 'Score' }
},
required: ['score']
}
},
// ── Invalid: duplicate same-case x-mcp-header values ── // ── Invalid: duplicate same-case x-mcp-header values ──
{ {
name: 'invalid_duplicate_same_case', name: 'invalid_duplicate_same_case',

View file

@ -23,13 +23,13 @@ requirements:
text: 'The x-mcp-header value MUST be case-insensitively unique within a single tool definition.' text: 'The x-mcp-header value MUST be case-insensitively unique within a single tool definition.'
url: https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers url: https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers
- check: sep-2243-x-mcp-header-primitive-only - check: sep-2243-x-mcp-header-primitive-only
text: 'x-mcp-header MUST only be applied to parameters with primitive types (number, string, or boolean).' text: 'x-mcp-header MUST only be applied to parameters with primitive types (integer, string, boolean). Parameters with type `number` are not permitted.'
url: https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers url: https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers
- check: sep-2243-client-reject-invalid-tool - check: sep-2243-client-reject-invalid-tool
text: 'Clients MUST reject tool definitions where any x-mcp-header value violates these constraints. Rejection means the client MUST exclude the invalid tool from the set of tools returned by tools/list.' text: 'Clients MUST reject tool definitions where any x-mcp-header value violates these constraints. Rejection means the client MUST exclude the invalid tool from the set of tools returned by tools/list.'
url: https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers url: https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers
- check: sep-2243-client-encode-values - check: sep-2243-client-encode-values
text: 'Clients MUST encode parameter values before including them in HTTP headers: number values MUST be converted to their decimal string representation; boolean values MUST be converted to the lowercase strings "true" or "false".' text: 'Clients MUST encode parameter values before including them in HTTP headers: integer values MUST be converted to their decimal string representation; boolean values MUST be converted to the lowercase strings "true" or "false".'
- check: sep-2243-client-base64-unsafe - check: sep-2243-client-base64-unsafe
text: 'When a value cannot be safely represented as plain ASCII (e.g., contains non-ASCII characters, control characters, or leading/trailing whitespace), clients MUST use Base64 encoding of the UTF-8 representation, wrapped as =?base64?{encoded}?=.' text: 'When a value cannot be safely represented as plain ASCII (e.g., contains non-ASCII characters, control characters, or leading/trailing whitespace), clients MUST use Base64 encoding of the UTF-8 representation, wrapped as =?base64?{encoded}?=.'
- check: sep-2243-server-decode-base64 - check: sep-2243-server-decode-base64

View file

@ -209,7 +209,7 @@
{ {
"check": "sep-2243-x-mcp-header-primitive-only", "check": "sep-2243-x-mcp-header-primitive-only",
"status": "tested", "status": "tested",
"text": "x-mcp-header MUST only be applied to parameters with primitive types (number, string, or boolean).", "text": "x-mcp-header MUST only be applied to parameters with primitive types (integer, string, boolean). Parameters with type `number` are not permitted.",
"url": "https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers" "url": "https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers"
}, },
{ {
@ -221,7 +221,7 @@
{ {
"check": "sep-2243-client-encode-values", "check": "sep-2243-client-encode-values",
"status": "tested", "status": "tested",
"text": "Clients MUST encode parameter values before including them in HTTP headers: number values MUST be converted to their decimal string representation; boolean values MUST be converted to the lowercase strings \"true\" or \"false\"." "text": "Clients MUST encode parameter values before including them in HTTP headers: integer values MUST be converted to their decimal string representation; boolean values MUST be converted to the lowercase strings \"true\" or \"false\"."
}, },
{ {
"check": "sep-2243-client-base64-unsafe", "check": "sep-2243-client-base64-unsafe",