Refactor transform list methods to pure function pattern (#2942)

This commit is contained in:
Jeremiah Lowin 2026-01-19 16:21:35 -05:00 committed by GitHub
commit 4d2feb0c29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 143 additions and 334 deletions

View file

@ -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
@ -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):
@ -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:
@ -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:

View file

@ -136,23 +136,18 @@ class 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
@ -185,15 +180,10 @@ class Provider:
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
@ -225,15 +215,10 @@ class Provider:
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
@ -267,15 +252,10 @@ class Provider:
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
@ -456,50 +436,21 @@ class Provider:
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()
]

View file

@ -13,7 +13,6 @@ from __future__ import annotations
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
@ -640,43 +639,21 @@ class FastMCPProvider(Provider):
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()
]

View file

@ -8,7 +8,6 @@ from __future__ import annotations
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
@ -112,46 +111,20 @@ class _WrappedProvider(Provider):
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()
]

View file

@ -535,44 +535,18 @@ class FastMCP(
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:

View file

@ -1,8 +1,8 @@
"""Transform system for component transformations.
Transforms modify components (tools, resources, prompts) using a middleware pattern.
Each transform wraps the next in the chain via `call_next`, allowing transforms to
intercept, modify, or replace component queries.
Transforms modify components (tools, resources, prompts). List operations use a pure
function pattern where transforms receive sequences and return transformed sequences.
Get operations use a middleware pattern with `call_next` to chain lookups.
Unlike middleware (which operates on requests), transforms are observable by the
system for task registration, tag filtering, and component introspection.
@ -20,7 +20,7 @@ Example:
from __future__ import annotations
from collections.abc import Awaitable, Callable, Sequence
from collections.abc import Awaitable, Sequence
from typing import TYPE_CHECKING, Protocol
from fastmcp.utilities.versions import VersionSpec
@ -31,13 +31,6 @@ if TYPE_CHECKING:
from fastmcp.resources.template import ResourceTemplate
from fastmcp.tools.tool import Tool
# Type aliases for call_next signatures
# List methods are simple callables
ListToolsNext = Callable[[], Awaitable[Sequence["Tool"]]]
ListResourcesNext = Callable[[], Awaitable[Sequence["Resource"]]]
ListResourceTemplatesNext = Callable[[], Awaitable[Sequence["ResourceTemplate"]]]
ListPromptsNext = Callable[[], Awaitable[Sequence["Prompt"]]]
# Get methods use Protocol to express keyword-only version parameter
class GetToolNext(Protocol):
@ -75,19 +68,15 @@ class GetPromptNext(Protocol):
class Transform:
"""Base class for component transformations.
Transforms use a middleware pattern with `call_next` to chain operations.
Each transform can intercept, modify, or pass through component queries.
For list operations, call `call_next()` to get components from downstream,
then transform the result. For get operations, optionally transform the
name/uri before calling `call_next`, then transform the result.
List operations use a pure function pattern: transforms receive sequences
and return transformed sequences. Get operations use a middleware pattern
with `call_next` to chain lookups.
Example:
```python
class MyTransform(Transform):
async def list_tools(self, call_next):
tools = await call_next() # Get tools from downstream
return [transform(t) for t in tools] # Transform them
async def list_tools(self, tools):
return [transform(t) for t in tools] # Transform sequence
async def get_tool(self, name, call_next, *, version=None):
original = self.reverse_name(name) # Map to original name
@ -103,16 +92,16 @@ class Transform:
# Tools
# -------------------------------------------------------------------------
async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]:
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
"""List tools with transformation applied.
Args:
call_next: Callable to get tools from downstream transforms/provider.
tools: Sequence of tools to transform.
Returns:
Transformed sequence of tools.
"""
return await call_next()
return tools
async def get_tool(
self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
@ -133,16 +122,16 @@ class Transform:
# Resources
# -------------------------------------------------------------------------
async def list_resources(self, call_next: ListResourcesNext) -> Sequence[Resource]:
async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:
"""List resources with transformation applied.
Args:
call_next: Callable to get resources from downstream transforms/provider.
resources: Sequence of resources to transform.
Returns:
Transformed sequence of resources.
"""
return await call_next()
return resources
async def get_resource(
self,
@ -168,17 +157,17 @@ class Transform:
# -------------------------------------------------------------------------
async def list_resource_templates(
self, call_next: ListResourceTemplatesNext
self, templates: Sequence[ResourceTemplate]
) -> Sequence[ResourceTemplate]:
"""List resource templates with transformation applied.
Args:
call_next: Callable to get templates from downstream transforms/provider.
templates: Sequence of resource templates to transform.
Returns:
Transformed sequence of resource templates.
"""
return await call_next()
return templates
async def get_resource_template(
self,
@ -203,16 +192,16 @@ class Transform:
# Prompts
# -------------------------------------------------------------------------
async def list_prompts(self, call_next: ListPromptsNext) -> Sequence[Prompt]:
async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
"""List prompts with transformation applied.
Args:
call_next: Callable to get prompts from downstream transforms/provider.
prompts: Sequence of prompts to transform.
Returns:
Transformed sequence of prompts.
"""
return await call_next()
return prompts
async def get_prompt(
self, name: str, call_next: GetPromptNext, *, version: VersionSpec | None = None
@ -242,10 +231,6 @@ __all__ = [
"GetResourceNext",
"GetResourceTemplateNext",
"GetToolNext",
"ListPromptsNext",
"ListResourceTemplatesNext",
"ListResourcesNext",
"ListToolsNext",
"Namespace",
"ToolTransform",
"Transform",

View file

@ -19,10 +19,6 @@ from fastmcp.server.transforms import (
GetResourceNext,
GetResourceTemplateNext,
GetToolNext,
ListPromptsNext,
ListResourcesNext,
ListResourceTemplatesNext,
ListToolsNext,
Transform,
)
from fastmcp.utilities.versions import VersionSpec
@ -195,9 +191,8 @@ class Enabled(Transform):
# Transform methods (mark components, don't filter)
# -------------------------------------------------------------------------
async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]:
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
"""Mark tools by enabled state."""
tools = await call_next()
return [self._mark_component(t) for t in tools]
async def get_tool(
@ -213,9 +208,8 @@ class Enabled(Transform):
# Resources
# -------------------------------------------------------------------------
async def list_resources(self, call_next: ListResourcesNext) -> Sequence[Resource]:
async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:
"""Mark resources by enabled state."""
resources = await call_next()
return [self._mark_component(r) for r in resources]
async def get_resource(
@ -236,10 +230,9 @@ class Enabled(Transform):
# -------------------------------------------------------------------------
async def list_resource_templates(
self, call_next: ListResourceTemplatesNext
self, templates: Sequence[ResourceTemplate]
) -> Sequence[ResourceTemplate]:
"""Mark resource templates by enabled state."""
templates = await call_next()
return [self._mark_component(t) for t in templates]
async def get_resource_template(
@ -259,9 +252,8 @@ class Enabled(Transform):
# Prompts
# -------------------------------------------------------------------------
async def list_prompts(self, call_next: ListPromptsNext) -> Sequence[Prompt]:
async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
"""Mark prompts by enabled state."""
prompts = await call_next()
return [self._mark_component(p) for p in prompts]
async def get_prompt(

View file

@ -11,10 +11,6 @@ from fastmcp.server.transforms import (
GetResourceNext,
GetResourceTemplateNext,
GetToolNext,
ListPromptsNext,
ListResourcesNext,
ListResourceTemplatesNext,
ListToolsNext,
Transform,
)
from fastmcp.utilities.versions import VersionSpec
@ -98,9 +94,8 @@ class Namespace(Transform):
# Tools
# -------------------------------------------------------------------------
async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]:
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
"""Prefix tool names with namespace."""
tools = await call_next()
return [
t.model_copy(update={"name": self._transform_name(t.name)}) for t in tools
]
@ -121,9 +116,8 @@ class Namespace(Transform):
# Resources
# -------------------------------------------------------------------------
async def list_resources(self, call_next: ListResourcesNext) -> Sequence[Resource]:
async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:
"""Add namespace path segment to resource URIs."""
resources = await call_next()
return [
r.model_copy(update={"uri": self._transform_uri(str(r.uri))})
for r in resources
@ -150,10 +144,9 @@ class Namespace(Transform):
# -------------------------------------------------------------------------
async def list_resource_templates(
self, call_next: ListResourceTemplatesNext
self, templates: Sequence[ResourceTemplate]
) -> Sequence[ResourceTemplate]:
"""Add namespace path segment to template URIs."""
templates = await call_next()
return [
t.model_copy(update={"uri_template": self._transform_uri(t.uri_template)})
for t in templates
@ -181,9 +174,8 @@ class Namespace(Transform):
# Prompts
# -------------------------------------------------------------------------
async def list_prompts(self, call_next: ListPromptsNext) -> Sequence[Prompt]:
async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
"""Prefix prompt names with namespace."""
prompts = await call_next()
return [
p.model_copy(update={"name": self._transform_name(p.name)}) for p in prompts
]

View file

@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
from fastmcp.server.transforms import GetToolNext, ListToolsNext, Transform
from fastmcp.server.transforms import GetToolNext, Transform
from fastmcp.tools.tool_transform import ToolTransformConfig
from fastmcp.utilities.versions import VersionSpec
@ -61,9 +61,8 @@ class ToolTransform(Transform):
return f"ToolTransform({names!r})"
return f"ToolTransform({names[:3]!r}... +{len(names) - 3} more)"
async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]:
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
"""Apply transforms to matching tools."""
tools = await call_next()
result: list[Tool] = []
for tool in tools:
if tool.name in self._transforms:

View file

@ -10,10 +10,6 @@ from fastmcp.server.transforms import (
GetResourceNext,
GetResourceTemplateNext,
GetToolNext,
ListPromptsNext,
ListResourcesNext,
ListResourceTemplatesNext,
ListToolsNext,
Transform,
)
from fastmcp.utilities.versions import VersionSpec
@ -73,8 +69,7 @@ class VersionFilter(Transform):
# Tools
# -------------------------------------------------------------------------
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 self._spec.matches(t.version)]
async def get_tool(
@ -86,8 +81,7 @@ class VersionFilter(Transform):
# Resources
# -------------------------------------------------------------------------
async def list_resources(self, call_next: ListResourcesNext) -> Sequence[Resource]:
resources = await call_next()
async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:
return [r for r in resources if self._spec.matches(r.version)]
async def get_resource(
@ -104,9 +98,8 @@ class VersionFilter(Transform):
# -------------------------------------------------------------------------
async def list_resource_templates(
self, call_next: ListResourceTemplatesNext
self, templates: Sequence[ResourceTemplate]
) -> Sequence[ResourceTemplate]:
templates = await call_next()
return [t for t in templates if self._spec.matches(t.version)]
async def get_resource_template(
@ -122,8 +115,7 @@ class VersionFilter(Transform):
# Prompts
# -------------------------------------------------------------------------
async def list_prompts(self, call_next: ListPromptsNext) -> Sequence[Prompt]:
prompts = await call_next()
async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
return [p for p in prompts if self._spec.matches(p.version)]
async def get_prompt(

View file

@ -4,6 +4,7 @@ from typing import Any
from fastmcp.server.providers.base import Provider
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.server.transforms import Namespace
from fastmcp.tools.tool import Tool, ToolResult
@ -77,3 +78,14 @@ class TestBaseProviderGetTasks:
assert len(tasks) == 1
assert tasks[0].name == "enabled"
async def test_get_tasks_applies_transforms(self):
"""get_tasks should apply provider transforms to component names."""
tool = CustomTool(name="my_tool", description="A tool")
provider = SimpleProvider(tools=[tool])
provider.add_transform(Namespace("api"))
tasks = await provider.get_tasks()
assert len(tasks) == 1
assert tasks[0].name == "api_my_tool"

View file

@ -600,11 +600,9 @@ class TestProviderToolTransformations:
layer = ToolTransform({"my_tool": ToolTransformConfig(name="renamed_tool")})
provider.add_transform(layer)
# Use call_next pattern
async def get_tools():
return await provider.list_tools()
transformed_tools = await layer.list_tools(get_tools)
# Get tools and pass directly to transform
tools = await provider.list_tools()
transformed_tools = await layer.list_tools(tools)
assert len(transformed_tools) == 1
assert transformed_tools[0].name == "renamed_tool"
@ -676,11 +674,8 @@ class TestProviderToolTransformations:
original_tools = await provider._list_tools()
assert original_tools[0].name == "my_tool"
# Transform modifies them when applied via call_next
async def get_tools():
return original_tools
transformed_tools = await layer.list_tools(get_tools)
# Transform modifies them when applied directly
transformed_tools = await layer.list_tools(original_tools)
assert transformed_tools[0].name == "renamed"
def test_transform_layer_duplicate_target_name_raises_error(self):

View file

@ -23,11 +23,9 @@ class TestNamespaceTransform:
provider = FastMCPProvider(server)
layer = Namespace("ns")
# Use call_next pattern - create a callable that returns the tools
async def get_tools():
return await provider.list_tools()
transformed_tools = await layer.list_tools(get_tools)
# Get tools and pass directly to transform
tools = await provider.list_tools()
transformed_tools = await layer.list_tools(tools)
assert len(transformed_tools) == 1
assert transformed_tools[0].name == "ns_my_tool"
@ -43,10 +41,8 @@ class TestNamespaceTransform:
provider = FastMCPProvider(server)
layer = Namespace("ns")
async def get_prompts():
return await provider.list_prompts()
transformed_prompts = await layer.list_prompts(get_prompts)
prompts = await provider.list_prompts()
transformed_prompts = await layer.list_prompts(prompts)
assert len(transformed_prompts) == 1
assert transformed_prompts[0].name == "ns_my_prompt"
@ -62,10 +58,8 @@ class TestNamespaceTransform:
provider = FastMCPProvider(server)
layer = Namespace("ns")
async def get_resources():
return await provider.list_resources()
transformed_resources = await layer.list_resources(get_resources)
resources = await provider.list_resources()
transformed_resources = await layer.list_resources(resources)
assert len(transformed_resources) == 1
assert str(transformed_resources[0].uri) == "resource://ns/data"
@ -81,10 +75,8 @@ class TestNamespaceTransform:
provider = FastMCPProvider(server)
layer = Namespace("ns")
async def get_templates():
return await provider.list_resource_templates()
transformed_templates = await layer.list_resource_templates(get_templates)
templates = await provider.list_resource_templates()
transformed_templates = await layer.list_resource_templates(templates)
assert len(transformed_templates) == 1
assert transformed_templates[0].uri_template == "resource://ns/{name}/data"
@ -104,10 +96,8 @@ class TestToolTransformRenames:
provider = FastMCPProvider(server)
layer = ToolTransform({"verbose_tool_name": ToolTransformConfig(name="short")})
async def get_tools():
return await provider.list_tools()
transformed_tools = await layer.list_tools(get_tools)
tools = await provider.list_tools()
transformed_tools = await layer.list_tools(tools)
assert len(transformed_tools) == 1
assert transformed_tools[0].name == "short"
@ -239,14 +229,10 @@ class TestTransformStacking:
inner_layer = Namespace("inner")
outer_layer = Namespace("outer")
# Build chain: base -> inner -> outer
async def base():
return await provider.list_tools()
async def inner_chain():
return await inner_layer.list_tools(base)
tools = await outer_layer.list_tools(inner_chain)
# Apply transforms sequentially: base -> inner -> outer
tools = await provider.list_tools()
tools = await inner_layer.list_tools(tools)
tools = await outer_layer.list_tools(tools)
assert len(tools) == 1
assert tools[0].name == "outer_inner_my_tool"
@ -289,10 +275,8 @@ class TestNoTransformation:
provider = FastMCPProvider(server)
transform = Transform()
async def get_tools():
return await provider.list_tools()
transformed_tools = await transform.list_tools(get_tools)
tools = await provider.list_tools()
transformed_tools = await transform.list_tools(tools)
assert len(transformed_tools) == 1
assert transformed_tools[0].name == "my_tool"
@ -308,10 +292,8 @@ class TestNoTransformation:
provider = FastMCPProvider(server)
layer = ToolTransform({})
async def get_tools():
return await provider.list_tools()
transformed_tools = await layer.list_tools(get_tools)
tools = await provider.list_tools()
transformed_tools = await layer.list_tools(tools)
assert len(transformed_tools) == 1
assert transformed_tools[0].name == "my_tool"

View file

@ -236,10 +236,7 @@ class TestTransformChain:
"""list_tools applies marks to matching components."""
disable_internal = Enabled(False, tags=set({"internal"}))
async def base():
return tools
result = await disable_internal.list_tools(base)
result = await disable_internal.list_tools(tools)
assert len(result) == 3
assert is_enabled(result[0]) # public
@ -251,12 +248,8 @@ class TestTransformChain:
disable_internal = Enabled(False, tags=set({"internal"}))
enable_safe = Enabled(True, tags=set({"safe"}))
async def base():
return tools
async def after_disable():
return await disable_internal.list_tools(base)
# Apply transforms sequentially
after_disable = await disable_internal.list_tools(tools)
result = await enable_safe.list_tools(after_disable)
enabled = [t for t in result if is_enabled(t)]
@ -270,12 +263,8 @@ class TestTransformChain:
disable_all = Enabled(False, match_all=True)
enable_public = Enabled(True, tags=set({"public"}))
async def base():
return tools
async def after_disable():
return await disable_all.list_tools(base)
# Apply transforms sequentially
after_disable = await disable_all.list_tools(tools)
result = await enable_public.list_tools(after_disable)
enabled = [t for t in result if is_enabled(t)]