From e0e7b4bcbe03e459ec97998477441deb405543b6 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Sun, 6 Sep 2026 19:03:13 +0000 Subject: [PATCH] fix(core): reject x-mcp-header on number-typed parameters The 2026-07-28 Streamable HTTP spec permits x-mcp-header only on integer, string and boolean parameters and requires clients to exclude tools that violate this. 'number' was allow-listed solely to pass an older conformance fixture that used number-typed header parameters; that fixture was corrected (modelcontextprotocol/conformance#371) and the conformance suite is about to add a negative case for it (modelcontextprotocol/conformance#444). Co-Authored-By: Claude --- .changeset/x-mcp-header-reject-number.md | 7 +++++++ .../core-internal/src/shared/mcpParamHeaders.ts | 15 ++++++--------- .../test/shared/mcpParamHeaders.test.ts | 4 ++++ 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 .changeset/x-mcp-header-reject-number.md diff --git a/.changeset/x-mcp-header-reject-number.md b/.changeset/x-mcp-header-reject-number.md new file mode 100644 index 0000000000..99cb02e83c --- /dev/null +++ b/.changeset/x-mcp-header-reject-number.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/core-internal': patch +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/server': patch +--- + +`x-mcp-header` on a `number`-typed tool parameter is now rejected, matching the 2026-07-28 spec ("Parameters with type `number` are not permitted"; clients MUST exclude such tools from `tools/list`). Previously `number` was accepted only to satisfy an older conformance fixture that has since been corrected. `integer`, `string` and `boolean` are unaffected. diff --git a/packages/core-internal/src/shared/mcpParamHeaders.ts b/packages/core-internal/src/shared/mcpParamHeaders.ts index 493cf50aeb..5a49a92dd3 100644 --- a/packages/core-internal/src/shared/mcpParamHeaders.ts +++ b/packages/core-internal/src/shared/mcpParamHeaders.ts @@ -58,16 +58,13 @@ export type XMcpHeaderScanResult = { valid: true; declarations: readonly XMcpHea const RFC9110_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; /** - * JSON Schema `type` values the spec admits on an `x-mcp-header` property. - * - * The spec text names `integer`, `string`, `boolean` and explicitly excludes - * `number`. The published conformance referee at the pinned release ships its - * `http-custom-headers` scenario with two `type: "number"` `x-mcp-header` - * parameters and expects the client to mirror them, so `number` is accepted - * here so that the conformance gate passes; the discrepancy is tracked - * upstream. Everything else (`object`, `array`, `null`, absent) is rejected. + * JSON Schema `type` values the spec admits on an `x-mcp-header` property: + * `integer`, `string` and `boolean`. `number` is explicitly not permitted + * (2026-07-28 Streamable HTTP, "Schema Extension"), so a tool that annotates a + * `number`-typed property is rejected like any other invalid declaration. + * Everything else (`object`, `array`, `null`, absent) is rejected too. */ -const PERMITTED_X_MCP_HEADER_TYPES: ReadonlySet = new Set(['string', 'integer', 'boolean', 'number']); +const PERMITTED_X_MCP_HEADER_TYPES: ReadonlySet = new Set(['string', 'integer', 'boolean']); /** * Scan a tool's JSON-serialized `inputSchema` for `x-mcp-header` declarations diff --git a/packages/core-internal/test/shared/mcpParamHeaders.test.ts b/packages/core-internal/test/shared/mcpParamHeaders.test.ts index 13d8ea582f..fedad8d36f 100644 --- a/packages/core-internal/test/shared/mcpParamHeaders.test.ts +++ b/packages/core-internal/test/shared/mcpParamHeaders.test.ts @@ -144,6 +144,10 @@ describe('scanXMcpHeaderDeclarations — constraint table', () => { expect(invalid({ type: 'object', properties: { a: { type: 'array', [X_MCP_HEADER_KEY]: 'Items' } } })).toMatch(/primitive/); }); + test('number-typed property is rejected (only integer, string, boolean are permitted)', () => { + expect(invalid({ type: 'object', properties: { a: { type: 'number', [X_MCP_HEADER_KEY]: 'Score' } } })).toMatch(/primitive/); + }); + test('null-typed property is rejected', () => { expect(invalid({ type: 'object', properties: { a: { type: 'null', [X_MCP_HEADER_KEY]: 'Nil' } } })).toMatch(/primitive/); });