Skip to content

fix: recognize object output schemas behind local references - #3035

Open
skulitom wants to merge 2 commits into
modelcontextprotocol:mainfrom
skulitom:codex/fix-referenced-output-schema
Open

skulitom wants to merge 2 commits into
modelcontextprotocol:mainfrom
skulitom:codex/fix-referenced-output-schema

Conversation

@skulitom

@skulitom skulitom commented Oct 15, 2026 •

Copy link
Copy Markdown

Fixes #3032.

On a 2025-11-25 connection, an object output schema expressed through a local root $ref is classified as non-object, so the compatibility conversion returns { result: { id, body } }. The equivalent inline schema returns { id, body }.

Follow same-document JSON Pointers when determining whether an output schema is constrained to objects, preserving the reference and definitions. Require object type constraints through references while retaining the existing inline object-keyword compatibility behavior. Cache completed reference classifications and keep path-local cycle detection so shared definition graphs do not cause exponential traversal. Leave unresolved, external, anchor, and nested-resource reference cases conservatively classified. No public API or dependency changes.

The 39 added cases cover reference chains, escaped names, recursive objects, shared graphs, cycles, missing targets, nested resources, and typeless targets, plus client/server HTTP exchanges on 2025-11-25 and 2026-07-28. The HTTP cases check inline and referenced objects, primitive wrapping, typeless schemas that accept strings, and rejection of invalid object results. The release note describes the corrected legacy output shape.

Validation

  • All 14 GitHub checks pass on 135384a, including the Node 20/22/24 main and end-to-end matrices, Bun/Deno, client/server conformance, build, and examples.
  • All 43 focused regression cases pass. The original bug was reproduced before the fix; review regressions also failed before their corrections (9 classification cases and both HTTP cases).
  • A deterministic 22-level shared-graph test bounds reference lookups without relying on timing. A separate 16-level reproduction drops from 131,071 reference lookups to 33 with caching.
  • Linux/WSL, Node 24.19.0: all 4,579 main-suite tests pass; the end-to-end suite passes with 2,695 passing and 141 existing expected-failure cases.
  • Windows, Node 24.19.0: 3,520 tests pass across core-internal, server, client, and integration.
  • Workspace builds and typechecks, changed-package lint/formatting, documentation build, snippets/skills checks, and ESM/CommonJS published-type smoke checks pass.
  • Independently checked the issue's Zod 4.6.7 .meta({ id }) reproduction: published SDK 2.3.1 exhibits the wrapper; the patched build returns the object directly for named and inline schemas on both protocols. The committed regression uses fromJsonSchema so the repository's pinned Zod version can remain unchanged.

Local check limitation: check:all locally stops on two import/no-extraneous-dependencies errors for vitest in test/helpers/src/helpers/http.ts:5 and oauth.ts:2. Both were reproduced on pristine base 9bc40e903fde15534ec06ff4c1347d47000dbe16 with the same dependency installation. The affected packages' lint and the documentation check pass separately; no unrelated changes are included to suppress these errors. GitHub runs the full aggregate checks on each revision (current build job).

AI disclosure

The reproduction, implementation, tests, and PR/issue text were primarily written with Codex, which also ran the validation above. No independent human review is claimed.

@skulitom
skulitom requested a review from a team as a code owner October 15, 2026 14:20
@changeset-bot

changeset-bot Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 135384a

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T14:28:37.559254Z c8d3cda PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/core

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 135384a

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8d3cdabaf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

function isProvablyObjectShapedRoot(schema: Record<string, unknown>, root = schema, ancestors: ReadonlySet<object> = new Set()): boolean {
if (schema.type !== undefined) return schema.type === 'object';
if (ancestors.has(schema) || (schema !== root && schema.$id !== undefined)) return false;
const seen = new Set(ancestors).add(schema);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Cache completed reference classifications

When a valid definition DAG has multiple composition members referencing the same next definition, seen only records the current ancestor path, so every branch recursively reclassifies the entire shared subtree. The work doubles at each level (a 22-level schema with two identical refs per anyOf took about 17 seconds locally), allowing a small generated output schema to block synchronous conversion and thus tools/list; retain path-local cycle detection but memoize completed classifications by target.

Useful? React with 👍 / 👎.

const seen = new Set(ancestors).add(schema);
if (schema.$ref !== undefined) {
const target = resolveLocalSchemaReference(root, schema.$ref);
return target !== undefined && isProvablyObjectShapedRoot(target, root, seen);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require referenced targets to constrain values to objects

When a local $ref targets a typeless schema containing properties, required, or additionalProperties, this recursion treats that keyword as proof of an object even though JSON Schema ignores those keywords for non-object instances. For example, a target { additionalProperties: false } accepts a string, but conversion now stamps the root as type: 'object'; on a 2025 connection projectCallToolResult wraps that valid string by value while tools/list advertises the stamped schema, and the resulting { result: ... } violates additionalProperties: false. Following a reference should therefore require a true object type constraint rather than reuse the loose inline compatibility heuristic.

Useful? React with 👍 / 👎.

@karaaslanz karaaslanz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Independent review of the new head 135384af8692a1ecd778a10fec30c3126d5a2212, specifically the two findings from the earlier Codex review of c8d3cda.

I fetched the exact packages/core-internal/src/util/standardSchema.ts blob (3434e81970c685f59dbd006ba25d79bdcb4b0c06), executed the actual isProvablyObjectShapedRoot and resolveLocalSchemaReference functions with Node 24's TypeScript-strip transform, and checked seven isolated cases:

  • named object output through a local root $ref -> recognized;
  • referenced typeless required and additionalProperties:false targets -> not misclassified as objects;
  • a chained reference -> recognized;
  • direct cycle and nullable object -> conservatively left unclassified;
  • 22-level shared-definition DAG with two references per level -> 45 reference reads, well below the 100-access bound (roughly 0.24 ms in this isolated run).

7/7 assertions passed, and the source blob was SHA-checked. The new completedReferences memoization appears to resolve the exponential traversal example, while requireObjectType addresses the typeless-reference counterexamples. No repo files were modified.

This is an isolated, exact-source helper verification, not a full SDK, cross-era HTTP, or published-package test. I am not asserting that all JSON Schema semantic corner cases are covered. The regression cases already added to this PR are appropriate for the two previous findings.

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Oct 16, 2026

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] outputSchema whose root is a $ref (zod .meta({ id })) is taken for a non-object: on 2025 connections an object result is wrapped as { result }

2 participants