diff --git a/docs/servers/providers/transforms.mdx b/docs/servers/providers/transforms.mdx index a53027ad20..27fdb17787 100644 --- a/docs/servers/providers/transforms.mdx +++ b/docs/servers/providers/transforms.mdx @@ -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: diff --git a/src/fastmcp/server/providers/base.py b/src/fastmcp/server/providers/base.py index e6dbc70141..03c00a8a25 100644 --- a/src/fastmcp/server/providers/base.py +++ b/src/fastmcp/server/providers/base.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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() ] diff --git a/src/fastmcp/server/providers/fastmcp_provider.py b/src/fastmcp/server/providers/fastmcp_provider.py index 95af4a7f69..8cc40506c2 100644 --- a/src/fastmcp/server/providers/fastmcp_provider.py +++ b/src/fastmcp/server/providers/fastmcp_provider.py @@ -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 @@ -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() ] diff --git a/src/fastmcp/server/providers/wrapped_provider.py b/src/fastmcp/server/providers/wrapped_provider.py index d406e708b8..4f5c0df92b 100644 --- a/src/fastmcp/server/providers/wrapped_provider.py +++ b/src/fastmcp/server/providers/wrapped_provider.py @@ -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 @@ -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() ] diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 4ac76509b5..a1c328a0c6 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -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: diff --git a/src/fastmcp/server/transforms/__init__.py b/src/fastmcp/server/transforms/__init__.py index 8a5eed9fb5..8c9a8a407d 100644 --- a/src/fastmcp/server/transforms/__init__.py +++ b/src/fastmcp/server/transforms/__init__.py @@ -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 @@ 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 @@ 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 @@ def __call__( 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 @@ def __repr__(self) -> str: # 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 @@ async def get_tool( # 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 @@ async def get_resource( # ------------------------------------------------------------------------- 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 @@ async def get_resource_template( # 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 @@ async def get_prompt( "GetResourceNext", "GetResourceTemplateNext", "GetToolNext", - "ListPromptsNext", - "ListResourceTemplatesNext", - "ListResourcesNext", - "ListToolsNext", "Namespace", "ToolTransform", "Transform", diff --git a/src/fastmcp/server/transforms/enabled.py b/src/fastmcp/server/transforms/enabled.py index 6b855e8066..faf9aeaaad 100644 --- a/src/fastmcp/server/transforms/enabled.py +++ b/src/fastmcp/server/transforms/enabled.py @@ -19,10 +19,6 @@ GetResourceNext, GetResourceTemplateNext, GetToolNext, - ListPromptsNext, - ListResourcesNext, - ListResourceTemplatesNext, - ListToolsNext, Transform, ) from fastmcp.utilities.versions import VersionSpec @@ -195,9 +191,8 @@ def _mark_component(self, component: T) -> T: # 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 @@ async def get_tool( # 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 @@ async def get_resource( # ------------------------------------------------------------------------- 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 @@ async def get_resource_template( # 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( diff --git a/src/fastmcp/server/transforms/namespace.py b/src/fastmcp/server/transforms/namespace.py index a9e46e418f..152d493e58 100644 --- a/src/fastmcp/server/transforms/namespace.py +++ b/src/fastmcp/server/transforms/namespace.py @@ -11,10 +11,6 @@ GetResourceNext, GetResourceTemplateNext, GetToolNext, - ListPromptsNext, - ListResourcesNext, - ListResourceTemplatesNext, - ListToolsNext, Transform, ) from fastmcp.utilities.versions import VersionSpec @@ -98,9 +94,8 @@ def _reverse_uri(self, uri: str) -> str | None: # 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 @@ async def get_tool( # 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 @@ async def get_resource( # ------------------------------------------------------------------------- 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 @@ async def get_resource_template( # 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 ] diff --git a/src/fastmcp/server/transforms/tool_transform.py b/src/fastmcp/server/transforms/tool_transform.py index 34c4b10f43..ed58788daa 100644 --- a/src/fastmcp/server/transforms/tool_transform.py +++ b/src/fastmcp/server/transforms/tool_transform.py @@ -5,7 +5,7 @@ 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 @@ def __repr__(self) -> str: 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: diff --git a/src/fastmcp/server/transforms/version_filter.py b/src/fastmcp/server/transforms/version_filter.py index 9837d1d739..d49928cd04 100644 --- a/src/fastmcp/server/transforms/version_filter.py +++ b/src/fastmcp/server/transforms/version_filter.py @@ -10,10 +10,6 @@ GetResourceNext, GetResourceTemplateNext, GetToolNext, - ListPromptsNext, - ListResourcesNext, - ListResourceTemplatesNext, - ListToolsNext, Transform, ) from fastmcp.utilities.versions import VersionSpec @@ -73,8 +69,7 @@ def __repr__(self) -> str: # 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 @@ async def get_tool( # 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 @@ async def get_resource( # ------------------------------------------------------------------------- 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 @@ async def get_resource_template( # 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( diff --git a/tests/server/providers/test_base_provider.py b/tests/server/providers/test_base_provider.py index 084e5ff460..ed084c34dd 100644 --- a/tests/server/providers/test_base_provider.py +++ b/tests/server/providers/test_base_provider.py @@ -4,6 +4,7 @@ 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 @@ async def run(self, arguments: dict[str, Any]) -> ToolResult: 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" diff --git a/tests/server/providers/test_local_provider.py b/tests/server/providers/test_local_provider.py index 773dc01d8b..d920333933 100644 --- a/tests/server/providers/test_local_provider.py +++ b/tests/server/providers/test_local_provider.py @@ -600,11 +600,9 @@ def my_tool(x: int) -> int: 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 @@ def my_tool(x: int) -> int: 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): diff --git a/tests/server/providers/test_transforming_provider.py b/tests/server/providers/test_transforming_provider.py index 8a89572753..ce7acd2a4e 100644 --- a/tests/server/providers/test_transforming_provider.py +++ b/tests/server/providers/test_transforming_provider.py @@ -23,11 +23,9 @@ def my_tool() -> str: 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 @@ def my_prompt() -> str: 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 @@ def my_resource() -> str: 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 @@ def my_template(name: str) -> str: 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 @@ def verbose_tool_name() -> str: 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 @@ def my_tool() -> str: 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 @@ def my_tool() -> str: 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 @@ def my_tool() -> str: 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" diff --git a/tests/server/transforms/test_enabled.py b/tests/server/transforms/test_enabled.py index c31083cd56..fca9511d5c 100644 --- a/tests/server/transforms/test_enabled.py +++ b/tests/server/transforms/test_enabled.py @@ -236,10 +236,7 @@ async def test_list_tools_marks_matching(self, tools): """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 @@ async def test_later_transform_overrides(self, tools): 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 @@ async def test_allowlist_pattern(self, tools): 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)]