Skip to content

[v1.x] Resolve tool output-schema references within the schema document only - #3396

Merged
maxisbey merged 1 commit into
v1.xfrom
local-ref-resolution-v1.x
Aug 26, 2026
Merged

[v1.x] Resolve tool output-schema references within the schema document only#3396
maxisbey merged 1 commit into
v1.xfrom
local-ref-resolution-v1.x

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

Backport of #3394 to v1.x.

Passes an explicit empty referencing.Registry to the client's output-schema validation (ClientSession._validate_tool_result), so $refs in a tool's outputSchema resolve within that schema and the bundled metaschemas, per the spec's $ref resolution section. A reference that doesn't resolve there surfaces from call_tool as RuntimeError("Invalid schema for tool …"), the same shape the method already uses for an invalid schema.

Motivation and Context

Same change as on main; see #3394. jsonschema merges its bundled metaschemas into any registry it is given, so #/$defs/…, #/definitions/…, $anchor, embedded $id resources and $refs to the standard metaschema URIs resolve as before.

How Has This Been Tested?

New test in tests/client/test_output_schema_validation.py through the in-memory client session; the existing recursive-$defs server test covers in-document references. Full suite, coverage, pyright and ruff clean locally.

Breaking Changes

No API changes. A result whose validation reaches a $ref outside the schema document now fails with RuntimeError: Invalid schema for tool <name>: …, and a dangling in-document $ref surfaces as that same RuntimeError rather than a referencing exception.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

referencing is jsonschema's own dependency and the type of its registry= parameter; it is imported directly, as on main. The new test uses the file's existing bypass_server_output_validation() helper like its neighbours, and is a plain top-level function per the current test guidelines.

AI Disclaimer

…nt only

Backport of #3394. Pass an explicit empty `referencing.Registry` to the
client's output-schema validation so `$ref`s resolve within the tool's
schema and the bundled metaschemas, and surface a reference that does
not resolve there from `call_tool` as the documented `RuntimeError`.
@maxisbey
maxisbey marked this pull request as ready for review August 26, 2026 13:50

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 2 files

Re-trigger cubic

@claude claude Bot 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.

Beyond the inline findings, I also checked whether the empty Registry could break legitimate in-document references — it doesn't: the schema document remains its own resolution root, so #/$defs/... refs still resolve, and the existing self-referential-model test in tests/server/fastmcp/test_server.py exercises exactly that path through client-side call_tool validation.

Extended reasoning...

The main correctness risk of passing an explicit empty referencing.Registry to jsonschema.validate in ClientSession._validate_tool_result (src/mcp/client/session.py) is regressing schemas that use legitimate in-document $refs (#/$defs/...), which FastMCP itself generates for recursive models. I ruled this out: with the registry= parameter, the schema under validation is still registered as its own root resource, so document-internal pointers and anchors resolve without any external retrieval, and the existing test_tool_structured_output_self_referential_model test validates a #/$defs/Node result end-to-end through the client session, so CI covers that path. The two inline findings (the transitively-supplied referencing import not being a declared dependency, and the inaccurate DeprecationWarning comment in the new test) are minor and do not affect the fix's behavior; the security direction of the change — refusing automatic retrieval of http:/file: refs from server-supplied output schemas — is sound.

Comment thread src/mcp/client/session.py

if output_schema is not None:
from jsonschema import SchemaError, ValidationError, validate
from referencing import Registry

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.

🟡 nit: referencing is now imported at runtime (here and in tests/client/test_output_schema_validation.py) but is not declared in pyproject.toml dependencies — it is only available transitively through jsonschema>=4.20.0, so a future jsonschema release that drops or vendors it would break call_tool output validation with an ImportError. Declare referencing as a direct dependency (e.g. uv add referencing).

Extended reasoning...

src/mcp/client/session.py:431-432 does from referencing import Registry / from referencing.exceptions import Unresolvable inside _validate_tool_result, executed on every call_tool for a tool with an outputSchema. pyproject.toml lists only jsonschema>=4.20.0 (line 37); referencing appears nowhere as a direct dependency. Today jsonschema 4.18+ depends on referencing so the import resolves, which is why tests pass — but the package now has a direct code dependency it does not declare, violating packaging hygiene: if jsonschema pins change (vendoring, extras split) within the allowed >=4.20.0 range, or a downstream resolver installs jsonschema without referencing, _validate_tool_result raises ImportError at call time instead of validating. On the base branch neither src nor tests imported referencing, so this undeclared dependency is introduced by this PR. Fix: add referencing to [project.dependencies] (uv add referencing).

Verification: nit. The factual claims all check out. The diff adds runtime imports of an undeclared package at src/mcp/client/session.py:431-432 (from referencing import Registry / from referencing.exceptions import Unresolvable), executed inside _validate_tool_result on every call_tool for a tool with an output schema, and tests/client/test_output_schema_validation.py:8 also imports it at module level.

Comment on lines +222 to +224
# jsonschema's fallback retriever emits this DeprecationWarning; keep it a plain warning so the
# assertions below decide the outcome rather than the suite's warnings-as-errors filter.
@pytest.mark.filterwarnings("default:Automatically retrieving remote references:DeprecationWarning")

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.

🟡 nit: This comment and filterwarnings mark claim jsonschema's fallback retriever emits the "Automatically retrieving remote references" DeprecationWarning during this test, but that deprecated auto-retrieval only exists on the default registry jsonschema uses when no registry= is passed; with the explicit empty Registry() the validator combines it with the retrieve-less bundled SPECIFICATIONS, so lookup raises Unresolvable without ever invoking a retriever and the warning can never fire — the mark is dead and the comment falsely implies retrieval is still attempted under the fix. Drop both, or reword them as purely defensive.

Extended reasoning...

Path: the client calls jsonschema.validate(..., registry=Registry()) (src/mcp/client/session.py:439-441). In jsonschema 4.25.1, Validator.attrs_post_init checks if registry is not <default>: registry = SPECIFICATIONS.combine(registry); the warning-emitting retrieve function is attached only to the default registry used when no registry kwarg is given (that is how the base branch triggered the warning), and SPECIFICATIONS carries no retrieve function — it cannot, since referencing's Registry.combine raises "conflicting retrieval functions" when both sides define one, which would break the documented pattern of passing a registry with a custom retrieve. So for the file: $ref here, Resolver.lookup -> crawl -> get_or_retrieve hits referencing's _fail_to_retrieve, raising NoSuchResource, wrapped into Unresolvable and then jsonschema's _WrappedReferencingError; no retriever runs and no DeprecationWarning is emitted at any point (check_schema validates the schema as an instance and follows no instance-position refs either). pytest's default:... filterwarnings mark does not require

Verification: nit. The filterwarnings mark and its comment at tests/client/test_output_schema_validation.py:221-223 ("jsonschema's fallback retriever emits this DeprecationWarning; keep it a plain warning...") describe a warning that cannot fire on this test's code path. The client validates via src/mcp/client/session.py: registry: Registry[Any] = Registry() then `validate(result.structuredContent, output_s

@maxisbey
maxisbey merged commit 5ebdfed into v1.x Aug 26, 2026
53 of 54 checks passed
@maxisbey
maxisbey deleted the local-ref-resolution-v1.x branch August 26, 2026 14:11
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