Make $ref dereferencing optional via FastMCP(dereference_refs=...) (#3151)

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
This commit is contained in:
Jeremiah Lowin 2026-02-11 13:45:37 -05:00 committed by GitHub
commit fe57c3d689
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 460 additions and 128 deletions

View file

@ -70,6 +70,16 @@ Background tasks now use a distributed Redis notification queue for reliable del
Auth check functions can now be `async`, enabling authorization decisions that depend on asynchronous operations like reading server state via `Context.get_state` or calling external services ([#3150](https://github.com/jlowin/fastmcp/issues/3150)). Sync and async checks can be freely mixed. Previously, passing an async function as an auth check would silently pass (coroutine objects are truthy).
### Optional `$ref` Dereferencing in Schemas
Schema `$ref` dereferencing — which inlines all `$defs` for compatibility with MCP clients that don't handle `$ref` — is now controlled by the `dereference_schemas` constructor kwarg ([#3141](https://github.com/jlowin/fastmcp/issues/3141)). Default is `True` (dereference on) because the non-compliant clients are popular and the failure mode is silent breakage that server authors can't diagnose. Opt out when you know your clients handle `$ref` and want smaller schemas:
```python
mcp = FastMCP("my-server", dereference_schemas=False)
```
Dereferencing is implemented as middleware (`DereferenceRefsMiddleware`) that runs at serve-time, so schemas are stored with `$ref` intact and only inlined when sent to clients.
### Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed
Sixteen deprecated keyword arguments have been removed from `FastMCP.__init__`. Passing any of them now raises `TypeError` with a migration hint. Environment variables (e.g., `FASTMCP_HOST`) continue to work — only the constructor kwargs moved.

View file

@ -0,0 +1,35 @@
---
title: dereference
sidebarTitle: dereference
---
# `fastmcp.server.middleware.dereference`
Middleware that dereferences $ref in JSON schemas before sending to clients.
## Classes
### `DereferenceRefsMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/dereference.py#L15" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Dereferences $ref in component schemas before sending to clients.
Some MCP clients (e.g., VS Code Copilot) don't handle JSON Schema $ref
properly. This middleware inlines all $ref definitions so schemas are
self-contained. Enabled by default via ``FastMCP(dereference_schemas=True)``.
**Methods:**
#### `on_list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/dereference.py#L24" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_list_tools(self, context: MiddlewareContext[mt.ListToolsRequest], call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
```
#### `on_list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/dereference.py#L33" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_list_resource_templates(self, context: MiddlewareContext[mt.ListResourceTemplatesRequest], call_next: CallNext[mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]]) -> Sequence[ResourceTemplate]
```

View file

@ -26,7 +26,7 @@ Default lifespan context manager that does nothing.
- An empty dictionary as the lifespan result.
### `create_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2049" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2057" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@ -64,49 +64,49 @@ Wrapper for stored context state values.
**Methods:**
#### `name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L345" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
name(self) -> str
```
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L341" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L349" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
instructions(self) -> str | None
```
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L345" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L353" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
instructions(self, value: str | None) -> None
```
#### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L349" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L357" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
version(self) -> str | None
```
#### `website_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L353" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `website_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L361" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
website_url(self) -> str | None
```
#### `icons` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L357" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `icons` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L365" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
icons(self) -> list[mcp.types.Icon]
```
#### `add_middleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L374" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_middleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L382" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_middleware(self, middleware: Middleware) -> None
```
#### `add_provider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L377" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_provider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L385" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_provider(self, provider: Provider) -> None
@ -126,7 +126,7 @@ always take precedence over providers.
- Prompts become "namespace_promptname"
#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L399" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L407" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@ -138,7 +138,7 @@ Overrides AggregateProvider.get_tasks() to apply server-level transforms
after aggregation. AggregateProvider handles provider-level namespacing.
#### `add_transform` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L428" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_transform` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L436" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_transform(self, transform: Transform) -> None
@ -153,7 +153,7 @@ They transform tools, resources, and prompts from ALL providers.
- `transform`: The transform to add.
#### `add_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L448" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L456" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None
@ -165,7 +165,7 @@ Add a tool transformation.
Use ``add_transform(ToolTransform({...}))`` instead.
#### `remove_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L465" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L473" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
remove_tool_transformation(self, _tool_name: str) -> None
@ -177,7 +177,7 @@ Remove a tool transformation.
Tool transformations are now immutable. Use enable/disable controls instead.
#### `list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L480" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L488" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_tools(self) -> Sequence[Tool]
@ -190,7 +190,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L550" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L558" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None
@ -210,7 +210,7 @@ session transforms can override provider-level disables.
- The tool if found and enabled, None otherwise.
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L576" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L584" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources(self) -> Sequence[Resource]
@ -223,7 +223,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L648" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L656" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None
@ -242,7 +242,7 @@ transforms (including session-level) have been applied.
- The resource if found and enabled, None otherwise.
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L673" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L681" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resource_templates(self) -> Sequence[ResourceTemplate]
@ -255,7 +255,7 @@ auth filtering, and middleware execution. Returns all versions (no deduplication
Protocol handlers deduplicate for MCP wire format.
#### `get_resource_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L747" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L755" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None
@ -274,7 +274,7 @@ all transforms (including session-level) have been applied.
- The template if found and enabled, None otherwise.
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L772" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L780" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts(self) -> Sequence[Prompt]
@ -287,7 +287,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L842" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L850" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None
@ -306,19 +306,19 @@ transforms (including session-level) have been applied.
- The prompt if found and enabled, None otherwise.
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L868" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L876" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult
```
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L879" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L887" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
```
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L889" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L897" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult | mcp.types.CreateTaskResult
@ -348,19 +348,19 @@ return ToolResult.
- `ValidationError`: If arguments fail validation
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L985" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L993" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str) -> ResourceResult
```
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L995" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1003" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str) -> mcp.types.CreateTaskResult
```
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1004" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1012" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str) -> ResourceResult | mcp.types.CreateTaskResult
@ -389,19 +389,19 @@ return ResourceResult.
- `ResourceError`: If resource read fails
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1138" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1146" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult
```
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1157" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
```
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1167" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult | mcp.types.CreateTaskResult
@ -431,7 +431,7 @@ return PromptResult.
- `PromptError`: If prompt rendering fails
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1235" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1243" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
@ -449,7 +449,7 @@ with the Context type annotation. See the @tool decorator for examples.
- The tool instance that was added to the server.
#### `remove_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1249" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1257" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
remove_tool(self, name: str, version: str | None = None) -> None
@ -465,19 +465,19 @@ Remove tool(s) from the server.
- `NotFoundError`: If no matching tool is found.
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1269" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1277" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: AnyFunction) -> FunctionTool
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1290" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1298" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1310" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1318" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
@ -533,7 +533,7 @@ server.tool(my_function, name="custom_name")
```
#### `add_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1409" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1417" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate
@ -548,7 +548,7 @@ Add a resource to the server.
- The resource instance that was added to the server.
#### `add_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1422" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1430" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_template(self, template: ResourceTemplate) -> ResourceTemplate
@ -563,7 +563,7 @@ Add a resource template to the server.
- The template instance that was added to the server.
#### `resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1433" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1441" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]
@ -622,7 +622,7 @@ async def get_weather(city: str) -> str:
```
#### `add_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1555" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1563" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt
@ -637,19 +637,19 @@ Add a prompt to the server.
- The prompt instance that was added to the server.
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1567" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1575" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt
```
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1583" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1591" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt]
```
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1598" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1606" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]
@ -726,7 +726,7 @@ Decorator to register a prompt.
```
#### `mount` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1698" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `mount` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1706" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, as_proxy: bool | None = None, tool_names: dict[str, str] | None = None, prefix: str | None = None) -> None
@ -773,7 +773,7 @@ mounted server.
- `prefix`: Deprecated. Use namespace instead.
#### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1792" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1800" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None) -> None
@ -814,7 +814,7 @@ templates, and prompts are imported with their original names.
objects are imported with their original names.
#### `from_openapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1892" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_openapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1900" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, validate_output: bool = True, **settings: Any) -> Self
@ -843,7 +843,7 @@ response structure while still returning structured JSON.
- A FastMCP server with an OpenAPIProvider attached.
#### `from_fastapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1943" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_fastapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1951" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> Self
@ -867,7 +867,7 @@ Use this to configure timeout and other client settings.
- A FastMCP server with an OpenAPIProvider attached.
#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1998" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2006" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@ -885,7 +885,7 @@ instance or any value accepted as the `transport` argument of
`fastmcp.client.Client` constructor.
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2035" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2043" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_name(cls, name: str | None = None) -> str

View file

@ -60,17 +60,12 @@ the referenced definition while preserving $defs for nested references.
### `compress_schema` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/json_schema.py#L364" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
compress_schema(schema: dict[str, Any], prune_params: list[str] | None = None, prune_additional_properties: bool = False, prune_titles: bool = False) -> dict[str, Any]
compress_schema(schema: dict[str, Any], prune_params: list[str] | None = None, prune_additional_properties: bool = False, prune_titles: bool = False, dereference: bool = False) -> dict[str, Any]
```
Compress and optimize a JSON schema for MCP compatibility.
This function dereferences all $ref entries (inlining definitions) to ensure
compatibility with MCP clients that don't properly handle $ref in schemas
(e.g., VS Code Copilot). It also applies various optimizations to reduce
schema size.
**Args:**
- `schema`: The schema to compress
- `prune_params`: List of parameter names to remove from properties
@ -78,4 +73,7 @@ schema size.
Defaults to False to maintain MCP client compatibility, as some clients
(e.g., Claude) require additionalProperties\: false for strict validation.
- `prune_titles`: Whether to remove title fields from the schema
- `dereference`: Whether to dereference $ref by inlining definitions.
Defaults to False; dereferencing is typically handled by
middleware at serve-time instead.

View file

@ -175,6 +175,12 @@ By default, FastMCP converts Python functions into MCP tools by inspecting the f
<Note>
FastMCP automatically dereferences `$ref` entries in tool schemas to ensure compatibility with MCP clients that don't fully support JSON Schema references (e.g., VS Code Copilot, Claude Desktop). This means complex Pydantic models with shared types are inlined in the schema rather than using `$defs` references.
Dereferencing happens at serve-time via middleware, so your schemas are stored with `$ref` intact and only inlined when sent to clients. If you know your clients handle `$ref` correctly and prefer smaller schemas, you can opt out:
```python
mcp = FastMCP("my-server", dereference_schemas=False)
```
</Note>
### Type Annotations

View file

@ -0,0 +1,78 @@
"""Middleware that dereferences $ref in JSON schemas before sending to clients."""
from collections.abc import Sequence
from typing import Any
import mcp.types as mt
from typing_extensions import override
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
from fastmcp.tools.tool import Tool
from fastmcp.utilities.json_schema import dereference_refs
class DereferenceRefsMiddleware(Middleware):
"""Dereferences $ref in component schemas before sending to clients.
Some MCP clients (e.g., VS Code Copilot) don't handle JSON Schema $ref
properly. This middleware inlines all $ref definitions so schemas are
self-contained. Enabled by default via ``FastMCP(dereference_schemas=True)``.
"""
@override
async def on_list_tools(
self,
context: MiddlewareContext[mt.ListToolsRequest],
call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
) -> Sequence[Tool]:
tools = await call_next(context)
return [_dereference_tool(tool) for tool in tools]
@override
async def on_list_resource_templates(
self,
context: MiddlewareContext[mt.ListResourceTemplatesRequest],
call_next: CallNext[
mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]
],
) -> Sequence[ResourceTemplate]:
templates = await call_next(context)
return [_dereference_resource_template(t) for t in templates]
def _dereference_tool(tool: Tool) -> Tool:
"""Return a copy of the tool with dereferenced schemas."""
updates: dict[str, object] = {}
if "$defs" in tool.parameters or _has_ref(tool.parameters):
updates["parameters"] = dereference_refs(tool.parameters)
if tool.output_schema is not None and (
"$defs" in tool.output_schema or _has_ref(tool.output_schema)
):
updates["output_schema"] = dereference_refs(tool.output_schema)
if updates:
return tool.model_copy(update=updates)
return tool
def _dereference_resource_template(template: ResourceTemplate) -> ResourceTemplate:
"""Return a copy of the template with dereferenced schemas."""
if "$defs" in template.parameters or _has_ref(template.parameters):
return template.model_copy(
update={"parameters": dereference_refs(template.parameters)}
)
return template
def _has_ref(schema: dict[str, Any]) -> bool:
"""Check if a schema contains any $ref."""
if "$ref" in schema:
return True
for value in schema.values():
if isinstance(value, dict) and _has_ref(value):
return True
if isinstance(value, list):
for item in value:
if isinstance(item, dict) and _has_ref(item):
return True
return False

View file

@ -229,6 +229,7 @@ class FastMCP(
tools: Sequence[Tool | Callable[..., Any]] | None = None,
on_duplicate: DuplicateBehavior | None = None,
mask_error_details: bool | None = None,
dereference_schemas: bool = True,
strict_input_validation: bool | None = None,
list_page_size: int | None = None,
tasks: bool | None = None,
@ -322,6 +323,13 @@ class FastMCP(
self.middleware: list[Middleware] = list(middleware or [])
if dereference_schemas:
from fastmcp.server.middleware.dereference import (
DereferenceRefsMiddleware,
)
self.middleware.append(DereferenceRefsMiddleware())
# Set up MCP protocol handlers
self._setup_handlers()

View file

@ -366,15 +366,11 @@ def compress_schema(
prune_params: list[str] | None = None,
prune_additional_properties: bool = False,
prune_titles: bool = False,
dereference: bool = False,
) -> dict[str, Any]:
"""
Compress and optimize a JSON schema for MCP compatibility.
This function dereferences all $ref entries (inlining definitions) to ensure
compatibility with MCP clients that don't properly handle $ref in schemas
(e.g., VS Code Copilot). It also applies various optimizations to reduce
schema size.
Args:
schema: The schema to compress
prune_params: List of parameter names to remove from properties
@ -382,22 +378,27 @@ def compress_schema(
Defaults to False to maintain MCP client compatibility, as some clients
(e.g., Claude) require additionalProperties: false for strict validation.
prune_titles: Whether to remove title fields from the schema
dereference: Whether to dereference $ref by inlining definitions.
Defaults to False; dereferencing is typically handled by
middleware at serve-time instead.
"""
# Dereference $ref - this inlines all definitions and removes $defs
# Required for MCP client compatibility
schema = dereference_refs(schema)
if dereference:
schema = dereference_refs(schema)
# Resolve root-level $ref for MCP spec compliance (requires type: object at root)
schema = resolve_root_ref(schema)
# Remove specific parameters if requested
for param in prune_params or []:
schema = _prune_param(schema, param=param)
# Apply combined optimizations in a single tree traversal
if prune_titles or prune_additional_properties:
schema = _single_pass_optimize(
schema,
prune_titles=prune_titles,
prune_additional_properties=prune_additional_properties,
prune_defs=False,
)
# Apply combined optimizations in a single tree traversal.
# Always prune unused $defs to keep schemas clean after parameter removal.
schema = _single_pass_optimize(
schema,
prune_titles=prune_titles,
prune_additional_properties=prune_additional_properties,
prune_defs=True,
)
return schema

View file

@ -288,7 +288,7 @@ class TestResponseCachingMiddlewareIntegration:
request: pytest.FixtureRequest,
):
"""Create a FastMCP server for caching tests."""
mcp = FastMCP("CachingTestServer")
mcp = FastMCP("CachingTestServer", dereference_schemas=False)
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir:
disk_store: DiskStore = DiskStore(directory=temp_dir)

View file

@ -0,0 +1,136 @@
"""Tests for DereferenceRefsMiddleware."""
from enum import Enum
import pydantic
from fastmcp import Client, FastMCP
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
class PaintRequest(pydantic.BaseModel):
color: Color
opacity: float = 1.0
class TestDereferenceRefsMiddleware:
"""End-to-end tests for the dereference_schemas server kwarg."""
async def test_dereference_schemas_true_inlines_refs(self):
"""With dereference_schemas=True (default), tool schemas have $ref inlined."""
mcp = FastMCP("test", dereference_schemas=True)
@mcp.tool
def paint(request: PaintRequest) -> str:
return "ok"
async with Client(mcp) as client:
tools = await client.list_tools()
schema = tools[0].inputSchema
# $defs should be removed — everything inlined
assert "$defs" not in schema
# The Color enum should be inlined into the request property
assert "$ref" not in str(schema)
async def test_dereference_schemas_false_preserves_refs(self):
"""With dereference_schemas=False, $ref and $defs are preserved."""
mcp = FastMCP("test", dereference_schemas=False)
@mcp.tool
def paint(request: PaintRequest) -> str:
return "ok"
async with Client(mcp) as client:
tools = await client.list_tools()
schema = tools[0].inputSchema
# $defs should still be present
assert "$defs" in schema
async def test_default_is_true(self):
"""Default behavior dereferences $ref."""
mcp = FastMCP("test")
@mcp.tool
def paint(request: PaintRequest) -> str:
return "ok"
async with Client(mcp) as client:
tools = await client.list_tools()
schema = tools[0].inputSchema
assert "$defs" not in schema
async def test_does_not_mutate_original_tool(self):
"""Middleware should not mutate the shared Tool object."""
mcp = FastMCP("test", dereference_schemas=True)
@mcp.tool
def paint(request: PaintRequest) -> str:
return "ok"
# Get the original tool's parameters before middleware runs
original_tools = await mcp._local_provider._list_tools()
assert "$defs" in original_tools[0].parameters
# List tools through the client (triggers middleware)
async with Client(mcp) as client:
await client.list_tools()
# The original tool stored in the server should still have $defs
tools_after = await mcp._local_provider._list_tools()
assert "$defs" in tools_after[0].parameters
async def test_output_schema_dereferenced(self):
"""Middleware also dereferences output_schema when present."""
mcp = FastMCP("test", dereference_schemas=True)
@mcp.tool
def paint(request: PaintRequest) -> PaintRequest:
return request
async with Client(mcp) as client:
tools = await client.list_tools()
tool = tools[0]
# Both input and output schemas should be dereferenced
assert "$defs" not in tool.inputSchema
if tool.outputSchema is not None:
assert "$defs" not in tool.outputSchema
async def test_resource_templates_dereferenced(self):
"""Middleware dereferences resource template schemas."""
mcp = FastMCP("test", dereference_schemas=True)
@mcp.resource("paint://{color}")
def get_paint(color: Color) -> str:
return f"paint: {color}"
async with Client(mcp) as client:
templates = await client.list_resource_templates()
# Resource templates also get their schemas dereferenced
# (only if the template parameters have $ref)
assert len(templates) == 1
async def test_no_ref_schemas_unchanged(self):
"""Tools without $ref should pass through unmodified."""
mcp = FastMCP("test", dereference_schemas=True)
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
async with Client(mcp) as client:
tools = await client.list_tools()
schema = tools[0].inputSchema
# Simple schema should not have $defs regardless
assert "$defs" not in schema
assert schema["properties"]["a"]["type"] == "integer"

View file

@ -196,9 +196,8 @@ class TestToolFromFunction:
"description": "Create a new user.",
"tags": set(),
"parameters": {
"additionalProperties": False,
"properties": {
"user": {
"$defs": {
"UserInput": {
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
@ -206,6 +205,10 @@ class TestToolFromFunction:
"required": ["name", "age"],
"type": "object",
},
},
"additionalProperties": False,
"properties": {
"user": {"$ref": "#/$defs/UserInput"},
"flag": {"type": "boolean"},
},
"required": ["user", "flag"],

View file

@ -346,8 +346,8 @@ class TestInputSchema:
def test_merge_schema_with_defs_precedence(self):
"""Test _merge_schema_with_precedence merges $defs correctly.
Note: This tests the raw merge behavior before dereferencing.
The final schema output will be dereferenced by compress_schema.
Note: compress_schema no longer dereferences $ref by default.
Used definitions are kept in $defs; unused definitions are pruned.
"""
base_schema = {
"type": "object",
@ -374,13 +374,17 @@ class TestInputSchema:
# SharedType should no longer be present on the schema (unused)
assert "SharedType" not in transformed_tool_schema.get("$defs", {})
# Schema is dereferenced so no $defs in final output
# $ref and $defs are preserved for used definitions
assert transformed_tool_schema == snapshot(
{
"type": "object",
"properties": {
"field1": {"type": "string", "description": "base"},
"field2": {"type": "boolean"},
"field1": {"$ref": "#/$defs/BaseType"},
"field2": {"$ref": "#/$defs/OverrideType"},
},
"$defs": {
"BaseType": {"type": "string", "description": "base"},
"OverrideType": {"type": "boolean"},
},
"required": [],
"additionalProperties": False,
@ -390,8 +394,8 @@ class TestInputSchema:
def test_transform_tool_with_complex_defs_pruning(self):
"""Test that tool transformation properly handles hidden params.
With schema dereferencing, unused types are automatically removed
since $defs is eliminated entirely.
Unused type definitions are pruned from $defs when their
corresponding parameters are hidden. Used types remain as $ref.
"""
class UsedType(BaseModel):
@ -411,18 +415,21 @@ class TestInputSchema:
complex_tool, transform_args={"unused_param": ArgTransform(hide=True)}
)
# Schema is dereferenced - no $defs
assert "$defs" not in transformed_tool.parameters
# UnusedType should be pruned from $defs, but UsedType remains
assert "UnusedType" not in transformed_tool.parameters.get("$defs", {})
assert transformed_tool.parameters == snapshot(
{
"type": "object",
"properties": {
"used_param": {
"used_param": {"$ref": "#/$defs/UsedType"},
},
"$defs": {
"UsedType": {
"properties": {"value": {"type": "string"}},
"required": ["value"],
"type": "object",
}
},
},
"required": ["used_param"],
"additionalProperties": False,
@ -430,7 +437,7 @@ class TestInputSchema:
)
def test_transform_with_custom_function_preserves_needed_types(self):
"""Test that custom transform functions preserve necessary types inline."""
"""Test that custom transform functions preserve necessary type definitions."""
class InputType(BaseModel):
data: str
@ -452,18 +459,19 @@ class TestInputSchema:
transform_args={"input_data": ArgTransform(name="renamed_input")},
)
# Schema is dereferenced - types are inlined
assert "$defs" not in transformed.parameters
# Used type definitions are preserved as $ref/$defs
assert transformed.parameters == snapshot(
{
"type": "object",
"properties": {
"renamed_input": {
"renamed_input": {"$ref": "#/$defs/InputType"},
},
"$defs": {
"InputType": {
"properties": {"data": {"type": "string"}},
"required": ["data"],
"type": "object",
}
},
},
"required": ["renamed_input"],
"additionalProperties": False,
@ -471,7 +479,7 @@ class TestInputSchema:
)
def test_chained_transforms_inline_types(self):
"""Test that chained transformations produce correct inlined schemas."""
"""Test that chained transformations produce correct schemas with $ref/$defs."""
class TypeA(BaseModel):
a: str
@ -492,19 +500,23 @@ class TestInputSchema:
transform_args={"param_c": ArgTransform(hide=True, default=TypeC(c=True))},
)
# Schema is dereferenced - types are inlined
assert "$defs" not in transform1.parameters
# TypeC should be pruned from $defs, TypeA and TypeB remain
assert "TypeC" not in transform1.parameters.get("$defs", {})
assert transform1.parameters == snapshot(
{
"type": "object",
"properties": {
"param_a": {
"param_a": {"$ref": "#/$defs/TypeA"},
"param_b": {"$ref": "#/$defs/TypeB"},
},
"$defs": {
"TypeA": {
"properties": {"a": {"type": "string"}},
"required": ["a"],
"type": "object",
},
"param_b": {
"TypeB": {
"properties": {"b": {"type": "integer"}},
"required": ["b"],
"type": "object",
@ -521,17 +533,21 @@ class TestInputSchema:
transform_args={"param_b": ArgTransform(hide=True, default=TypeB(b=42))},
)
assert "$defs" not in transform2.parameters
# TypeB should be pruned from $defs, only TypeA remains
assert "TypeB" not in transform2.parameters.get("$defs", {})
assert transform2.parameters == snapshot(
{
"type": "object",
"properties": {
"param_a": {
"param_a": {"$ref": "#/$defs/TypeA"},
},
"$defs": {
"TypeA": {
"properties": {"a": {"type": "string"}},
"required": ["a"],
"type": "object",
}
},
},
"required": ["param_a"],
"additionalProperties": False,

View file

@ -205,10 +205,11 @@ async def test_hidden_param_prunes_defs():
schema = new_tool.parameters
# Only 'a' should be visible
assert list(schema["properties"].keys()) == ["a"]
# Schema should be fully dereferenced (no $defs)
assert "$defs" not in schema
# VisibleType should be inlined in the property
assert schema["properties"]["a"] == {
# HiddenType should be pruned from $defs
assert "HiddenType" not in schema.get("$defs", {})
# VisibleType should remain in $defs and be referenced via $ref
assert schema["properties"]["a"] == {"$ref": "#/$defs/VisibleType"}
assert schema["$defs"]["VisibleType"] == {
"properties": {"x": {"type": "integer"}},
"required": ["x"],
"type": "object",
@ -396,10 +397,8 @@ def test_transform_args_with_parent_defaults():
new_tool = Tool.from_tool(tool)
# Both tools should have the same dereferenced schema
# Both tools should have the same schema (with $ref/$defs preserved)
assert new_tool.parameters == tool.parameters
# Schema should be fully dereferenced (no $defs)
assert "$defs" not in new_tool.parameters
def test_transform_args_validation_unknown_arg(add_tool):

View file

@ -581,7 +581,7 @@ class TestEdgeCases:
) # Should have some properties from one of the content types
def test_oneof_reference_dereferenced(self):
"""Test that schemas referenced in oneOf are dereferenced."""
"""Test that schemas referenced in oneOf are preserved and unused defs pruned."""
schema = {
"type": "object",
@ -594,14 +594,15 @@ class TestEdgeCases:
result = compress_schema(schema)
# $defs should be removed (all refs dereferenced)
assert "$defs" not in result
# UnusedSchema should be pruned, TestSchema should be kept
assert "UnusedSchema" not in result.get("$defs", {})
assert result["$defs"]["TestSchema"] == {"type": "string"}
# TestSchema should be inlined in oneOf
assert result["properties"]["data"]["oneOf"] == [{"type": "string"}]
# $ref should be preserved in oneOf
assert result["properties"]["data"]["oneOf"] == [{"$ref": "#/$defs/TestSchema"}]
def test_anyof_reference_dereferenced(self):
"""Test that schemas referenced in anyOf are dereferenced."""
"""Test that schemas referenced in anyOf are preserved and unused defs pruned."""
schema = {
"type": "object",
@ -614,14 +615,15 @@ class TestEdgeCases:
result = compress_schema(schema)
# $defs should be removed (all refs dereferenced)
assert "$defs" not in result
# UnusedSchema should be pruned, TestSchema should be kept
assert "UnusedSchema" not in result.get("$defs", {})
assert result["$defs"]["TestSchema"] == {"type": "string"}
# TestSchema should be inlined in anyOf
assert result["properties"]["data"]["anyOf"] == [{"type": "string"}]
# $ref should be preserved in anyOf
assert result["properties"]["data"]["anyOf"] == [{"$ref": "#/$defs/TestSchema"}]
def test_allof_reference_dereferenced(self):
"""Test that schemas referenced in allOf are dereferenced."""
"""Test that schemas referenced in allOf are preserved and unused defs pruned."""
schema = {
"type": "object",
@ -634,8 +636,9 @@ class TestEdgeCases:
result = compress_schema(schema)
# $defs should be removed (all refs dereferenced)
assert "$defs" not in result
# UnusedSchema should be pruned, TestSchema should be kept
assert "UnusedSchema" not in result.get("$defs", {})
assert result["$defs"]["TestSchema"] == {"type": "string"}
# TestSchema should be inlined in allOf
assert result["properties"]["data"]["allOf"] == [{"type": "string"}]
# $ref should be preserved in allOf
assert result["properties"]["data"]["allOf"] == [{"$ref": "#/$defs/TestSchema"}]

View file

@ -196,8 +196,8 @@ class TestDereferenceRefs:
class TestCompressSchema:
"""Tests for the compress_schema function."""
def test_dereferences_by_default(self):
"""Test that compress_schema dereferences $refs by default."""
def test_preserves_refs_by_default(self):
"""Test that compress_schema preserves $refs by default."""
schema = {
"properties": {
"foo": {"$ref": "#/$defs/foo_def"},
@ -208,10 +208,9 @@ class TestCompressSchema:
}
result = compress_schema(schema)
# $ref should be inlined
assert result["properties"]["foo"] == {"type": "string"}
# $defs should be removed
assert "$defs" not in result
# $ref should be preserved (dereferencing is handled by middleware)
assert result["properties"]["foo"] == {"$ref": "#/$defs/foo_def"}
assert "$defs" in result
def test_prune_params(self):
"""Test pruning parameters with compress_schema."""
@ -271,7 +270,7 @@ class TestCompressSchema:
assert "remove" not in result["properties"]
# Check that required list was updated
assert result["required"] == ["keep"]
# Check that $defs was removed (dereferenced)
# All $defs entries are now unreferenced after pruning "remove", so they're cleaned up
assert "$defs" not in result
# Check that additionalProperties was removed
assert "additionalProperties" not in result
@ -442,6 +441,46 @@ class TestCompressSchema:
)
class TestCompressSchemaDereference:
"""Tests for the dereference parameter of compress_schema."""
SCHEMA_WITH_REFS = {
"properties": {
"foo": {"$ref": "#/$defs/foo_def"},
},
"$defs": {
"foo_def": {"type": "string"},
},
}
def test_dereference_true_inlines_refs(self):
result = compress_schema(self.SCHEMA_WITH_REFS, dereference=True)
assert result["properties"]["foo"] == {"type": "string"}
assert "$defs" not in result
def test_dereference_false_preserves_refs(self):
result = compress_schema(self.SCHEMA_WITH_REFS, dereference=False)
assert result["properties"]["foo"] == {"$ref": "#/$defs/foo_def"}
assert "$defs" in result
def test_other_optimizations_still_apply_without_dereference(self):
schema = {
"properties": {
"foo": {"$ref": "#/$defs/foo_def"},
"bar": {"type": "integer", "title": "Bar"},
},
"$defs": {
"foo_def": {"type": "string"},
},
}
result = compress_schema(
schema, dereference=False, prune_params=["bar"], prune_titles=True
)
assert "bar" not in result["properties"]
assert "$ref" in result["properties"]["foo"]
assert "$defs" in result
class TestResolveRootRef:
"""Tests for the resolve_root_ref function.