Skip to content

fix(core-internal): use an already-converted JSON Schema document directly - #3138

Open
BlueX888 wants to merge 1 commit into
modelcontextprotocol:mainfrom
BlueX888:contrib/zod-preconverted-json-schema-direct-use
Open

BlueX888 wants to merge 1 commit into
modelcontextprotocol:mainfrom
BlueX888:contrib/zod-preconverted-json-schema-direct-use

Conversation

@BlueX888

Copy link
Copy Markdown

Summary

registerTool() / registerPrompt() schemas (and elicitation requestedSchema) that are themselves already-converted JSON Schema documents — most prominently the output of zod's toJSONSchema(), which retains a ~standard prop whose converters reference the original zod schema — were re-converted through those converters on every tools/list and toolInputSchemaJson() call, instead of being used directly (#3132).

standardSchemaToJsonSchema() now detects a JSON Schema document by its own $schema / properties keywords and uses it as-is, before consulting ~standard.jsonSchema. Live Standard Schemas cannot be misdetected this way: zod schemas carry def, valibot uses entries, and ArkType nodes are functions — none expose $schema or properties at the root.

Fixes #3132

Behavior notes

  • For a toJSONSchema() document the advertised schema is now the document itself. For example, additionalProperties: false that zod emits is preserved as registered — matching the documented fromJsonSchema() contract — where the previous re-conversion path dropped it.
  • The same direct use applies to elicitation requestedSchema: a document carrying root-level constraints elicitation does not support (e.g. additionalProperties) now fails with the documented unsupported-constraint error — the same treatment a raw JSON Schema already receives — where the previous re-conversion silently dropped them.
  • Documents with neither $schema nor properties (e.g. an openapi-3.0-style scalar root) keep the previous best-effort re-conversion behavior.

Testing

  • packages/core-internal/test/util/standardSchema.test.ts: a toJSONSchema() document is used directly with zero converter invocations; a live zod schema still converts exactly once.
  • packages/server/test/server/lazyToolSchemaConversion.test.ts: the reporter's scenario — registerTool with a pre-converted document, then toolInputSchemaJson() and tools/list, each with zero converter invocations.
  • Full core-internal and server suites pass (the two pre-existing core-internal conformance failures are identical on unpatched main); tsgo, eslint and prettier are clean. The reporter's 1000-tool MRE drops from ~7.8 ms to ~1.9 ms of conversion work.

Zod's toJSONSchema() retains a ~standard prop on its output whose
converters re-run the conversion from the original zod schema, so such a
document passed to registerTool()/registerPrompt() was re-converted on
every tools/list and toolInputSchemaJson() call (modelcontextprotocol#3132). A schema
carrying $schema or properties is now recognized as a document and used
as-is; live schemas never carry those keywords (zod: def, valibot:
entries, ArkType nodes are functions).
@BlueX888
BlueX888 requested a review from a team as a code owner October 31, 2026 06:59
@changeset-bot

changeset-bot Bot commented Oct 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 26564b0

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-internal Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch

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 Oct 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/core

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 26564b0

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Oct 31, 2026
*/
function isJsonSchemaDocument(schema: StandardJSONSchemaV1): boolean {
const { $schema, properties } = schema as { $schema?: unknown; properties?: unknown };
if (typeof $schema === 'string') return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I am new to this library so this my not be relevant but would it also be worthwhile validating that the $schema matches JSON_SCHEMA_CONVERSION_TARGET. If so this function may be more suitably named isCompatibleJsonSchemaDocument.

@BlueX888

BlueX888 commented Nov 1, 2026

Copy link
Copy Markdown
Author

Thanks for the review. On the $schema point: the documented way to register a raw JSON Schema document is fromJsonSchema(), and it deliberately passes the document through as registered without any dialect validation (jsonSchema.input returns the schema unchanged). This fast path aims to treat a toJSONSchema() output exactly like that — the same document, used directly.

Validating $schema === draft-2020-12 here would make this path stricter than fromJsonSchema for the same document, and would send other-dialect documents (e.g. draft-07) back through ~standard.jsonSchema, which regenerates at the conversion target and ignores the declared dialect — a behavior change beyond this fix. So the check stays structural: is the input itself a JSON Schema document (own $schema string or properties object)? The name follows that — it detects document-ness, not compatibility. If dialect normalization across all JSON-Schema entry points is wanted, that looks like a separate, broader change for the maintainers to weigh in on.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v2] Unnecessary zod schema conversion

2 participants