Skip to content

feat(server): accept Standard Schemas for elicitation#2369

Open
mattzcarey wants to merge 8 commits into
mainfrom
feat/elicitation-standard-schema
Open

feat(server): accept Standard Schemas for elicitation#2369
mattzcarey wants to merge 8 commits into
mainfrom
feat/elicitation-standard-schema

Conversation

@mattzcarey

Copy link
Copy Markdown
Contributor

Fixes #662.

Allows Server.elicitInput() and ctx.mcpReq.elicitInput() to accept a Standard Schema/Zod object as requestedSchema, converts it to the existing JSON Schema wire shape, and keeps JSON Schema input working unchanged.

Verification:

  • pnpm --filter @modelcontextprotocol/server test -- jsonSchemaValidatorOverride.test.ts
  • pnpm --filter @modelcontextprotocol/core typecheck
  • pnpm --filter @modelcontextprotocol/server typecheck
  • pnpm --filter @modelcontextprotocol/core lint
  • pnpm --filter @modelcontextprotocol/server lint
  • pnpm --filter @modelcontextprotocol/server build
  • pre-push hook: pnpm run typecheck:all, pnpm run build:all, pnpm run lint:all
  • git diff --check

@mattzcarey mattzcarey requested a review from a team as a code owner June 25, 2026 12:46
@changeset-bot

changeset-bot Bot commented Jun 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a5ba576

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core Minor
@modelcontextprotocol/server Minor
@modelcontextprotocol/express Major
@modelcontextprotocol/fastify Major
@modelcontextprotocol/hono Major
@modelcontextprotocol/node Major

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

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2369

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2369

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2369

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2369

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2369

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2369

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2369

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2369

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2369

commit: a5ba576

Comment thread packages/server/src/server/server.ts Outdated
Comment thread packages/core-internal/src/shared/protocol.ts
Comment thread packages/core-internal/src/shared/protocol.ts
Comment thread packages/server/src/server/server.ts Outdated
Comment thread examples/server/src/serverGuide.examples.ts
@mattzcarey mattzcarey force-pushed the feat/elicitation-standard-schema branch from 01f43f4 to f732d6f Compare June 25, 2026 13:51
Comment thread packages/server/src/server/server.ts Outdated
Comment thread packages/server/src/server/server.ts Outdated
Comment thread examples/server/src/elicitationFormExample.ts

@felixweinberger felixweinberger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

Comment thread packages/server/src/server/server.ts Outdated
Comment thread packages/server/src/server/server.ts Outdated
Comment thread packages/server/src/server/elicitation.ts
Comment on lines +1 to +4
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/server': minor
---

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The changeset bumps @modelcontextprotocol/core, but this PR does not touch packages/core at all — the protocol changes (new ElicitInputFormParams/ElicitInputResult types, widened ServerContext.mcpReq.elicitInput) live in packages/core-internal plus packages/server, and @modelcontextprotocol/client re-exports the changed public barrel so its bundled types change too. As written, core gets a no-op minor release while core-internal and client are not versioned; the front-matter should be '@modelcontextprotocol/core-internal': minor, '@modelcontextprotocol/server': minor, and '@modelcontextprotocol/client': minor, with core removed.

Extended reasoning...

What the bug is. .changeset/standard-schema-elicitation.md declares minor bumps for '@modelcontextprotocol/core' and '@modelcontextprotocol/server'. But this PR's library changes live entirely in packages/core-internal (src/shared/protocol.ts adds ElicitInputFormParams/ElicitInputResult and widens ServerContext.mcpReq.elicitInput into an overload set; src/exports/public/index.ts exports the new types) and packages/server (server.ts overloads, the new elicitation.ts module). Nothing under packages/core is touched.

Why core is the wrong package. packages/core is the public schemas-only package — its barrel re-exports only the *Schema Zod constants (spec + OAuth/OpenID) bundled from core-internal/schemas and core-internal/auth. This PR adds TypeScript types and runtime conversion logic, not new *Schema constants, so @modelcontextprotocol/core's published surface is byte-for-byte unchanged. Bumping it produces a pointless minor release with a misleading changelog entry (the changeset-bot table on this PR confirms core: Minor is what would be released).

Why core-internal and client should be listed instead. Repo convention is consistent: the other pending changesets that touch core-internal/src/shared/protocol.ts (e.g. custom-methods-minimal.md, wraphandler-hook.md, add-sdk-http-error.md, support-standard-json-schema.md) all attribute the change to '@modelcontextprotocol/core-internal' plus the consuming public packages (client and/or server); the only changeset that names '@modelcontextprotocol/core' is add-core-public-package.md, which created that package. core-internal is private but is versioned via changesets (it appears in pre.json initialVersions and in ~30 existing changesets), so omitting it breaks the dependency-cascade bookkeeping. Additionally, both packages/client/src/index.ts and packages/server/src/index.ts re-export @modelcontextprotocol/core-internal/public, and that barrel now gains ElicitInputFormParams/ElicitInputResult plus the modified ServerContext — so the client package's published type surface changes too, yet client gets no bump from this changeset (and with core-internal omitted, the updateInternalDependencies cascade can't pick it up either).

Why nothing catches it. The changeset-bot only checks that a changeset exists; it does not verify that the named packages match the touched paths. The earlier claude[bot] comment that asked for a changeset itself suggested "@modelcontextprotocol/core and /server", which referred to the pre-rename path layout and likely propagated the wrong package name into this changeset.

Step-by-step proof.

  1. git diff for this PR shows changes under packages/core-internal/, packages/server/, docs, and examples — zero files under packages/core/.
  2. The changeset front-matter lists '@modelcontextprotocol/core': minor and '@modelcontextprotocol/server': minor.
  3. On the next changeset version/release, @modelcontextprotocol/core is published with a bumped version and a changelog entry describing Standard Schema elicitation — a feature that does not exist in that package's contents.
  4. Meanwhile @modelcontextprotocol/client (whose d.ts surface now includes the new exported types and the changed ServerContext.mcpReq.elicitInput overloads via the re-exported public barrel) and @modelcontextprotocol/core-internal (where the change actually lives) get no version bump, so the change is unrecorded for the packages that actually changed.

How to fix. Replace the front-matter with:

---
'@modelcontextprotocol/core-internal': minor
'@modelcontextprotocol/server': minor
'@modelcontextprotocol/client': minor
---

(client could arguably be patch, but minor matches how sibling changesets like add-sdk-http-error.md treat new public type exports.) The changeset body text can stay as-is.

Comment on lines +42 to +47
const registrationSchema = z.object({
username: z.string().min(3).max(20).meta({ title: 'Username', description: 'Your desired username (3-20 characters)' }),
email: z.string().email().meta({ title: 'Email', description: 'Your email address' }),
password: z.string().min(8).meta({ title: 'Password', description: 'Your password (min 8 characters)' }),
newsletter: z.boolean().default(false).meta({ title: 'Newsletter', description: 'Subscribe to newsletter?' })
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The new register_user example (examples/server/src/elicitationFormExample.ts:44) and the new accept-path test in jsonSchemaValidatorOverride.test.ts:138 use z.string().email(), but in Zod v4 the ZodString .email() method is @deprecated in favor of the top-level z.email() helper (which the same test file already uses elsewhere). Since this is brand-new showcase code for the Zod-based elicitation path, switch to z.email().meta({...}) — runtime behavior is identical.

Extended reasoning...

What the issue is. This PR rewrites the register_user example and adds a new accept-path test, both using email: z.string().email(). In the workspace's Zod v4 (catalog ^4.2.0, currently resolving to 4.3.x), ZodString.prototype.email() is explicitly marked /** @deprecated Use z.email() instead. */ in zod/v4/classic/schemas.d.ts (the same applies to .url(), .uuid(), etc.). Zod's v4 docs steer users to the top-level z.email() helper, and the deprecated string-method form is slated for removal in a future major.

Where it appears. Both occurrences are newly introduced by this PR — they are the only usages of z.string().email() in the repo, so this is not following an existing convention:

  • examples/server/src/elicitationFormExample.ts:44email: z.string().email().meta({ title: 'Email', ... }) in the rewritten register_user registration schema.
  • packages/server/test/server/jsonSchemaValidatorOverride.test.ts:138 — the new "accepts a Standard Schema requestedSchema" test.

Notably, the same test file already uses the modern form a little further down: the rejection test uses z.email({ pattern: /@corp\.com$/ }). So the PR is internally inconsistent about which API it models.

Why it matters. These two files are the canonical showcase for the new Standard Schema elicitation path — the example is what users will copy. Teaching the deprecated API surfaces editor strikethrough/lint deprecation warnings for anyone following the example, and would break when Zod removes the method. Since the SDK's docs prose added by this PR (docs/server.md, docs/migration.md) already recommends the Zod v4 idioms (.meta({ title })), the example should model the current API as well.

Why nothing breaks at runtime. Both z.string().email() and z.email() emit identical JSON Schema through standardSchemaToJsonSchemaformat: 'email' plus Zod's canonical email regex as pattern — and that canonical pattern is exactly what the new redundant-pattern exemption in isRedundantFormatPattern (packages/server/src/server/elicitation.ts) accepts. So the wire schema, the strip check, and the accept-path validation behave the same either way. This is purely an API-currency issue, not a functional bug.

Step-by-step. (1) A user copies the register_user example into their project. (2) Their editor shows email() struck through with "Use z.email() instead", and lint rules like @typescript-eslint/no-deprecated flag it. (3) They either ignore the warning (and inherit a removal-pending API) or have to figure out the replacement themselves — neither of which a flagship example should require. The same applies to anyone reading the test as reference usage.

How to fix. One-token change in both places: email: z.email().meta({ title: 'Email', description: 'Your email address' }) in the example, and email: z.email().meta({ title: 'Email', description: 'Email address' }) in the test. The expected wire schema and assertions in the test remain valid unchanged.

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.

Support Zod schema with Elicitation?

2 participants