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

This commit is contained in:
Max 2026-08-26 15:11:38 +01:00 • committed by GitHub
commit 5ebdfed406
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 1 deletions

View file

@ -428,17 +428,24 @@ class ClientSession(
if output_schema is not None:
from jsonschema import SchemaError, ValidationError, validate
from referencing import Registry
from referencing.exceptions import Unresolvable
if result.structuredContent is None:
raise RuntimeError(
f"Tool {name} has an output schema but did not return structured content"
) # pragma: no cover
# An explicit empty registry: `$ref`s resolve within the schema document and the bundled metaschemas.
registry: Registry[Any] = Registry()
try:
validate(result.structuredContent, output_schema)
validate(result.structuredContent, output_schema, registry=registry)
except ValidationError as e:
raise RuntimeError(f"Invalid structured content returned by tool {name}: {e}") # pragma: no cover
except SchemaError as e: # pragma: no cover
raise RuntimeError(f"Invalid schema for tool {name}: {e}") # pragma: no cover
except Unresolvable as e:
# A `$ref` did not resolve within the schema document.
raise RuntimeError(f"Invalid schema for tool {name}: {e}") from e
@overload
@deprecated("Use list_prompts(params=PaginatedRequestParams(...)) instead")

View file

@ -1,9 +1,11 @@
import logging
from contextlib import contextmanager
from pathlib import Path
from typing import Any
from unittest.mock import patch
import pytest
from referencing.exceptions import Unresolvable
from mcp.server.lowlevel import Server
from mcp.shared.memory import (
@ -215,3 +217,34 @@ class TestClientOutputSchemaValidation:
# Check that warning was logged
assert "Tool mystery_tool not listed" in caplog.text
# 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")
@pytest.mark.anyio
async def test_output_schema_ref_outside_the_document_is_rejected(tmp_path: Path):
"""A `$ref` to a URI outside the output schema is not resolved, and a result whose validation
reaches one fails as an invalid schema (spec `$ref` resolution; applying it to `file:` URIs too
is SDK-defined)."""
target = tmp_path / "schema.json"
target.write_text("{}", encoding="utf-8")
server = Server("test-server")
@server.list_tools()
async def list_tools():
return [
Tool(name="probe", description="", inputSchema={"type": "object"}, outputSchema={"$ref": target.as_uri()})
]
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]):
return {"v": 1}
with bypass_server_output_validation():
async with client_session(server) as client:
with pytest.raises(RuntimeError) as exc_info:
await client.call_tool("probe", {})
# SDK-authored prefix only; the tail is `referencing`'s text.
assert str(exc_info.value).startswith("Invalid schema for tool probe: ")
assert isinstance(exc_info.value.__cause__, Unresolvable)