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,