From cf3e28a63bd0bd1f7d41b5ecf3f12eb47cb06ced Mon Sep 17 00:00:00 2001 From: Yudistira Putra <85178972+Yudis-bit@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:22:12 +0700 Subject: [PATCH 1/2] feat(client): validate safe integer range for x-mcp-header parameters (#445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the Streamable HTTP specification (SEP-2243) and server/tools.mdx: 'Integer values MUST be within the safe range for integers represented using IEEE754 double-precision floating point numbers (−2^53+1 to 2^53−1)' Previously, the conformance harness had no requirement row in sep-2243.yaml, no emitted check ID, and no verification that clients refrain from mirroring out-of-range integer values into Mcp-Param headers. 1. Add sep-2243-x-mcp-header-integer-safe-range to src/seps/sep-2243.yaml and CUSTOM_HEADERS_DECLARED_CHECK_IDS in http-custom-headers.ts. 2. Add an annotated integer parameter unsafe_integer_val to test_custom_headers with context argument 9007199254740992 (2^53). 3. In HttpCustomHeadersScenario.handleToolsCall, verify that the client does not mirror unsafe integer arguments into Mcp-Param-UnsafeInteger. 4. Add positive and negative unit test assertions in http-custom-headers.test.ts. 5. Update traceability.json for SEP-2243. Closes #445 --- .../client/http-custom-headers.test.ts | 76 +++++++++++++++++++ src/scenarios/client/http-custom-headers.ts | 41 +++++++++- src/seps/sep-2243.yaml | 3 + src/seps/traceability.json | 8 +- 4 files changed, 126 insertions(+), 2 deletions(-) diff --git a/src/scenarios/client/http-custom-headers.test.ts b/src/scenarios/client/http-custom-headers.test.ts index 28341918..cc1a013b 100644 --- a/src/scenarios/client/http-custom-headers.test.ts +++ b/src/scenarios/client/http-custom-headers.test.ts @@ -86,6 +86,7 @@ describe('HttpCustomHeadersScenario (SEP-2243) check IDs', () => { arguments: { region: 'us-west1', priority: 42, + unsafe_integer_val: 9007199254740992, non_ascii_val: nonAscii, query: 'SELECT 1' } @@ -188,6 +189,7 @@ describe('HttpCustomHeadersScenario (SEP-2243) check IDs', () => { arguments: { region: 'us-west1', priority: 42, + unsafe_integer_val: 9007199254740992, non_ascii_val: nonAscii, query: 'SELECT 1' } @@ -240,6 +242,80 @@ describe('HttpCustomHeadersScenario (SEP-2243) check IDs', () => { await scenario.stop(); } }); + + it('FAILs safe-integer-range when a client mirrors an out-of-range integer header', async () => { + const scenario = new HttpCustomHeadersScenario(); + const { serverUrl } = await scenario.start(testScenarioContext()); + try { + await post( + serverUrl, + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'test_custom_headers', + arguments: { + region: 'us-west1', + priority: 42, + unsafe_integer_val: 9007199254740992, + query: 'SELECT 1' + } + } + }, + { + 'Mcp-Method': 'tools/call', + 'Mcp-Name': 'test_custom_headers', + 'Mcp-Param-Region': 'us-west1', + 'Mcp-Param-Priority': '42', + 'Mcp-Param-UnsafeInteger': '9007199254740992' + } + ); + const checks = scenario.getChecks(); + expect( + statusesFor(checks, 'sep-2243-x-mcp-header-integer-safe-range') + ).toContain('FAILURE'); + } finally { + await scenario.stop(); + } + }); + + it('PASSes safe-integer-range when a client omits the out-of-range integer header', async () => { + const scenario = new HttpCustomHeadersScenario(); + const { serverUrl } = await scenario.start(testScenarioContext()); + try { + await post( + serverUrl, + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'test_custom_headers', + arguments: { + region: 'us-west1', + priority: 42, + unsafe_integer_val: 9007199254740992, + query: 'SELECT 1' + } + } + }, + { + 'Mcp-Method': 'tools/call', + 'Mcp-Name': 'test_custom_headers', + 'Mcp-Param-Region': 'us-west1', + 'Mcp-Param-Priority': '42' + // Mcp-Param-UnsafeInteger is deliberately omitted + } + ); + const checks = scenario.getChecks(); + expect( + statusesFor(checks, 'sep-2243-x-mcp-header-integer-safe-range') + ).toContain('SUCCESS'); + } finally { + await scenario.stop(); + } + }); }); describe('HttpInvalidToolHeadersScenario (SEP-2243) check IDs', () => { diff --git a/src/scenarios/client/http-custom-headers.ts b/src/scenarios/client/http-custom-headers.ts index 062efd72..15add2d8 100644 --- a/src/scenarios/client/http-custom-headers.ts +++ b/src/scenarios/client/http-custom-headers.ts @@ -42,7 +42,8 @@ export const CUSTOM_HEADERS_DECLARED_CHECK_IDS = [ 'sep-2243-client-mirrors-designated-params', 'sep-2243-client-encode-values', 'sep-2243-client-base64-unsafe', - 'sep-2243-client-omit-null' + 'sep-2243-client-omit-null', + 'sep-2243-x-mcp-header-integer-safe-range' ] as const; /** @@ -199,6 +200,7 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { arguments: { region: 'us-west1', priority: 42, + unsafe_integer_val: 9007199254740992, verbose: false, debug: true, empty_val: '', @@ -298,6 +300,12 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { description: 'Integer numeric value', 'x-mcp-header': 'Priority' }, + unsafe_integer_val: { + type: 'integer', + description: + 'Integer value outside IEEE754 safe range (-2^53+1 to 2^53-1) — MUST NOT be mirrored to an HTTP header', + 'x-mcp-header': 'UnsafeInteger' + }, verbose: { type: 'boolean', description: 'Boolean value', @@ -453,6 +461,37 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { // Check Mcp-Param-Priority header (integer) this.checkParamHeader(req, 'Priority', args.priority, 'integer'); + // Check Mcp-Param-UnsafeInteger header: + // SEP-2243: "Integer values MUST be within the safe range for integers + // represented using IEEE754 double-precision floating point numbers (-2^53+1 to 2^53-1)" + // An out-of-range integer argument MUST NOT be mirrored into an HTTP header. + if ( + args.unsafe_integer_val !== undefined && + args.unsafe_integer_val !== null + ) { + const unsafeIntegerHeader = req.headers['mcp-param-unsafeinteger'] as + | string + | undefined; + this.checks.push({ + id: 'sep-2243-x-mcp-header-integer-safe-range', + name: 'ClientCustomHeaderSafeIntegerRange', + description: + 'Integer values outside IEEE754 safe range (-2^53+1 to 2^53-1) MUST NOT be mirrored into Mcp-Param headers', + status: unsafeIntegerHeader === undefined ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + unsafeIntegerHeader !== undefined + ? `Client mirrored unsafe integer value '${unsafeIntegerHeader}' into Mcp-Param-UnsafeInteger header. Integer values MUST be within the safe range (-2^53+1 to 2^53-1).` + : undefined, + specReferences: [SPEC_REFERENCE_TOOL_DEF, SPEC_REFERENCE_CUSTOM], + details: { + headerName: 'Mcp-Param-UnsafeInteger', + rawHeaderValue: unsafeIntegerHeader, + bodyValue: args.unsafe_integer_val + } + }); + } + // Check Mcp-Param-Verbose header (boolean value) // checkParamHeader already FAILs on missing header, so this also covers // "optional parameter present → client MUST include header" without a diff --git a/src/seps/sep-2243.yaml b/src/seps/sep-2243.yaml index ddb06ffb..9fcf9462 100644 --- a/src/seps/sep-2243.yaml +++ b/src/seps/sep-2243.yaml @@ -25,6 +25,9 @@ requirements: - check: sep-2243-x-mcp-header-primitive-only 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-x-mcp-header-integer-safe-range + text: 'Integer values MUST be within the safe range for integers represented using IEEE754 double-precision floating point numbers (−2^53+1 to 2^53−1).' + 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 diff --git a/src/seps/traceability.json b/src/seps/traceability.json index 674f2e9c..9cb30c21 100644 --- a/src/seps/traceability.json +++ b/src/seps/traceability.json @@ -212,6 +212,12 @@ "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-x-mcp-header-integer-safe-range", + "status": "tested", + "text": "Integer values MUST be within the safe range for integers represented using IEEE754 double-precision floating point numbers (−2^53+1 to 2^53−1).", + "url": "https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers" + }, { "check": "sep-2243-client-reject-invalid-tool", "status": "tested", @@ -289,7 +295,7 @@ "sep-2243-server-no-xmcp-tool" ], "summary": { - "tested": 18, + "tested": 19, "untested": 2, "excluded": 4, "untracked": 3, From 7482d377a7babfea783cf46cd7f92fa291271e06 Mon Sep 17 00:00:00 2001 From: Yudistira Putra <85178972+Yudis-bit@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:36:28 +0700 Subject: [PATCH 2/2] fix(client): isolate unsafe integer header probe from ordinary checks --- .../client/http-custom-headers.test.ts | 259 ++++++++++++------ src/scenarios/client/http-custom-headers.ts | 107 +++++--- src/seps/traceability.json | 8 +- 3 files changed, 250 insertions(+), 124 deletions(-) diff --git a/src/scenarios/client/http-custom-headers.test.ts b/src/scenarios/client/http-custom-headers.test.ts index cc1a013b..ba0d3608 100644 --- a/src/scenarios/client/http-custom-headers.test.ts +++ b/src/scenarios/client/http-custom-headers.test.ts @@ -69,72 +69,106 @@ describe('HttpCustomHeadersScenario (SEP-2243) check IDs', () => { } }); - it('maps each parameter kind to its requirement ID on a conforming tool call', async () => { - const scenario = new HttpCustomHeadersScenario(); - const { serverUrl } = await scenario.start(testScenarioContext()); - try { - const nonAscii = 'Hello, 世界'; - const nonAsciiB64 = Buffer.from(nonAscii, 'utf-8').toString('base64'); - await post( - serverUrl, - { - jsonrpc: '2.0', - id: 1, - method: 'tools/call', - params: { - name: 'test_custom_headers', - arguments: { - region: 'us-west1', - priority: 42, - unsafe_integer_val: 9007199254740992, - non_ascii_val: nonAscii, - query: 'SELECT 1' + it.each([false, true])( + 'isolates range probe rejection (rejected: %s) from ordinary header checks', + async (rejectProbe) => { + const scenario = new HttpCustomHeadersScenario(); + const { serverUrl } = await scenario.start(testScenarioContext()); + try { + const nonAscii = 'Hello, 世界'; + const nonAsciiB64 = Buffer.from(nonAscii, 'utf-8').toString('base64'); + await post( + serverUrl, + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'test_custom_headers', + arguments: { + region: 'us-west1', + priority: 42, + non_ascii_val: nonAscii, + query: 'SELECT 1' + } } + }, + { + 'Mcp-Method': 'tools/call', + 'Mcp-Name': 'test_custom_headers', + 'Mcp-Param-Region': 'us-west1', + 'Mcp-Param-Priority': '42', + 'Mcp-Param-NonAscii': `=?base64?${nonAsciiB64}?=` } - }, - { - 'Mcp-Method': 'tools/call', - 'Mcp-Name': 'test_custom_headers', - 'Mcp-Param-Region': 'us-west1', - 'Mcp-Param-Priority': '42', - 'Mcp-Param-NonAscii': `=?base64?${nonAsciiB64}?=` - } - ); - await post( - serverUrl, - { - jsonrpc: '2.0', - id: 2, - method: 'tools/call', - params: { - name: 'test_custom_headers_null', - arguments: { - region: 'us-east1', - priority: 1, - verbose: null, - query: 'SELECT 1' + ); + await post( + serverUrl, + { + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { + name: 'test_custom_headers_null', + arguments: { + region: 'us-east1', + priority: 1, + verbose: null, + query: 'SELECT 1' + } } + }, + { + 'Mcp-Method': 'tools/call', + 'Mcp-Name': 'test_custom_headers_null', + 'Mcp-Param-Region': 'us-east1', + 'Mcp-Param-Priority': '1' + // Mcp-Param-Verbose deliberately omitted: value is null } - }, - { - 'Mcp-Method': 'tools/call', - 'Mcp-Name': 'test_custom_headers_null', - 'Mcp-Param-Region': 'us-east1', - 'Mcp-Param-Priority': '1' - // Mcp-Param-Verbose deliberately omitted: value is null + ); + + if (!rejectProbe) { + await post(serverUrl, { + jsonrpc: '2.0', + id: 3, + method: 'tools/call', + params: { + name: 'test_custom_headers_unsafe_integer', + arguments: { unsafe_integer_val: 9007199254740992 } + } + }); } - ); - const checks = scenario.getChecks(); - for (const id of CUSTOM_HEADERS_DECLARED_CHECK_IDS) { - const statuses = statusesFor(checks, id); - expect(statuses.length, id).toBeGreaterThan(0); - expect(statuses, id).not.toContain('FAILURE'); + const checks = scenario.getChecks(); + expect(idsOf(checks)).toEqual( + new Set(CUSTOM_HEADERS_DECLARED_CHECK_IDS) + ); + for (const id of CUSTOM_HEADERS_DECLARED_CHECK_IDS) { + const statuses = statusesFor(checks, id); + if ( + rejectProbe && + id === 'sep-2243-x-mcp-header-integer-safe-range' + ) { + expect(statuses).toEqual(['FAILURE']); + const check = checks.find((c) => c.id === id)!; + expect(check.details?.untestable).toBe(true); + expect(check.errorMessage).toContain( + 'test_custom_headers_unsafe_integer' + ); + expect(check.errorMessage).toContain( + 'rejected the argument locally' + ); + continue; + } + expect(statuses.length, id).toBeGreaterThan(0); + expect(statuses, id).not.toContain('FAILURE'); + } + const checkCount = checks.length; + expect(scenario.getChecks()).toHaveLength(checkCount); + } finally { + await scenario.stop(); } - } finally { - await scenario.stop(); } - }); + ); it('FAILs client-mirrors-designated-params when an annotated header is missing', async () => { const scenario = new HttpCustomHeadersScenario(); @@ -189,7 +223,6 @@ describe('HttpCustomHeadersScenario (SEP-2243) check IDs', () => { arguments: { region: 'us-west1', priority: 42, - unsafe_integer_val: 9007199254740992, non_ascii_val: nonAscii, query: 'SELECT 1' } @@ -231,6 +264,16 @@ describe('HttpCustomHeadersScenario (SEP-2243) check IDs', () => { : {} ); + await post(serverUrl, { + jsonrpc: '2.0', + id: 4, + method: 'tools/call', + params: { + name: 'test_custom_headers_unsafe_integer', + arguments: { unsafe_integer_val: 9007199254740992 } + } + }); + expect(toolsList.body.result.ttlMs).toBeGreaterThan(0); const checks = scenario.getChecks(); for (const id of CUSTOM_HEADERS_DECLARED_CHECK_IDS) { @@ -254,20 +297,13 @@ describe('HttpCustomHeadersScenario (SEP-2243) check IDs', () => { id: 1, method: 'tools/call', params: { - name: 'test_custom_headers', - arguments: { - region: 'us-west1', - priority: 42, - unsafe_integer_val: 9007199254740992, - query: 'SELECT 1' - } + name: 'test_custom_headers_unsafe_integer', + arguments: { unsafe_integer_val: 9007199254740992 } } }, { 'Mcp-Method': 'tools/call', - 'Mcp-Name': 'test_custom_headers', - 'Mcp-Param-Region': 'us-west1', - 'Mcp-Param-Priority': '42', + 'Mcp-Name': 'test_custom_headers_unsafe_integer', 'Mcp-Param-UnsafeInteger': '9007199254740992' } ); @@ -291,20 +327,13 @@ describe('HttpCustomHeadersScenario (SEP-2243) check IDs', () => { id: 1, method: 'tools/call', params: { - name: 'test_custom_headers', - arguments: { - region: 'us-west1', - priority: 42, - unsafe_integer_val: 9007199254740992, - query: 'SELECT 1' - } + name: 'test_custom_headers_unsafe_integer', + arguments: { unsafe_integer_val: 9007199254740992 } } }, { 'Mcp-Method': 'tools/call', - 'Mcp-Name': 'test_custom_headers', - 'Mcp-Param-Region': 'us-west1', - 'Mcp-Param-Priority': '42' + 'Mcp-Name': 'test_custom_headers_unsafe_integer' // Mcp-Param-UnsafeInteger is deliberately omitted } ); @@ -318,6 +347,78 @@ describe('HttpCustomHeadersScenario (SEP-2243) check IDs', () => { }); }); +describe('HttpCustomHeadersScenario range probe', () => { + it('advertises a dedicated integer tool and schedules it after the ordinary calls', async () => { + const scenario = new HttpCustomHeadersScenario(); + const { serverUrl, context } = await scenario.start(testScenarioContext()); + try { + const toolCalls = context!.toolCalls as Array<{ + name: string; + arguments: Record; + }>; + expect(toolCalls.map((call) => call.name)).toEqual([ + 'test_custom_headers', + 'test_custom_headers_null', + 'test_custom_headers_unsafe_integer' + ]); + expect(toolCalls[0].arguments).not.toHaveProperty('unsafe_integer_val'); + expect(toolCalls[2].arguments).toEqual({ + unsafe_integer_val: 9007199254740992 + }); + const listed = await postJson(serverUrl, { + jsonrpc: '2.0', + id: 1, + method: 'tools/list' + }); + const tools = listed.result.tools; + expect(tools[0].inputSchema.properties).not.toHaveProperty( + 'unsafe_integer_val' + ); + expect(tools[2]).toMatchObject({ + name: 'test_custom_headers_unsafe_integer', + inputSchema: { + properties: { + unsafe_integer_val: { + type: 'integer', + 'x-mcp-header': 'UnsafeInteger' + } + }, + required: ['unsafe_integer_val'] + } + }); + } finally { + await scenario.stop(); + } + }); + + it.each([undefined, null, 42, '9007199254740992'])( + 'does not pass when the client changes or omits the probe argument (%s)', + async (value) => { + const scenario = new HttpCustomHeadersScenario(); + const { serverUrl } = await scenario.start(testScenarioContext()); + try { + await post(serverUrl, { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'test_custom_headers_unsafe_integer', + arguments: { unsafe_integer_val: value } + } + }); + const checks = scenario + .getChecks() + .filter((c) => c.id === 'sep-2243-x-mcp-header-integer-safe-range'); + expect(checks).toHaveLength(1); + expect(checks[0].status).toBe('FAILURE'); + expect(checks[0].details?.untestable).toBe(true); + } finally { + await scenario.stop(); + } + } + ); +}); + describe('HttpInvalidToolHeadersScenario (SEP-2243) check IDs', () => { it('emits every x-mcp-header constraint ID, SUCCESS when only valid_tool is called', async () => { const scenario = new HttpInvalidToolHeadersScenario(); diff --git a/src/scenarios/client/http-custom-headers.ts b/src/scenarios/client/http-custom-headers.ts index 15add2d8..462a1295 100644 --- a/src/scenarios/client/http-custom-headers.ts +++ b/src/scenarios/client/http-custom-headers.ts @@ -14,6 +14,7 @@ import type { ScenarioContext } from '../../mock-server'; import http from 'http'; import { ScenarioUrls, ConformanceCheck } from '../../types.js'; import { BaseHttpScenario } from './http-base.js'; +import { untestableCheck } from '../untestable.js'; const SPEC_REFERENCE_CUSTOM = { id: 'SEP-2243-Custom-Headers', @@ -200,7 +201,6 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { arguments: { region: 'us-west1', priority: 42, - unsafe_integer_val: 9007199254740992, verbose: false, debug: true, empty_val: '', @@ -225,6 +225,12 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { verbose: null, query: 'SELECT 1' } + }, + // Keep the range probe last: a client may reject this argument before + // sending the request, but must still exercise the ordinary checks. + { + name: 'test_custom_headers_unsafe_integer', + arguments: { unsafe_integer_val: 9007199254740992 } } ] }; @@ -237,6 +243,18 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { // calls the annotated tools. The `some()` guard makes this idempotent. for (const id of CUSTOM_HEADERS_DECLARED_CHECK_IDS) { if (this.checks.some((c) => c.id === id)) continue; + if (id === 'sep-2243-x-mcp-header-integer-safe-range') { + this.checks.push( + untestableCheck( + id, + 'ClientCustomHeaderSafeIntegerRange', + 'Out-of-range integer arguments are not mirrored into Mcp-Param headers', + 'Client did not send test_custom_headers_unsafe_integer with unsafe_integer_val = 9007199254740992. It may have rejected the argument locally; the server cannot distinguish rejection from a skipped probe.', + [SPEC_REFERENCE_TOOL_DEF] + ) + ); + continue; + } const missingNullCall = id === 'sep-2243-client-omit-null' && !this.nullToolCallReceived; this.checks.push({ @@ -300,12 +318,6 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { description: 'Integer numeric value', 'x-mcp-header': 'Priority' }, - unsafe_integer_val: { - type: 'integer', - description: - 'Integer value outside IEEE754 safe range (-2^53+1 to 2^53-1) — MUST NOT be mirrored to an HTTP header', - 'x-mcp-header': 'UnsafeInteger' - }, verbose: { type: 'boolean', description: 'Boolean value', @@ -418,6 +430,21 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { }, required: ['region', 'priority', 'query'] } + }, + { + name: 'test_custom_headers_unsafe_integer', + description: + 'A separate probe for out-of-range integer header values', + inputSchema: { + type: 'object', + properties: { + unsafe_integer_val: { + type: 'integer', + 'x-mcp-header': 'UnsafeInteger' + } + }, + required: ['unsafe_integer_val'] + } } ] } @@ -461,37 +488,6 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { // Check Mcp-Param-Priority header (integer) this.checkParamHeader(req, 'Priority', args.priority, 'integer'); - // Check Mcp-Param-UnsafeInteger header: - // SEP-2243: "Integer values MUST be within the safe range for integers - // represented using IEEE754 double-precision floating point numbers (-2^53+1 to 2^53-1)" - // An out-of-range integer argument MUST NOT be mirrored into an HTTP header. - if ( - args.unsafe_integer_val !== undefined && - args.unsafe_integer_val !== null - ) { - const unsafeIntegerHeader = req.headers['mcp-param-unsafeinteger'] as - | string - | undefined; - this.checks.push({ - id: 'sep-2243-x-mcp-header-integer-safe-range', - name: 'ClientCustomHeaderSafeIntegerRange', - description: - 'Integer values outside IEEE754 safe range (-2^53+1 to 2^53-1) MUST NOT be mirrored into Mcp-Param headers', - status: unsafeIntegerHeader === undefined ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: - unsafeIntegerHeader !== undefined - ? `Client mirrored unsafe integer value '${unsafeIntegerHeader}' into Mcp-Param-UnsafeInteger header. Integer values MUST be within the safe range (-2^53+1 to 2^53-1).` - : undefined, - specReferences: [SPEC_REFERENCE_TOOL_DEF, SPEC_REFERENCE_CUSTOM], - details: { - headerName: 'Mcp-Param-UnsafeInteger', - rawHeaderValue: unsafeIntegerHeader, - bodyValue: args.unsafe_integer_val - } - }); - } - // Check Mcp-Param-Verbose header (boolean value) // checkParamHeader already FAILs on missing header, so this also covers // "optional parameter present → client MUST include header" without a @@ -623,6 +619,41 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario { : undefined, specReferences: [SPEC_REFERENCE_CUSTOM] }); + } else if (toolName === 'test_custom_headers_unsafe_integer') { + const unsafeIntegerHeader = req.headers['mcp-param-unsafeinteger']; + if (args.unsafe_integer_val !== 9007199254740992) { + this.checks.push( + untestableCheck( + 'sep-2243-x-mcp-header-integer-safe-range', + 'ClientCustomHeaderSafeIntegerRange', + 'Out-of-range integer arguments are not mirrored into Mcp-Param headers', + 'Client called test_custom_headers_unsafe_integer without the requested unsafe_integer_val = 9007199254740992.', + [SPEC_REFERENCE_TOOL_DEF] + ) + ); + } else { + // This checks only the on-wire range constraint. Whether an invalid + // argument must cause a local error instead of header omission remains + // an open spec question in #445; SUCCESS does not settle that question. + this.checks.push({ + id: 'sep-2243-x-mcp-header-integer-safe-range', + name: 'ClientCustomHeaderSafeIntegerRange', + description: + 'Out-of-range integer arguments are not mirrored into Mcp-Param headers', + status: unsafeIntegerHeader === undefined ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + unsafeIntegerHeader !== undefined + ? `Client sent Mcp-Param-UnsafeInteger '${unsafeIntegerHeader}' for out-of-range argument 9007199254740992. Integer values MUST be within the safe range (-2^53+1 to 2^53-1).` + : undefined, + specReferences: [SPEC_REFERENCE_TOOL_DEF, SPEC_REFERENCE_CUSTOM], + details: { + headerName: 'Mcp-Param-UnsafeInteger', + rawHeaderValue: unsafeIntegerHeader, + bodyValue: args.unsafe_integer_val + } + }); + } } else if (toolName === 'test_custom_headers_null') { this.nullToolCallReceived = true; diff --git a/src/seps/traceability.json b/src/seps/traceability.json index 9cb30c21..674f2e9c 100644 --- a/src/seps/traceability.json +++ b/src/seps/traceability.json @@ -212,12 +212,6 @@ "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-x-mcp-header-integer-safe-range", - "status": "tested", - "text": "Integer values MUST be within the safe range for integers represented using IEEE754 double-precision floating point numbers (−2^53+1 to 2^53−1).", - "url": "https://modelcontextprotocol.io/specification/draft/server/tools#custom-headers" - }, { "check": "sep-2243-client-reject-invalid-tool", "status": "tested", @@ -295,7 +289,7 @@ "sep-2243-server-no-xmcp-tool" ], "summary": { - "tested": 19, + "tested": 18, "untested": 2, "excluded": 4, "untracked": 3,