Repository navigation
Description
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
- 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. - Confirm the
inputSchemabytes 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. - 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. - 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
-32602failure on page 2 and the 66-of-118 consequence; thatz.coerce.number()left theinputSchemabyte-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
serge-ivo commentedon Aug 18, 2026
Audit — still LIVE, and the ratio has got worse. Denominator re-measured.
Re-run today against
workers/mcp/src:z.coercez.number()Three numeric parameters have been added since this was filed (the MCP wave:
#696,#699,#704all 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.
fix(mcp): the meta-tool pattern is named on all four channels, so a c…
feat(mcp): a session pinned to one instance at /mcp/i/<id>, publishin…