feat(compat): registerTool/registerPrompt accept raw Zod shape, auto-wrap with z.object()#1901
feat(compat): registerTool/registerPrompt accept raw Zod shape, auto-wrap with z.object()#1901felixweinberger wants to merge 13 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: b5854c1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
182ec53 to
9b606ab
Compare
2972af1 to
a1bf55a
Compare
…wrap with z.object)
a1bf55a to
27e4ddf
Compare
|
@claude review |
|
@claude review |
…tputSchema raw-shape test
|
@claude review |
…spy; cover normalizeRawShapeSchema passthrough/undefined
688b282 to
1af9ed2
Compare
|
@claude review |
There was a problem hiding this comment.
1af9ed2 (test-only) looks good and the two open nits on the overload asymmetry and InferRawShape optionality are non-blocking from my side — deferring to a human for sign-off on the new public API surface (the @deprecated overloads and the ZodRawShape/InferRawShape/Legacy*Callback exports).
Extended reasoning...
Overview
This PR adds v1-compat raw-Zod-shape support to registerTool/registerPrompt: a new normalizeRawShapeSchema helper in @modelcontextprotocol/core that detects { field: z.string() } records and wraps them with z.object(), plus @deprecated overloads on McpServer.registerTool/registerPrompt and four new exported types (ZodRawShape, InferRawShape, LegacyToolCallback, LegacyPromptCallback). Across five review rounds the substantive issues — empty-shape crash, non-Zod-field over-acceptance, changeset/JSDoc inconsistency, missing outputSchema coverage — were all addressed. The latest commit (1af9ed2) is test-only: drops the now-vestigial console.warn spies, adds passthrough/undefined unit tests for normalizeRawShapeSchema, and adds an end-to-end tools/call test. Two 🟡 nits remain open (wrapped-input + raw-output overload gap; InferRawShape not applying ? to optional keys); both are explicitly low-impact and the author appears to have intentionally left them.
Security risks
None identified. The change is purely a schema-normalization shim on the registration path; no auth, transport, or untrusted-input handling is touched. The auto-wrap only fires on values that pass a Zod-specific structural check (_def / vendor === 'zod'), so there's no new injection or prototype-pollution surface.
Level of scrutiny
Moderate-to-high. The runtime change is small and well-tested, but this grows the public API of McpServer — the primary user-facing class — with new overloads and exported types, and REVIEW.md's guiding principles put the burden of proof on additions and ask that every new export be intentional. The PR is part of a coordinated 22-PR v2-bc series with its own reviewer guide and OSS-repo validation, which is exactly the kind of context a human maintainer tracking the series should sign off on rather than a per-PR bot.
Other factors
- The new
Legacy*types are exported frommcp.tsbut (per the author's earlier reply) intentionally not re-exported frompackages/server/src/index.ts; a human should confirm that's the desired surface. - Migration docs (
docs/migration.md/migration-SKILL.md) still say raw shapes are unsupported; the author has batched those updates into #1910 — reasonable, but another reason a human should be in the loop on the series. - Test coverage is now solid: unit tests for the detector/normalizer (incl. empty shape, non-Zod rejection, passthrough, undefined) and integration tests for inputSchema/outputSchema/argsSchema plus an e2e
tools/call.
v1 accepted any zod schema (AnySchema). v2 narrowed to StandardSchemaWithJSON, which requires ~standard.jsonSchema. completable() itself only needs validate (to type the callback's value param); JSON-Schema generation happens at the outer registerPrompt argsSchema level, not per-field. Widening to StandardSchemaV1 restores the v1 surface (any Standard Schema lib, zod >=3.24). Adds raw-shape registerPrompt test with a completable() field.
KKonstantinov
left a comment
There was a problem hiding this comment.
LGTM, small nits only.
- Both migration docs (the skill and general doc) state: "Raw object shapes are no longer accepted." This PR makes them accepted again. The changeset text is accurate, but the migration docs are not updated. Per REVIEW.md's "Documentation & Changesets" recurring catch, this is a flag.
…normalizeRawShapeSchema input; preserve optional in InferRawShape - Move isZodSchema/isZodRawShape/normalizeRawShapeSchema from standardSchema.ts to a new zodCompat.ts so standardSchema.ts is Standard-Schema-spec only. - normalizeRawShapeSchema now throws TypeError for inputs that are neither a raw shape nor a Standard Schema, instead of silently returning them. - InferRawShape now uses z.infer<z.ZodObject<S>> so .optional() fields produce ?: keys. - Changeset mentions the completable() constraint widening.
…malizeRawShapeSchema guard to require ~standard.jsonSchema
…ble error isZodSchema previously matched on `_def` / `~standard.vendor === 'zod'`, both of which Zod v3 schemas also satisfy. A v3 raw shape would pass isZodRawShape, get wrapped by v4's z.object(), and crash deep inside zod when listing or calling the tool. Now detect v4 via the `_zod` property (absent on v3), and add a dedicated runtime guard in normalizeRawShapeSchema that throws a clear TypeError when v3 fields are seen, telling the user to import from zod/v4 or wrap the shape themselves.
068ff56 to
a6b25ee
Compare
…ard v3-detection branch
There was a problem hiding this comment.
b5854c1 addresses both remaining nits (plain-object prototype check + null guard), and I have nothing further — leaving final sign-off to a human since this adds public overloads/types and widens the completable() constraint.
Extended reasoning...
Overview
This PR adds a v1-compat shim so registerTool/registerPrompt accept raw Zod shapes ({ field: z.string() }) and auto-wrap them with z.object(). It introduces packages/core/src/util/zodCompat.ts (isZodRawShape, normalizeRawShapeSchema), adds @deprecated overloads + new exported types (ZodRawShape, InferRawShape, LegacyToolCallback, LegacyPromptCallback) in mcp.ts, and widens the completable() generic constraint from StandardSchemaWithJSON to StandardSchemaV1. Tests live in zodCompat.test.ts and mcp.compat.test.ts.
Status of prior feedback
Every issue I raised across the review rounds has been addressed, most recently in b5854c1: isZodRawShape now requires a plain-object prototype (rejects [], Date, Map, etc., with tests), and the v3-detection branch in normalizeRawShapeSchema now null-guards before Object.values() (with a test asserting the helpful TypeError fires). The earlier rounds covered the empty-shape case, Zod-only narrowing, v3 detection, isStandardSchemaWithJSON guard, changeset wording, test-file split, outputSchema coverage, and InferRawShape optionality — all resolved. KKonstantinov's two asks (separate zodCompat module; runtime guard for invalid pass-through) are also applied.
Security risks
None identified. This is schema-handling glue for developer-supplied tool/prompt definitions; no auth, transport, or untrusted-input parsing changes.
Level of scrutiny
Moderate-to-high: it expands the public API surface of @modelcontextprotocol/server (new overloads and exported types), relaxes a public generic constraint on completable(), and is one of ~22 coordinated v2-bc PRs whose docs land separately in #1910. Per REVIEW.md ("Burden of proof is on addition", "Every new export is intentional"), API-surface additions warrant maintainer sign-off rather than bot approval.
Other factors
KKonstantinov already engaged on this PR and his asks were addressed; final approval should come from him or another maintainer. Migration-doc updates are intentionally deferred to #1910. The one remaining optional suggestion (re-exporting StandardSchemaV1 from core/public) was explicitly flagged non-blocking and is fine to skip.
Part of the v2 backwards-compatibility series — see reviewer guide.
v2 requires StandardSchema objects (e.g.
z.object({...})) forinputSchema. v1 accepted raw shapes{x: z.string()}. This auto-wraps raw shapesMotivation and Context
v2 requires StandardSchema objects (e.g.
z.object({...})) forinputSchema. v1 accepted raw shapes{x: z.string()}. This auto-wraps raw shapesv1 vs v2 pattern & evidence
v1 pattern:
`registerTool('x', {inputSchema: {a: z.string()}}, cb)`v2-native:
`registerTool('x', {inputSchema: z.object({a: z.string()})}, cb)`Evidence: ~70% of typical server migration LOC was wrapping shapes. Took multiple OSS repos to zero.
How Has This Been Tested?
v2-bc-integrationvalidation branchpnpm typecheck:all && pnpm lint:all && pnpm test:allgreenBreaking Changes
None — additive
@deprecatedshim.Types of changes
Checklist
Additional context
Stacks on: C1