diff --git a/src/scenarios/client/http-custom-headers.test.ts b/src/scenarios/client/http-custom-headers.test.ts index 8a68f22a..56bf8c8a 100644 --- a/src/scenarios/client/http-custom-headers.test.ts +++ b/src/scenarios/client/http-custom-headers.test.ts @@ -30,6 +30,18 @@ async function post( }); } +async function postJson(serverUrl: string, body: object): Promise { + 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 { 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(); + } + }); }); diff --git a/src/scenarios/client/http-custom-headers.ts b/src/scenarios/client/http-custom-headers.ts index 8e64e6ab..4a73df37 100644 --- a/src/scenarios/client/http-custom-headers.ts +++ b/src/scenarios/client/http-custom-headers.ts @@ -68,6 +68,7 @@ const INVALID_TOOL_CONSTRAINT_IDS: Record = { 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', diff --git a/src/seps/sep-2243.yaml b/src/seps/sep-2243.yaml index a39b6d2c..ddb06ffb 100644 --- a/src/seps/sep-2243.yaml +++ b/src/seps/sep-2243.yaml @@ -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 diff --git a/src/seps/traceability.json b/src/seps/traceability.json index bdf6c628..674f2e9c 100644 --- a/src/seps/traceability.json +++ b/src/seps/traceability.json @@ -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",