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:
parent
542381b7dc
commit
3507f94150
4 changed files with 72 additions and 4 deletions
|
|
@ -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> {
|
||||
return new Set(checks.map((c) => c.id));
|
||||
}
|
||||
|
|
@ -198,4 +210,42 @@ describe('HttpInvalidToolHeadersScenario (SEP-2243) check IDs', () => {
|
|||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ const INVALID_TOOL_CONSTRAINT_IDS: Record<string, string> = {
|
|||
invalid_object_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_number_header: 'sep-2243-x-mcp-header-primitive-only',
|
||||
invalid_duplicate_same_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',
|
||||
|
|
@ -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 ──
|
||||
{
|
||||
name: 'invalid_duplicate_same_case',
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ requirements:
|
|||
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
|
||||
- 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
|
||||
- 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.'
|
||||
url: https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers
|
||||
- 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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@
|
|||
{
|
||||
"check": "sep-2243-x-mcp-header-primitive-only",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
|
|
@ -221,7 +221,7 @@
|
|||
{
|
||||
"check": "sep-2243-client-encode-values",
|
||||
"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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue