fix(server): enforce Mcp-Param-* parity for numbers the header codec cannot represent - #2690
Open
mrpmohiburrahman wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: 4ea82c1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2689.
The bug
A
tools/callwhosex-mcp-header-annotated integer argument is9007199254740992(2^53), sentwith the
Mcp-Param-*header omitted, answers HTTP 200 and runs the tool handler. Reproduces on3924de99.validateMcpParamHeadersconverts each annotated body value to a header string before comparing it.mcpParamPrimitiveToStringreturnsundefinedfor two unrelated reasons, and the caller onlyhandled the first:
safe range
Both hit the same
continue, two statements above the missing-header check, so for that declarationthe missing-header, invalid-encoding and comparison checks were all skipped.
Nothing downstream covered the second case. An unsafe integer is a valid JSON Schema
integer:Ajv's check is
!(data % 1)with no range bound, andz.number()accepts it. SovalidateMcpParamHeadersreturnedundefined,createMcpHandlerfell through to dispatch, and thehandler ran.
Both values are reachable over the wire, since
JSON.parse('{"a":9007199254740993,"b":1e400}')yields9007199254740992andInfinity.Mcp-Param-N9007199254740992-3202090071992547409921-320201e400(Infinity)-3202042-32020Row 2 goes wider than the issue reports: a flatly contradictory header was swallowed too.
The fix
One guard, gated on
decl.typerather than ontypeof bodyRawalone, so a number against atype: 'string'declaration keeps deferring to params validation:Opening that path needed two follow-ons at the same site. The comparison falls back to
String(bodyRaw)whenbodyStringisundefined, so an agreeing pair still passes when the value'sspelling is not plain decimal;
1e21mirrored as'1e+21'fails the header-sideCANONICAL_DECIMALgate and would otherwise be reported as disagreeing with itself. And since
JSON.stringifyrendersevery non-finite number as
null, a rejection would have announcedthe body carries n=null, namingthe one value these checks are documented to let through, so non-finite numbers now print as
themselves.
Existing cells and
paramHeaderMismatchRejectionare reused unchanged: 400,-32020,rung: 'param-header-validation'. No new cell id, no new constructor, no ladder change, no newdependency.
mcpParamPrimitiveToStringis left alone. It is correct, it is pinned by its own tests, andbuildMcpParamHeadersshares it, whereundefinedcorrectly means "omit the header". The guardbelongs at the validate site.
Deliberately unchanged
An unsafe integer whose header matches stays accepted. Parity holds, and SEP-2243's safe-range MUST
is client-side and definition-scoped; the server's mandate is the Server-Behavior row "Client omits
header but value is in body, server MUST reject". A number against a
type: 'string'declarationstill skips, and so does a genuine non-primitive.
The client is untouched.
buildMcpParamHeadersstill omits the header for exactly these values, so asame-SDK call carrying an unsafe integer now costs one
tools/listrefetch beforecallToolreissues and rethrows. Worth a follow-up, but not this PR.
docs/migration/support-2026-07-28.mdalready documents the fixed behaviour, so the code is beingbrought up to the doc and no doc change is needed.
Tests
Eight cases in the existing
validateMcpParamHeadersserver-behavior block: unsafe integer andnon-finite with the header absent, a disagreeing header, a numerically matching header, both
spellings of
1e21, the non-finite rejection message, and a regression guard that a number on atype: 'string'declaration still defers to params validation.One end-to-end case through
createMcpHandler.fetch(): an unsafe-integer body with no header isrejected 400
-32020and the handler never runs, asserted with avi.fn()spy. The issue asks forrejection before handler invocation and the status alone does not prove that. The body is built from
a raw JSON string so the value survives to the wire.
Every one of them was watched failing against unpatched source first.
Validation
pnpm run check:allexits 0.core-internalgoes 1447 to 1455 tests,server475 to 476, nothingelse moves.
Two failures remain, both verified to fail identically on unmodified
3924de99:client › stdio.test.ts › should fire onerror and close when ReadBuffer overflows, a 5s timeout.test/e2e › protocol.test.ts › protocol:timeout:max-total, intermittent. It drives real SSEdelivery under
vi.useFakeTimers()and asserts a looseticks.length >= 3. Runningscenarios/protocol.test.tsfive times each way gave 1 failure in 5 with this change and 1 failurein 5 with it reverted.
A changeset is included (
core-internal+server, patch).