Skip to content

33 numeric MCP parameters, 5 coercions — a client with a cached tool list sends a string and gets -32602 #670

Closed

Description

@serge-ivo

33 numeric MCP parameters, 5 coercions — a host with a cached tool list gets -32602 on the rest

Found while fixing #614, and it caused a production failure there before being caught.

What happened. #614 added offset to a paged tool. The first production check passed on page 1 and failed on page 2 with -32602 Invalid params: hosts cache the tool list, so a client whose cache predates the new parameter has no schema to cast against and sends "63" as a string. z.number() rejects it.

Why that is worse than the bug it was fixing: page 1 looked correct while 66 of 118 cards were unreachable. It fails silently in the direction of success — the caller gets a valid-looking first page and no indication the rest is gone.

The scope, measured now:

$ grep -rc 'z.coerce' workers/mcp/src --include='*.ts'   →  5 occurrences
$ grep -rho 'z\.number()' workers/mcp/src --include='*.ts' | wc -l   →  33

#614 fixed the three arguments where a stale client costs data. The other numeric parameters across the surface share the same fragility and were deliberately not swept inside that issue.

Why this is structural, not a one-off

A cached tool list is the normal state of an MCP client, not an edge case. This session alone hit tool-list staleness three times: the ChatGPT connector served an unannotated list until manually refreshed, this Claude session held 135 tools while the server served 136, and #573 exists because a surface change had no version signal to invalidate against. Any parameter added or widened after a client last cached will arrive as a string.

It is also cheap and safe to fix: #614 measured that z.coerce.number() publishes a byte-identical inputSchema, so the surface lock still matched and no MCP_SERVER_VERSION bump was needed. That was verified, not assumed.

Acceptance criteria

  1. Every numeric MCP parameter coerces, or the ones that deliberately do not are enumerated with a reason. Booleans deserve the same question — check whether z.boolean() has the same exposure and report the count either way.
  2. Confirm the inputSchema bytes are unchanged, as agent_trace at 163 KB and instance_board at 129 KB are over the wire limit — the documented first debugging read cannot be delivered #614 did; if any tool's schema does move, that is an append-only surface-lock entry plus a version bump, never a rewrite.
  3. A guard asserting no bare z.number() reaches a published parameter — denominator stated per ADR 0002 as parameters found, not files swept, which is the distinction docs-drift swept the file and missed a stale 124 — the detector matches phrasings, so its denominator counts files read, not claims checked #603/docs/ is swept by no gate — 27 findings across 9 files sit behind a green docs:drift #604 established.
  4. Red demonstrated: a string argument to a coerced parameter succeeds, and the guard fails when a bare z.number() is reintroduced.

Verified vs inferred

  • Verified: both grep counts, run 2026-08-16; the -32602 failure on page 2 and the 66-of-118 consequence; that z.coerce.number() left the inputSchema byte-identical at version 0.1.6.
  • Inferred: that the remaining parameters are reachable by a stale client in practice. They are all published parameters, so the mechanism applies — but which tools a real host has cached was not measured, and the practical exposure depends on which parameters changed recently. AC1's enumeration should note that a long-stable parameter is lower risk than a newly added one.

Activity

  1. added
    P2: correctnessReal defect, no live harm today — inert fields, miscounts, missing guards
    bugSomething isn't working
    backendBackend / Worker / API work
    mcpMCP server surface — the operator's instrument panel, not the product
    on Aug 18, 2026
  2. serge-ivo commented on Aug 18, 2026

    @serge-ivo
    ContributorAuthor

    Audit — still LIVE, and the ratio has got worse. Denominator re-measured.

    Re-run today against workers/mcp/src:

    issue asserts today
    z.coerce 5 5
    z.number() 33 36

    Three numeric parameters have been added since this was filed (the MCP wave: #696, #699, #704 all added bounded numeric args — limit, before, since_seq), and none of them coerce. So the gap this issue reports is not static: it widens with each tool that takes a number, which is the argument for fixing it at the schema-construction layer rather than per parameter.

    Worth stating in the ticket, per ADR 0002: the number to report is 31 of 36 uncoerced, and the shape of the fix should be one that a new tool inherits by default — otherwise the next audit re-measures 39.

    Leaving open, labels unchanged.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2: correctnessReal defect, no live harm today — inert fields, miscounts, missing guardsbackendBackend / Worker / API workbugSomething isn't workingmcpMCP server surface — the operator's instrument panel, not the product

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions