Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 16 additions & 20 deletions docs/servers/providers/transforms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ Think of transforms as filters in a pipeline. Components flow from providers thr
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.
When listing components, transforms receive sequences and return transformed sequences—a pure function pattern. When getting a specific component by name, transforms use a middleware pattern with `call_next`, working in reverse: mapping the client's requested name back to the original, then transforming the result.

## Namespace

Expand Down Expand Up @@ -347,7 +345,7 @@ Create custom transforms by subclassing `Transform` and overriding the methods y

```python
from collections.abc import Sequence
from fastmcp.server.transforms import Transform, ListToolsNext, GetToolNext
from fastmcp.server.transforms import Transform, GetToolNext
from fastmcp.tools.tool import Tool

class TagFilter(Transform):
Expand All @@ -356,8 +354,7 @@ class TagFilter(Transform):
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()
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
return [t for t in tools if t.tags & self.required_tags]

async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:
Expand All @@ -369,28 +366,27 @@ class TagFilter(Transform):

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:
Each component type has two methods with different patterns:

| 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 |
| Method | Pattern | Purpose |
|--------|---------|---------|
| `list_tools(tools)` | Pure function | Transform the sequence of tools |
| `get_tool(name, call_next)` | Middleware | Transform lookup by name |
| `list_resources(resources)` | Pure function | Transform the sequence of resources |
| `get_resource(uri, call_next)` | Middleware | Transform lookup by URI |
| `list_resource_templates(templates)` | Pure function | Transform the sequence of templates |
| `get_resource_template(uri, call_next)` | Middleware | Transform template lookup by URI |
| `list_prompts(prompts)` | Pure function | Transform the sequence of prompts |
| `get_prompt(name, call_next)` | Middleware | 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()`.
List methods receive sequences directly and return transformed sequences. Get methods use `call_next` for routing flexibility—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()
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
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:
Expand Down
97 changes: 24 additions & 73 deletions src/fastmcp/server/providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,23 +136,18 @@ def wrap_transform(self, transform: Transform) -> Provider:
async def list_tools(self) -> Sequence[Tool]:
"""List tools with all transforms applied.

Builds a middleware chain: base → transforms (in order).
Each transform wraps the previous via call_next.
Applies transforms sequentially: base → transforms (in order).
Each transform receives the result from the previous transform.
Components may be marked as disabled but are NOT filtered here -
filtering happens at the server level to allow session transforms to override.

Returns:
Transformed sequence of tools (including disabled ones).
"""

async def base() -> Sequence[Tool]:
return await self._list_tools()

chain = base
tools = await self._list_tools()
for transform in self.transforms:
chain = partial(transform.list_tools, call_next=chain)

return await chain()
tools = await transform.list_tools(tools)
return tools

async def get_tool(
self, name: str, version: VersionSpec | None = None
Expand Down Expand Up @@ -185,15 +180,10 @@ async def list_resources(self) -> Sequence[Resource]:

Components may be marked as disabled but are NOT filtered here.
"""

async def base() -> Sequence[Resource]:
return await self._list_resources()

chain = base
resources = await self._list_resources()
for transform in self.transforms:
chain = partial(transform.list_resources, call_next=chain)

return await chain()
resources = await transform.list_resources(resources)
return resources

async def get_resource(
self, uri: str, version: VersionSpec | None = None
Expand Down Expand Up @@ -225,15 +215,10 @@ async def list_resource_templates(self) -> Sequence[ResourceTemplate]:

Components may be marked as disabled but are NOT filtered here.
"""

async def base() -> Sequence[ResourceTemplate]:
return await self._list_resource_templates()

chain = base
templates = await self._list_resource_templates()
for transform in self.transforms:
chain = partial(transform.list_resource_templates, call_next=chain)

return await chain()
templates = await transform.list_resource_templates(templates)
return templates

async def get_resource_template(
self, uri: str, version: VersionSpec | None = None
Expand Down Expand Up @@ -267,15 +252,10 @@ async def list_prompts(self) -> Sequence[Prompt]:

Components may be marked as disabled but are NOT filtered here.
"""

async def base() -> Sequence[Prompt]:
return await self._list_prompts()

chain = base
prompts = await self._list_prompts()
for transform in self.transforms:
chain = partial(transform.list_prompts, call_next=chain)

return await chain()
prompts = await transform.list_prompts(prompts)
return prompts

async def get_prompt(
self, name: str, version: VersionSpec | None = None
Expand Down Expand Up @@ -456,50 +436,21 @@ async def get_tasks(self) -> Sequence[FastMCPComponent]:
templates = cast(Sequence[ResourceTemplate], results[2])
prompts = cast(Sequence[Prompt], results[3])

# Apply provider's own transforms to components using the chain pattern
# For tasks, we need the fully-transformed names, so use the list_ chain
# Note: We build mini-chains for each component type

async def tools_base() -> Sequence[Tool]:
return tools

async def resources_base() -> Sequence[Resource]:
return resources

async def templates_base() -> Sequence[ResourceTemplate]:
return templates

async def prompts_base() -> Sequence[Prompt]:
return prompts

# Apply transforms in order
tools_chain = tools_base
resources_chain = resources_base
templates_chain = templates_base
prompts_chain = prompts_base

# Apply provider's own transforms sequentially
# For tasks, we need the fully-transformed names
for transform in self.transforms:
tools_chain = partial(transform.list_tools, call_next=tools_chain)
resources_chain = partial(
transform.list_resources, call_next=resources_chain
)
templates_chain = partial(
transform.list_resource_templates, call_next=templates_chain
)
prompts_chain = partial(transform.list_prompts, call_next=prompts_chain)

transformed_tools = await tools_chain()
transformed_resources = await resources_chain()
transformed_templates = await templates_chain()
transformed_prompts = await prompts_chain()
tools = await transform.list_tools(tools)
resources = await transform.list_resources(resources)
templates = await transform.list_resource_templates(templates)
prompts = await transform.list_prompts(prompts)

return [
c
for c in [
*transformed_tools,
*transformed_resources,
*transformed_templates,
*transformed_prompts,
*tools,
*resources,
*templates,
*prompts,
]
if c.task_config.supports_tasks()
]
Expand Down
41 changes: 9 additions & 32 deletions src/fastmcp/server/providers/fastmcp_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import re
from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
from functools import partial
from typing import TYPE_CHECKING, Any, overload

import mcp.types
Expand Down Expand Up @@ -640,43 +639,21 @@ async def get_tasks(self) -> Sequence[FastMCPComponent]:
templates = [c for c in components if isinstance(c, ResourceTemplate)]
prompts = [c for c in components if isinstance(c, Prompt)]

# Apply this provider's transforms using call_next pattern

async def tools_base() -> Sequence[Tool]:
return tools

async def resources_base() -> Sequence[Resource]:
return resources

async def templates_base() -> Sequence[ResourceTemplate]:
return templates

async def prompts_base() -> Sequence[Prompt]:
return prompts

tools_chain = tools_base
resources_chain = resources_base
templates_chain = templates_base
prompts_chain = prompts_base

# Apply this provider's transforms sequentially
for transform in self.transforms:
tools_chain = partial(transform.list_tools, call_next=tools_chain)
resources_chain = partial(
transform.list_resources, call_next=resources_chain
)
templates_chain = partial(
transform.list_resource_templates, call_next=templates_chain
)
prompts_chain = partial(transform.list_prompts, call_next=prompts_chain)
tools = await transform.list_tools(tools)
resources = await transform.list_resources(resources)
templates = await transform.list_resource_templates(templates)
prompts = await transform.list_prompts(prompts)

# Filter to only task-eligible components (same as base Provider)
return [
c
for c in [
*await tools_chain(),
*await resources_chain(),
*await templates_chain(),
*await prompts_chain(),
*tools,
*resources,
*templates,
*prompts,
]
if c.task_config.supports_tasks()
]
Expand Down
45 changes: 9 additions & 36 deletions src/fastmcp/server/providers/wrapped_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
from functools import partial
from typing import TYPE_CHECKING

from fastmcp.server.providers.base import Provider
Expand Down Expand Up @@ -112,46 +111,20 @@ async def get_tasks(self) -> Sequence[FastMCPComponent]:
templates = [c for c in components if isinstance(c, ResourceTemplate)]
prompts = [c for c in components if isinstance(c, Prompt)]

async def tools_base() -> Sequence[Tool]:
return tools

async def resources_base() -> Sequence[Resource]:
return resources

async def templates_base() -> Sequence[ResourceTemplate]:
return templates

async def prompts_base() -> Sequence[Prompt]:
return prompts

# Apply this wrapper's transforms
tools_chain = tools_base
resources_chain = resources_base
templates_chain = templates_base
prompts_chain = prompts_base

# Apply this wrapper's transforms sequentially
for transform in self.transforms:
tools_chain = partial(transform.list_tools, call_next=tools_chain)
resources_chain = partial(
transform.list_resources, call_next=resources_chain
)
templates_chain = partial(
transform.list_resource_templates, call_next=templates_chain
)
prompts_chain = partial(transform.list_prompts, call_next=prompts_chain)

transformed_tools = await tools_chain()
transformed_resources = await resources_chain()
transformed_templates = await templates_chain()
transformed_prompts = await prompts_chain()
tools = await transform.list_tools(tools)
resources = await transform.list_resources(resources)
templates = await transform.list_resource_templates(templates)
prompts = await transform.list_prompts(prompts)

return [
c
for c in [
*transformed_tools,
*transformed_resources,
*transformed_templates,
*transformed_prompts,
*tools,
*resources,
*templates,
*prompts,
]
if c.task_config.supports_tasks()
]
Expand Down
44 changes: 9 additions & 35 deletions src/fastmcp/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,44 +535,18 @@ async def get_tasks(self) -> Sequence[FastMCPComponent]:
templates = [c for c in components if isinstance(c, ResourceTemplate)]
prompts = [c for c in components if isinstance(c, Prompt)]

# Apply server-level transforms using call_next pattern
async def tools_base() -> Sequence[Tool]:
return tools

async def resources_base() -> Sequence[Resource]:
return resources

async def templates_base() -> Sequence[ResourceTemplate]:
return templates

async def prompts_base() -> Sequence[Prompt]:
return prompts

tools_chain = tools_base
resources_chain = resources_base
templates_chain = templates_base
prompts_chain = prompts_base

# Apply server-level transforms sequentially
for transform in self.transforms:
tools_chain = partial(transform.list_tools, call_next=tools_chain)
resources_chain = partial(
transform.list_resources, call_next=resources_chain
)
templates_chain = partial(
transform.list_resource_templates, call_next=templates_chain
)
prompts_chain = partial(transform.list_prompts, call_next=prompts_chain)

transformed_tools = await tools_chain()
transformed_resources = await resources_chain()
transformed_templates = await templates_chain()
transformed_prompts = await prompts_chain()
tools = await transform.list_tools(tools)
resources = await transform.list_resources(resources)
templates = await transform.list_resource_templates(templates)
prompts = await transform.list_prompts(prompts)

return [
*transformed_tools,
*transformed_resources,
*transformed_templates,
*transformed_prompts,
*tools,
*resources,
*templates,
*prompts,
]

def add_transform(self, transform: Transform) -> None:
Expand Down
Loading