Release line
2.x (main), observed at b2025ab and on #3331.
Bug description
When an MCPServer tool's return type is self-referential, pydantic emits the output schema as {"$defs": {...}, "$ref": "#/$defs/Node"} with no root "type": "object". That's fine on 2026-07-28 sessions, but the 2025-11-25 Tool.outputSchema shape requires type: "object" at the root, so serializing the tools/list result for a legacy-negotiated client fails validation and the client gets an error for the entire listing, not just that tool.
On main this affects recursive BaseModel return types. #3331 hands TypedDict returns to pydantic natively, so recursive TypedDicts join them (previously the hand-built mirror model happened to inline the root).
Steps to reproduce
import anyio
from pydantic import BaseModel
from mcp import Client
from mcp.server.mcpserver import MCPServer
class Node(BaseModel):
name: str
children: list["Node"] = []
mcp = MCPServer("rec")
@mcp.tool()
def tree() -> Node:
return Node(name="root")
@mcp.tool()
def other() -> int:
return 1
async def main():
async with Client(mcp) as c: # 2026-07-28
print(sorted((await c.list_tools()).tools[1].output_schema)) # ['$defs', '$ref']
async with Client(mcp, mode="legacy") as c: # 2025-11-25
await c.list_tools() # raises; server logs "handler for 'tools/list' returned an invalid result"
anyio.run(main)
Server log on the legacy session:
ValidationError: 1 validation error for ListToolsResult
tools.0.outputSchema.type
Field required [type=missing, input_value={'$defs': {'Node': {...}}, '$ref': '#/$defs/Node'}, input_type=dict]
Expected behaviour
tools/list succeeds on both protocol versions; the recursive tool's schema has an object root (e.g. inline the root $ref, or wrap it as {"type": "object", "allOf": [{"$ref": ...}], "$defs": ...}).
Separately, it seems worth deciding whether one tool's unrepresentable schema should fail the whole listing on a legacy session or just drop/degrade that tool.
AI Disclaimer
Release line
2.x (
main), observed at b2025ab and on #3331.Bug description
When an MCPServer tool's return type is self-referential, pydantic emits the output schema as
{"$defs": {...}, "$ref": "#/$defs/Node"}with no root"type": "object". That's fine on 2026-07-28 sessions, but the 2025-11-25Tool.outputSchemashape requirestype: "object"at the root, so serializing thetools/listresult for a legacy-negotiated client fails validation and the client gets an error for the entire listing, not just that tool.On
mainthis affects recursiveBaseModelreturn types. #3331 handsTypedDictreturns to pydantic natively, so recursiveTypedDicts join them (previously the hand-built mirror model happened to inline the root).Steps to reproduce
Server log on the legacy session:
Expected behaviour
tools/listsucceeds on both protocol versions; the recursive tool's schema has an object root (e.g. inline the root$ref, or wrap it as{"type": "object", "allOf": [{"$ref": ...}], "$defs": ...}).Separately, it seems worth deciding whether one tool's unrepresentable schema should fail the whole listing on a legacy session or just drop/degrade that tool.
AI Disclaimer