Skip to content

Coerce string→number/boolean on MCP tool params (P2)#169

Merged
keysersoft merged 1 commit into
mainfrom
keysersoft/zod-coerce
May 12, 2026
Merged

Coerce string→number/boolean on MCP tool params (P2)#169
keysersoft merged 1 commit into
mainfrom
keysersoft/zod-coerce

Conversation

@keysersoft
Copy link
Copy Markdown
Contributor

Problem

Many MCP clients (and most LLM tool-call layers) serialize every argument as a string before transport. A tool generated from an OpenAPI `type: integer` parameter was rejecting valid calls like:

```
{ "top_k": "5" }
→ "Input validation error: expected number, received string"
```

This blocked the `koch-filesystem-bridge` integration on real client traffic — the Claude desktop MCP path was always passing `top_k` as a string.

Fix

In `McpServerService.jsonSchemaToZod`, switch the numeric / boolean / date paths to use `z.coerce.*`:

OpenAPI Old New
`type: 'integer'` `z.number()` `z.coerce.number().int()`
`type: 'number'` `z.number()` `z.coerce.number()`
`type: 'boolean'` `z.boolean()` `z.coerce.boolean()`
`type: 'string', format: 'date' | 'date-time'` `z.string()` `z.coerce.date()`

Coercion still rejects non-coercible strings (e.g. `"abc"` for integer, `"1.5"` for integer), so we keep the validation signal where it matters. Enum strings stay strict.

Defensive side-fix

`openapi-3.1-normalizer` now also strips `info.summary` — a 3.1-only field that the 3.0 schema validator rejects. We currently use `dereference()` on 3.1 docs (no validation), so this is a preventive cleanup: if any future code path calls `validate()` the relabeled 3.0.3 document stays accepted.

Test plan

  • 9 new unit tests on `jsonSchemaToZod` (number/integer/float/boolean/date-time/enum/optional/plain string/negative).
  • 2 new tests on the `info.summary` strip.
  • Full backend suite green: 643 passing across 27 suites.
  • `tsc --noEmit` clean.
  • After deploy: re-import a FastAPI 3.1 spec with a `top_k: int` parameter; call the tool with `{"top_k": "5"}` from a string-serializing client; expect a 200, not a validation error.

Several MCP clients (and a lot of LLM tool-call layers) serialize every
argument as a string before transport. A tool with a numeric param
generated from OpenAPI 'type: integer' was rejecting valid calls like

  { top_k: '5' }
  → 'Input validation error: expected number, received string'

Fix: in jsonSchemaToZod, use z.coerce.number(), z.coerce.number().int(),
z.coerce.boolean(), and z.coerce.date() (for string fields with
format: date / date-time). Coercion still rejects non-numeric strings,
so the validation signal where it matters is preserved.

Also defensive: openapi-3.1-normalizer now strips info.summary, a 3.1-only
field that 3.0 schema validation rejects. We currently use dereference()
on 3.1 docs (no validation), so this is preventive — if a future code
path calls validate() the relabeled 3.0.3 document stays accepted.

Tests: 9 new specs for the coerce paths (number, integer, float,
boolean, date-time, enum strict, optional, plain string, negative-case
'abc' rejection). 2 new specs for the info.summary strip. Full suite
643 passing.
@keysersoft keysersoft merged commit 20c568f into main May 12, 2026
11 checks passed
@keysersoft keysersoft deleted the keysersoft/zod-coerce branch May 12, 2026 07:47
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.

1 participant