fix: include defaults and descriptions in MCP schema#159
Draft
fix: include defaults and descriptions in MCP schema#159
Conversation
The custom zod→JSON-Schema converter dropped three things that matter
for MCP-facing tool schemas:
- ZodDefault was unhandled, so every `.default(X)` field emitted `{}`
(no type, no default) AND landed in `required[]` because the
optional-check only recognised ZodOptional. Callers and LLMs were
told these fields were mandatory with no type hint.
- `.describe(...)` strings were never read, so no tool parameter
description ever reached the JSON schema.
- ZodLiteral and ZodUnion produced `{}` as well, hiding the single
union use case in view-network-logs.
Now the converter unwraps ZodDefault / ZodOptional / ZodNullable,
carries defaults and descriptions through, and emits `const` / `anyOf`
for literals and unions. Defaulted fields are correctly omitted from
`required[]`. Covered by 18 new unit tests.
Member
Author
|
Opus 4.7 flagged this as a serious rookie mistake. I have to research what our MCP server actually expects from the zod schemas before handing this off to review, because this PR has a large impact to what the harness sees. |
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.
We currently have a lot of descriptions and defaults in our tool zod definitions, but all are scrubbed before being passed to the agent.
This PR fixes this.
Do we want to expose ALL our descriptions of every single tool parameter on our MCP server? I'm yet to test this, but we probably want to trim these descriptions before doing that.
Claude:
The custom zod → JSON-Schema converter at
packages/registry/src/zod-to-json-schema.tssilently dropped three things that every MCP-facing tool relies on:.default(X)values,.describe(...)strings, andz.literal/z.uniontypes. In practice every tool with defaulted params (all ofdebugger-*,react-profiler-*,profiler-*,network-*, plus others) was handing the LLM a schema that was simultaneously:{}with no type, no default, no description.required[]because the optional-check recognised onlyZodOptional, notZodDefault.Net effect: an LLM reading the tool list was told "you must pass
port,sample_interval_us,contextLines,maxItems, …" with no hint of type or default, instead of "these are optional numbers with sensible defaults."What changed
ZodDefault→ unwrap to base type, emitdefault: X, exclude fromrequired[]..describe()→ readtype.descriptionon the outermost wrapper and attach it to the emitted schema.ZodLiteral→ emit{ const: value }.ZodUnion→ emit{ anyOf: [...] }.ZodNullable→ unwrap to base type (was also emitting{}).isOptionalalso now treatsZodDefaultas optional so defaulted fields leaverequired[].Before / after (live HTTP
/tools)debugger-inspect-element—properties.port{}{ "type": "number", "default": 8081, "description": "Metro server port" }debugger-inspect-element—required[][port, device_id, x, y, contextLines, resolveSourceMaps, maxItems, includeSkipped][device_id, x, y]react-profiler-analyze—properties.platform{}(in required[]){ "type": "string", "enum": ["ios","android"], "default": "ios", "description": "Target platform" }view-network-logs—properties.pageIndex{}(in required[]){ "anyOf": [{"type":"number"}, {"const":"latest"}], "default": "latest", "description": "Page index (0-based) or \"latest\" for the most recent page. Each page contains up to 50 entries." }Required-field counts (sample)
debugger-inspect-elementreact-profiler-startreact-profiler-analyzeboot-device.optional())view-network-logsTests
18 new unit tests in
packages/registry/tests/zod-to-json-schema.test.ts, one per behavior:.default(), chained after.optional()).number,z.coerce.number,boolean,z.coerce.string, enum.required[].ZodDefaultwrappingZodOptionaland vice versa.ZodLiteral→const,ZodUnion→anyOf, defaulted union.ZodNullableunwraps to base type.All 18 pass; full registry (75) and tool-server (386) suites still pass.
Why a separate PR
This is not Android-specific — every platform, every tool is affected — so splitting it out from
feat/android-emulator-supportkeeps the Android diff focused and makes this fix independently revertable / bisectable if any strict downstream consumer reacts to the new schema fields.Test plan
npx vitest run --root packages/registry— new converter tests + existing registry tests.npx vitest run --root packages/tool-server— ensure no downstream test relies on the old empty-schema shape.curl /tools, spot-check a handful of schemas (debugger-inspect-element,react-profiler-analyze,view-network-logs).