Skip to content

fix(server): reject unsafe integers in x-mcp-header parameters when header is absent - #2772

Open
Yudis-bit wants to merge 1 commit into
modelcontextprotocol:mainfrom
Yudis-bit:fix/issue-2689-unsafe-integer-param-header
Open

fix(server): reject unsafe integers in x-mcp-header parameters when header is absent#2772
Yudis-bit wants to merge 1 commit into
modelcontextprotocol:mainfrom
Yudis-bit:fix/issue-2689-unsafe-integer-param-header

Conversation

@Yudis-bit

Copy link
Copy Markdown

Fixes #2689.

What

Per the Streamable HTTP specification (Custom Headers from Tool Parameters):

Constraints on x-mcp-header values:
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).

And the server-side behavior table states:

Scenario Client Behavior Server Behavior
Client omits header but value is in body Non-conforming client Server MUST reject the request

Previously, in validateMcpParamHeaders, the validation loop attempted to convert bodyRaw via mcpParamPrimitiveToString(bodyRaw). When that returned undefined, it executed continue, assuming that bodyString === undefined solely indicated that the body carried a non-primitive object or array (which schema validation would catch at dispatch):

// Before:
const bodyString = mcpParamPrimitiveToString(bodyRaw);
if (bodyString === undefined) {
    // Body carries a non-primitive where the schema declares one;
    // params validation owns that fault. Skip the header check.
    continue;
}
if (headerValue === null) { ... }

However, mcpParamPrimitiveToString also returns undefined for numbers outside the safe-integer range (Number.isInteger(value) && !Number.isSafeInteger(value)) and non-finite numbers. This caused tools/call requests carrying an annotated unsafe integer argument (e.g. 9007199254740992) to skip parity validation entirely when the client omitted the Mcp-Param-* header. Because JSON Schema type: "integer" accepts values outside the JavaScript safe integer range without custom range constraints, dispatch-time schema validation did not reject the call either, allowing the server to invoke the tool handler rather than rejecting the request.

Furthermore, if a client provided the header for an unsafe integer, numericComparable previously coerced it with Number(decoded) === bodyRaw. Above Number.MAX_SAFE_INTEGER, IEEE 754 float precision loss means that different integers compare equal (e.g. Number('9007199254740993') === 9007199254740992 evaluates to true), which would incorrectly accept mismatched headers.

Changes

  1. packages/core-internal/src/shared/mcpParamHeaders.ts:
    • Only skip header parity validation when bodyRaw is truly a non-primitive (typeof bodyRaw === 'object' || typeof bodyRaw === 'function').
    • Check for absent headers (headerValue === null) across all primitive values before string conversion, rejecting with 400 Bad Request / -32020 HeaderMismatch (param-header-missing).
    • Restrict numericComparable to safe numbers (isSafeNumeric: typeof bodyRaw === 'number' && Number.isFinite(bodyRaw) && (!Number.isInteger(bodyRaw) || Number.isSafeInteger(bodyRaw))). Unsafe integers and non-finite numbers are not numerically coerced, preventing false-equality matches due to floating-point precision loss.
  2. packages/core-internal/test/shared/mcpParamHeaders.test.ts:
    • Added unit tests verifying that an unsafe integer (9_007_199_254_740_992) without a mirrored header rejects as param-header-missing.
    • Added unit test verifying that an unsafe integer with a mirrored header rejects as param-header-mismatch.
    • Added unit test verifying that non-primitive objects still skip parity check so schema validation can own that fault.
  3. packages/server/test/server/mcpParamValidation.test.ts:
    • Added integration test verifying that an inbound tools/call with an annotated integer parameter called with 9_007_199_254_740_992 and no Mcp-Param-Count header is rejected with 400 / -32020 before the handler is invoked.
  4. .changeset/reject-unsafe-integer-param-header.md:
    • Added changeset documenting the patch for @modelcontextprotocol/core-internal and @modelcontextprotocol/server.

Validation

  • pnpm run typecheck:all: 52 workspace projects pass with 0 errors.
  • npx vitest run: all 69 test files (1,460 tests) pass in @modelcontextprotocol/core-internal.
  • npx vitest run: all 42 test files (483 tests) pass in @modelcontextprotocol/server.
  • Prettier: npx prettier --check clean across all modified files.
  • ESLint: clean with 0 errors and 0 warnings.

Written with Antigravity agent against issue #2689 with minimal reproducible regression tests.

…eader is absent

Per the Streamable HTTP specification, integer values in `x-mcp-header`
fields MUST be within the JavaScript safe-integer range (−2^53+1 to 2^53−1).
Previously, `validateMcpParamHeaders` executed `continue` when
`mcpParamPrimitiveToString(bodyRaw)` returned `undefined`, assuming that
this only occurred when the body carried a non-primitive object/array
belonging to schema validation.

However, `mcpParamPrimitiveToString` also returns `undefined` for unsafe
integers and non-finite numbers. This caused `tools/call` requests with
annotated unsafe integer arguments (such as 9007199254740992) to skip
parity validation completely when the client omitted the `Mcp-Param-*`
header, invoking the tool handler instead of rejecting the non-conforming
request.

`validateMcpParamHeaders` now:
1. Only skips parity checks for non-primitive objects and functions.
2. Checks for absent headers (`headerValue === null`) across all primitive
   values and rejects with 400 / -32020 (`param-header-missing`).
3. Restricts numeric equality coercion (`numericComparable`) to safe
   integers so unsafe integers cannot be accepted via float precision loss.

Fixes modelcontextprotocol#2689
@Yudis-bit
Yudis-bit requested a review from a team as a code owner September 9, 2026 03:16
@changeset-bot

changeset-bot Bot commented Sep 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ce53d7d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core-internal Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2772

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2772

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2772

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2772

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2772

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2772

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2772

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2772

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2772

commit: ce53d7d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP server accepts unsafe integer in x-mcp-header field when mirrored header is absent

1 participant