Skip to content

fix(schema): advertise every tool's outputSchema as permissive (propagates apple-mail-mcp#135) - #123

Merged
sweetrb merged 2 commits into
mainfrom
fix/permissive-output-schemas
Aug 6, 2026
Merged

fix(schema): advertise every tool's outputSchema as permissive (propagates apple-mail-mcp#135)#123
sweetrb merged 2 commits into
mainfrom
fix/permissive-output-schemas

Conversation

@sweetrb

@sweetrb sweetrb commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Propagates the schema half of sweetrb/apple-mail-mcp#135 to this repo. Same defect, same fix, verified the same way.

The defect

The MCP client validates a result's structuredContent against the JSON Schema the server advertised — not against the server's own zod object. A bare zod raw shape renders as additionalProperties: false, so any field a handler emits that its schema doesn't enumerate becomes a hard client-side -32602 … data must NOT have additional properties, discarding a payload the handler computed correctly.

The server never notices, because zod's own parse silently strips unknown keys instead of failing. That is exactly why the registerTool/outputSchema migration's "all fields optional, no .strict()" was believed permissive: it covered optionality, it did not cover undeclared keys.

Why it matters here

In apple-mail-mcp this was not latent — it broke get-mail-stats on every call for anyone with IMAP configured, because that tool's IMAP branch spreads an object carrying a perMailbox key the schema never declared. Measuring every repo in the family showed the same advertisement everywhere:

repo tools advertising additionalProperties: false (before)
apple-mail-mcp 50 of 50 — fixed in 2.10.6
apple-notes-mcp 36 of 36
apple-photos-mcp 21 of 21
apple-numbers-mcp 26 of 26

So in this repo it is currently latent: it costs nothing until some handler's payload gains a key its schema doesn't list, at which point that tool fails completely rather than degrading — and CI would stay green while it happened.

The fix

Every tool registers through a wrapper that wraps its shape in .passthrough(), advertising additionalProperties: true — the contract the migration intended. A declared field still documents the shape; an undeclared one is carried through instead of nuking the result. No tool signature, parameter or behaviour changes.

The guard

The outputSchema contract test now fails any tool advertising additionalProperties: false. The existing assertions couldn't see this class: they check that every tool has a schema and that none requires a field, then round-trip only the diagnostic tools — so a tool with an undeclared key passes CI and fails in the user's client. Verified a real guard rather than a tautology: it reports every tool in this repo as an offender before the fix and none after.

Verification

  • typecheck, lint, format:check clean; full unit suite passes
  • outputSchema contract suite passes against the freshly built bundle
  • Booted the built bundle over stdio and confirmed additionalProperties: false count went to 0
  • Committed bundle rebuilt and in sync

sweetrb added 2 commits August 6, 2026 08:00
The MCP CLIENT validates structuredContent against the ADVERTISED JSON Schema,
and a bare zod raw shape renders as additionalProperties:false — so any field a
handler emits that its schema doesn't enumerate is a hard client-side -32602
that discards an otherwise-correct result. The server never notices, because
zod's own parse strips unknown keys rather than failing, which is why the
registerTool/outputSchema migration's "all fields optional, no .strict()" read
as permissive: it covered optionality, not undeclared keys.

Every tool in this repo was advertising additionalProperties:false. All now
register through a wrapper applying .passthrough(), and the contract test fails
any tool that regresses.

Found while fixing the same defect in apple-mail-mcp, where it was not latent —
it broke get-mail-stats on every call (sweetrb/apple-mail-mcp#135).
@sweetrb
sweetrb merged commit 9a8d68b into main Aug 6, 2026
10 checks passed
@sweetrb
sweetrb deleted the fix/permissive-output-schemas branch August 6, 2026 12:04
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