fastmcp/docs/servers/providers/transforms.mdx
Jeremiah Lowin 07d89c4038
Add transform system for modifying components in provider chains (#2836)
* Consolidate tool transformation logic into TransformingProvider

Tool transformations were previously scattered across LocalProvider,
ProxyProvider, and MCPConfig. This consolidates all transformation
logic into TransformingProvider via with_transforms(tool_transforms={...}).

- Add tool_transforms parameter to TransformingProvider
- Add tool_transforms to Provider.with_transforms()
- Remove transformation storage from LocalProvider and ProxyProvider
- Remove add_tool_transformation() and remove_tool_transformation() from FastMCP
- Add tool_transforms parameter to factory methods (from_openapi, from_fastapi, create_proxy)
- Update tests to use new patterns

* Fix: reject tool lookups by pre-transform name

* Add collision validation for tool_transforms and fix docstring examples

- Validate duplicate target names in tool_transforms raise ValueError
- Fix docstring examples to use arguments/ArgTransformConfig (not args/ArgTransform)
- Add test for collision validation

* Add server-level tool transform APIs and fix task registration

- Add AggregateProvider to present multiple providers as one
- Add _get_root_provider() to apply server-level transforms uniformly
- Fix _docket_lifespan to use root provider (ensures renamed tools
  register with correct keys for background execution)
- Add tool_transforms kwarg to __init__ (non-deprecated)
- Add add_tool_transform(), remove_tool_transform(), tool_transforms property
- Deprecate old API names (tool_transformations, add_tool_transformation, etc.)
- Update tests to use new API

* Add graceful degradation for provider errors in AggregateProvider

* Match original behavior: parallel queries with DEBUG logging

* Refactor transforms to middleware-style call_next pattern

Replaces the ad-hoc transformation system with a unified Transform
abstraction using the same call_next pattern as server middleware.

Key changes:
- New src/fastmcp/server/transforms/ module with Transform base class
- Namespace, ToolTransform, Visibility all implement the same interface
- Transforms compose via functools.partial chain building
- Visibility is now just the first transform in provider._transforms
- Server-level transforms apply after provider aggregation
- Task registration now applies full transform chain

Removes TransformingProvider, _BoundTransform, ComponentSource protocol.
User-facing API unchanged: mount(), add_transform(), enable/disable all
work as before.

* Add comprehensive transforms and visibility documentation

New docs/servers/providers/transforms.mdx covering:
- Mental model for middleware-style transform pattern
- Built-in transforms (Namespace, ToolTransform)
- Server vs provider-level transforms and ordering
- Tool modification (immediate vs deferred)
- Custom transform creation

New docs/servers/visibility.mdx covering:
- Enable/disable API for runtime visibility control
- Keys and tags for targeting components
- Allowlist mode with only=True
- Server vs provider visibility layering

Updates existing docs to reference new pages and simplifies
redundant content. Visibility is documented as a user feature,
not as an implementation detail.

* Restructure transforms docs and delete tool-transformation pattern

* Cleanup: simplify get_tasks and remove unused Provider.get_component

* Update loq

* Update loq limits and add loq note to AGENTS.md

* Deprecate add_tool_transformation and tool_transformations param

* Address PR review feedback: remove redundant imports, fix path reference

* Add missing imports to code examples in v3-features.mdx
2026-01-12 22:11:16 -05:00

405 lines
13 KiB
Text

---
title: Transforms
sidebarTitle: Transforms
description: Modify components as they flow through your server
icon: wand-magic-sparkles
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.0.0" />
Transforms modify components as they flow from providers to clients. When a client asks "what tools do you have?", the request passes through each transform in the chain. Each transform can modify the components before passing them along.
## Mental Model
Think of transforms as filters in a pipeline. Components flow from providers through transforms to reach clients:
```
Provider → [Transform A] → [Transform B] → Client
```
When listing components, transforms see the original components and can modify them. When getting a specific component by name, transforms work in reverse: mapping the client's requested name back to the original, then transforming the result.
Each transform uses a middleware-style pattern with `call_next`. The transform receives a function that invokes the next stage in the chain. The transform can call `call_next()` to get components from downstream, then modify the results before returning them.
## Namespace
The `Namespace` transform prefixes all component names, preventing conflicts when composing multiple servers.
Tools and prompts receive an underscore-separated prefix. Resources and templates receive a path-segment prefix in their URIs.
| Component | Original | With `Namespace("api")` |
|-----------|----------|-------------------------|
| Tool | `my_tool` | `api_my_tool` |
| Prompt | `my_prompt` | `api_my_prompt` |
| Resource | `data://info` | `data://api/info` |
| Template | `data://{id}` | `data://api/{id}` |
The most common use is through the `mount()` method's `namespace` parameter.
```python
from fastmcp import FastMCP
weather = FastMCP("Weather")
calendar = FastMCP("Calendar")
@weather.tool
def get_data() -> str:
return "Weather data"
@calendar.tool
def get_data() -> str:
return "Calendar data"
# Without namespacing, these would conflict
main = FastMCP("Main")
main.mount(weather, namespace="weather")
main.mount(calendar, namespace="calendar")
# Clients see: weather_get_data, calendar_get_data
```
You can also apply namespacing directly using the `Namespace` transform.
```python
from fastmcp import FastMCP
from fastmcp.server.transforms import Namespace
mcp = FastMCP("Server")
@mcp.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
# Namespace all components
mcp.add_transform(Namespace("api"))
# Tool is now: api_greet
```
## Tool Transformation
Tool transformation lets you modify tool schemas - renaming tools, changing descriptions, adjusting tags, and reshaping argument schemas. FastMCP provides two mechanisms that share the same configuration options but differ in timing.
**Deferred transformation** with `ToolTransform` applies modifications when tools flow through a transform chain. Use this for tools from mounted servers, proxies, or other providers where you don't control the source directly.
**Immediate transformation** with `Tool.from_tool()` creates a modified tool object right away. Use this when you have direct access to a tool and want to transform it before registration.
### ToolTransform
The `ToolTransform` class is a transform that modifies tools as they flow through a provider. Provide a dictionary mapping original tool names to their transformation configuration.
```python
from fastmcp import FastMCP
from fastmcp.server.transforms import ToolTransform
from fastmcp.tools.tool_transform import ToolTransformConfig
mcp = FastMCP("Server")
@mcp.tool
def verbose_internal_data_fetcher(query: str) -> str:
"""Fetches data from the internal database."""
return f"Results for: {query}"
# Rename the tool to something simpler
mcp.add_transform(ToolTransform({
"verbose_internal_data_fetcher": ToolTransformConfig(
name="search",
description="Search the database.",
)
}))
# Clients see "search" with the cleaner description
```
`ToolTransform` is useful when you want to modify tools from mounted or proxied servers without changing the original source.
### Tool.from_tool()
Use `Tool.from_tool()` when you have the tool object and want to create a transformed version for registration.
```python
from fastmcp import FastMCP
from fastmcp.tools import Tool, tool
from fastmcp.tools.tool_transform import ArgTransform
# Create a tool without registering it
@tool
def search(q: str, limit: int = 10) -> list[str]:
"""Search for items."""
return [f"Result {i} for {q}" for i in range(limit)]
# Transform it before registration
better_search = Tool.from_tool(
search,
name="find_items",
description="Find items matching your search query.",
transform_args={
"q": ArgTransform(
name="query",
description="The search terms to look for.",
),
},
)
mcp = FastMCP("Server")
mcp.add_tool(better_search)
```
The standalone `@tool` decorator (from `fastmcp.tools`) creates a Tool object without registering it to any server. This separates creation from registration, letting you transform tools before deciding where they go.
### Modification Options
Both mechanisms support the same modifications.
**Tool-level options:**
| Option | Description |
|--------|-------------|
| `name` | New name for the tool |
| `description` | New description |
| `title` | Human-readable title |
| `tags` | Set of tags for categorization |
| `annotations` | MCP ToolAnnotations |
| `meta` | Custom metadata dictionary |
**Argument-level options** (via `ArgTransform` or `ArgTransformConfig`):
| Option | Description |
|--------|-------------|
| `name` | Rename the argument |
| `description` | New description for the argument |
| `default` | New default value |
| `default_factory` | Callable that generates a default (requires `hide=True`) |
| `hide` | Remove from client-visible schema |
| `required` | Make an optional argument required |
| `type` | Change the argument's type |
| `examples` | Example values for the argument |
### Hiding Arguments
Hide arguments to simplify the interface or inject values the client shouldn't control.
```python
from fastmcp.tools.tool_transform import ArgTransform
# Hide with a constant value
transform_args = {
"api_key": ArgTransform(hide=True, default="secret-key"),
}
# Hide with a dynamic value
import uuid
transform_args = {
"request_id": ArgTransform(hide=True, default_factory=lambda: str(uuid.uuid4())),
}
```
Hidden arguments disappear from the tool's schema. The client never sees them, but the underlying function receives the configured value.
<Warning>
`default_factory` requires `hide=True`. Visible arguments need static defaults that can be represented in JSON Schema.
</Warning>
### Renaming Arguments
Rename arguments to make them more intuitive for LLMs or match your API conventions.
```python
from fastmcp.tools import Tool, tool
from fastmcp.tools.tool_transform import ArgTransform
@tool
def search(q: str, n: int = 10) -> list[str]:
"""Search for items."""
return []
better_search = Tool.from_tool(
search,
transform_args={
"q": ArgTransform(name="query", description="Search terms"),
"n": ArgTransform(name="max_results", description="Maximum results to return"),
},
)
```
### Custom Transform Functions
For advanced scenarios, provide a `transform_fn` that intercepts tool execution. The function can validate inputs, modify outputs, or add custom logic while still calling the original tool via `forward()`.
```python
from fastmcp import FastMCP
from fastmcp.tools import Tool, tool
from fastmcp.tools.tool_transform import forward, ArgTransform
@tool
def divide(a: float, b: float) -> float:
"""Divide a by b."""
return a / b
async def safe_divide(numerator: float, denominator: float) -> float:
if denominator == 0:
raise ValueError("Cannot divide by zero")
return await forward(numerator=numerator, denominator=denominator)
safe_division = Tool.from_tool(
divide,
name="safe_divide",
transform_fn=safe_divide,
transform_args={
"a": ArgTransform(name="numerator"),
"b": ArgTransform(name="denominator"),
},
)
mcp = FastMCP("Server")
mcp.add_tool(safe_division)
```
The `forward()` function handles argument mapping automatically. Call it with the transformed argument names, and it maps them back to the original function's parameters.
For direct access to the original function without mapping, use `forward_raw()` with the original parameter names.
## Server vs Provider Transforms
Transforms can be added at two levels, each serving different purposes.
### Provider-Level Transforms
Provider transforms apply to components from a specific provider. They run first, modifying components before they reach the server level.
```python
from fastmcp import FastMCP
from fastmcp.server.providers import FastMCPProvider
from fastmcp.server.transforms import Namespace, ToolTransform
from fastmcp.tools.tool_transform import ToolTransformConfig
sub_server = FastMCP("Sub")
@sub_server.tool
def process(data: str) -> str:
return f"Processed: {data}"
# Create provider and add transforms
provider = FastMCPProvider(sub_server)
provider.add_transform(Namespace("api"))
provider.add_transform(ToolTransform({
"api_process": ToolTransformConfig(description="Process data through the API"),
}))
main = FastMCP("Main", providers=[provider])
# Tool is now: api_process with updated description
```
When using `mount()`, the returned provider reference lets you add transforms directly.
```python
main = FastMCP("Main")
mount = main.mount(sub_server, namespace="api")
mount.add_transform(ToolTransform({...}))
```
### Server-Level Transforms
Server transforms apply to all components from all providers. They run after provider transforms, seeing the already-transformed names.
```python
from fastmcp import FastMCP
from fastmcp.server.transforms import Namespace
mcp = FastMCP("Server")
@mcp.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
mcp.add_transform(Namespace("v1"))
# All tools become v1_toolname
```
Server-level transforms are useful for API versioning or applying consistent naming across your entire server.
### Transform Order
Transforms stack in the order they're added. The first transform added is innermost (closest to the provider), and subsequent transforms wrap it.
```python
from fastmcp.server.providers import FastMCPProvider
from fastmcp.server.transforms import Namespace, ToolTransform
from fastmcp.tools.tool_transform import ToolTransformConfig
provider = FastMCPProvider(server)
provider.add_transform(Namespace("api")) # Applied first
provider.add_transform(ToolTransform({ # Sees namespaced names
"api_verbose_name": ToolTransformConfig(name="short"),
}))
# Flow: "verbose_name" -> "api_verbose_name" -> "short"
```
When a client requests "short", the transforms reverse the mapping: ToolTransform maps "short" to "api_verbose_name", then Namespace strips the prefix to find "verbose_name" in the provider.
## Custom Transforms
Create custom transforms by subclassing `Transform` and overriding the methods you need.
```python
from collections.abc import Sequence
from fastmcp.server.transforms import Transform, ListToolsNext, GetToolNext
from fastmcp.tools.tool import Tool
class TagFilter(Transform):
"""Filter tools to only those with specific tags."""
def __init__(self, required_tags: set[str]):
self.required_tags = required_tags
async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]:
tools = await call_next()
return [t for t in tools if t.tags & self.required_tags]
async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:
tool = await call_next(name)
if tool and tool.tags & self.required_tags:
return tool
return None
```
The `Transform` base class provides default implementations that pass through unchanged. Override only the methods relevant to your transform.
Each component type has two methods:
| Method | Purpose |
|--------|---------|
| `list_tools(call_next)` | Transform the list of all tools |
| `get_tool(name, call_next)` | Transform lookup by name |
| `list_resources(call_next)` | Transform the list of all resources |
| `get_resource(uri, call_next)` | Transform lookup by URI |
| `list_resource_templates(call_next)` | Transform the list of all templates |
| `get_resource_template(uri, call_next)` | Transform template lookup by URI |
| `list_prompts(call_next)` | Transform the list of all prompts |
| `get_prompt(name, call_next)` | Transform lookup by name |
For get methods that change names, you must implement the reverse mapping. When a client requests "new_name", your transform maps it back to "original_name" before calling `call_next()`.
```python
class PrefixTransform(Transform):
def __init__(self, prefix: str):
self.prefix = prefix
async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]:
tools = await call_next()
return [t.model_copy(update={"name": f"{self.prefix}_{t.name}"}) for t in tools]
async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:
# Reverse the prefix to find the original
if not name.startswith(f"{self.prefix}_"):
return None
original = name[len(self.prefix) + 1:]
tool = await call_next(original)
if tool:
return tool.model_copy(update={"name": name})
return None
```