mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 12:34:17 +02:00
Refactor transform list methods to pure function pattern (#2942)
This commit is contained in:
parent
23bfdf0680
commit
4d2feb0c29
14 changed files with 143 additions and 334 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue