Accept numeric tool arguments sent as strings - #532
Conversation
MCP clients quote numeric arguments often enough that a bare z.number() refuses calls that are otherwise well formed. update-page was the visible case: a quoted section number was rejected with "Invalid input", leaving a full-page rewrite as the only way to edit one section. The same refusal applied to all 24 numeric parameters, among them update-page's own latestId, which its description tells callers to pair with section=N. unquoteNumber() reads a bare run of digits as the number it spells and hands everything else to the wrapped schema to refuse. So "", null, false and [] are still rejected rather than read as zero the way z.coerce.number() would, zero being a page's lead section. The published JSON Schema is unchanged, byte for byte across all 46 tools, so callers are still asked for a number. Optionality belongs on the wrapper's result rather than its argument, or the published schema would demand a value the tool treats as optional; the compiler refuses the wrong order. Tool tests call handle() or dispatch() with already-parsed arguments and so never reach the schema, which is why this went unnoticed. The regression tests go over a real MCP session, and a sweep over every numeric argument on the tool surface fails if one is ever left unwrapped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing this: quoted numeric arguments are a client-side defect, and absorbing them here is the wrong layer. The change worked and was verified — 24 numeric parameters, published schema byte-identical across all 46 tools, quoted values reaching the handler end to end. The objection is not to the implementation but to the premise. The server would be silently accepting values its own published schema says are invalid, which hides the defect from the clients that need to fix it and commits us to carrying that tolerance indefinitely. The Some of the work here is worth keeping and will be split out separately:
Branch left in place for reference. |
Fixes #526
MCP clients quote numeric arguments often enough that a bare
z.number()refuses calls that are otherwise well formed.update-pagewas the visible case: a quoted section number was rejected withInvalid input, leaving a full-page rewrite as the only way to edit one section. The same refusal applied to all 24 numeric parameters, among themupdate-page's ownlatestId, which its description tells callers to pair withsection=N.unquoteNumber()reads a bare run of digits as the number it spells and hands everything else to the wrapped schema to refuse. So"",null,falseand[]are still rejected rather than read as zero the wayz.coerce.number()would, zero being a page's lead section.The published JSON Schema is unchanged, byte for byte across all 46 tools, so callers are still asked for a number. Optionality belongs on the wrapper's result rather than its argument, or the published schema would demand a value the tool treats as optional; the compiler refuses the wrong order.
Tool tests call
handle()ordispatch()with already-parsed arguments and so never reach the schema, which is why this went unnoticed. The regression tests go over a real MCP session, and a sweep over every numeric argument on the tool surface fails if one is ever left unwrapped.To decide
Scope. The issue asks only for
update-page'ssection. That fix alone leaveslatestIdrefusing quoted numbers in the very workflow the issue reproduces, so this covers all numeric parameters. Say the word if you want it narrowed.z.boolean()has the identical bug, left out of this PR: six parameters (get-recent-changes's fivehide*flags,move-page.moveTalk) still refuse"true". A client that quotes2will quotetrue. Worth a follow-up issue.Read #526's diagnosis with care. Its title says
update-page"rejects all numeric section values" and its suspected cause saysget-pagecoerces. Both are wrong:section: 2passes on master, and nothing coerces anywhere —get-page section:"1",search-page limit:"3"andget-revision revisionId:"123"all fail identically. Only callers that quote the number were affected.Considered, omitted
neowiki-cypher-query.parameters—z.record(z.string(), z.unknown())forwards{"minYear": "2000"}to Neo4j as a string. No declared numeric type to wrap, so out of reach here.tests/runtime/cancellation.test.ts's hand-rolled sessions onto the new helper — unrelated churn.Verified
lint,typecheck,fmt:check,build,check:mcpand 1766 tests green.section,latestId,limit,revisionIdandnamespacesreach the handler and return real results;"lead","",nulland an omittedrevisionIdare still refused..nonoptional()fails only the test that names it, swapping inNumber()coercion fails the zero test, and unwrapping one parameter fails the sweep naming that parameter.