mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 21:44:18 +02:00
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:
parent
50b23299f8
commit
fe57c3d689
15 changed files with 460 additions and 128 deletions
|
|
@ -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.
|
||||
|
|
|
|||
35
docs/python-sdk/fastmcp-server-middleware-dereference.mdx
Normal file
35
docs/python-sdk/fastmcp-server-middleware-dereference.mdx
Normal 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]
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
78
src/fastmcp/server/middleware/dereference.py
Normal file
78
src/fastmcp/server/middleware/dereference.py
Normal 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
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
136
tests/server/middleware/test_dereference.py
Normal file
136
tests/server/middleware/test_dereference.py
Normal 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"
|
||||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"}]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue