fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Transform that exposes prompts as tools.
|
|
2
|
+
|
|
3
|
+
This transform generates tools for listing and getting prompts, enabling
|
|
4
|
+
clients that only support tools to access prompt functionality.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
```python
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
from fastmcp.server.transforms import PromptsAsTools
|
|
10
|
+
|
|
11
|
+
mcp = FastMCP("Server")
|
|
12
|
+
mcp.add_transform(PromptsAsTools(mcp))
|
|
13
|
+
# Now has list_prompts and get_prompt tools
|
|
14
|
+
```
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
22
|
+
|
|
23
|
+
from mcp.types import TextContent
|
|
24
|
+
|
|
25
|
+
from fastmcp.server.transforms import GetToolNext, Transform
|
|
26
|
+
from fastmcp.tools.tool import Tool
|
|
27
|
+
from fastmcp.utilities.versions import VersionSpec
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from fastmcp.server.providers.base import Provider
|
|
31
|
+
|
|
32
|
+
# Note: FastMCP imported inside tools to avoid circular import
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PromptsAsTools(Transform):
|
|
36
|
+
"""Transform that adds tools for listing and getting prompts.
|
|
37
|
+
|
|
38
|
+
Generates two tools:
|
|
39
|
+
- `list_prompts`: Lists all prompts from the provider
|
|
40
|
+
- `get_prompt`: Gets a specific prompt with optional arguments
|
|
41
|
+
|
|
42
|
+
The transform captures a provider reference at construction and queries it
|
|
43
|
+
for prompts when the generated tools are called. When used with FastMCP,
|
|
44
|
+
the provider's auth and visibility filtering is automatically applied.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
```python
|
|
48
|
+
mcp = FastMCP("Server")
|
|
49
|
+
mcp.add_transform(PromptsAsTools(mcp))
|
|
50
|
+
# Now has list_prompts and get_prompt tools
|
|
51
|
+
```
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, provider: Provider) -> None:
|
|
55
|
+
"""Initialize the transform with a provider reference.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
provider: The provider to query for prompts. Typically this is
|
|
59
|
+
the same FastMCP server the transform is added to.
|
|
60
|
+
"""
|
|
61
|
+
self._provider = provider
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
return f"PromptsAsTools({self._provider!r})"
|
|
65
|
+
|
|
66
|
+
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
|
|
67
|
+
"""Add prompt tools to the tool list."""
|
|
68
|
+
return [
|
|
69
|
+
*tools,
|
|
70
|
+
self._make_list_prompts_tool(),
|
|
71
|
+
self._make_get_prompt_tool(),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
async def get_tool(
|
|
75
|
+
self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
|
|
76
|
+
) -> Tool | None:
|
|
77
|
+
"""Get a tool by name, including generated prompt tools."""
|
|
78
|
+
# Check if it's one of our generated tools
|
|
79
|
+
if name == "list_prompts":
|
|
80
|
+
return self._make_list_prompts_tool()
|
|
81
|
+
if name == "get_prompt":
|
|
82
|
+
return self._make_get_prompt_tool()
|
|
83
|
+
|
|
84
|
+
# Otherwise delegate to downstream
|
|
85
|
+
return await call_next(name, version=version)
|
|
86
|
+
|
|
87
|
+
def _make_list_prompts_tool(self) -> Tool:
|
|
88
|
+
"""Create the list_prompts tool."""
|
|
89
|
+
provider = self._provider
|
|
90
|
+
|
|
91
|
+
async def list_prompts() -> str:
|
|
92
|
+
"""List all available prompts.
|
|
93
|
+
|
|
94
|
+
Returns JSON with prompt metadata including name, description,
|
|
95
|
+
and optional arguments.
|
|
96
|
+
"""
|
|
97
|
+
prompts = await provider.list_prompts()
|
|
98
|
+
|
|
99
|
+
result: list[dict[str, Any]] = []
|
|
100
|
+
for p in prompts:
|
|
101
|
+
result.append(
|
|
102
|
+
{
|
|
103
|
+
"name": p.name,
|
|
104
|
+
"description": p.description,
|
|
105
|
+
"arguments": [
|
|
106
|
+
{
|
|
107
|
+
"name": arg.name,
|
|
108
|
+
"description": arg.description,
|
|
109
|
+
"required": arg.required,
|
|
110
|
+
}
|
|
111
|
+
for arg in (p.arguments or [])
|
|
112
|
+
],
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return json.dumps(result, indent=2)
|
|
117
|
+
|
|
118
|
+
return Tool.from_function(fn=list_prompts)
|
|
119
|
+
|
|
120
|
+
def _make_get_prompt_tool(self) -> Tool:
|
|
121
|
+
"""Create the get_prompt tool."""
|
|
122
|
+
provider = self._provider
|
|
123
|
+
|
|
124
|
+
async def get_prompt(
|
|
125
|
+
name: Annotated[str, "The name of the prompt to get"],
|
|
126
|
+
arguments: Annotated[
|
|
127
|
+
dict[str, Any] | None,
|
|
128
|
+
"Optional arguments for the prompt",
|
|
129
|
+
] = None,
|
|
130
|
+
) -> str:
|
|
131
|
+
"""Get a prompt by name with optional arguments.
|
|
132
|
+
|
|
133
|
+
Returns the rendered prompt as JSON with a messages array.
|
|
134
|
+
Arguments should be provided as a dict mapping argument names to values.
|
|
135
|
+
"""
|
|
136
|
+
from fastmcp.server.server import FastMCP
|
|
137
|
+
|
|
138
|
+
# Use FastMCP.render_prompt() if available - runs middleware chain
|
|
139
|
+
if isinstance(provider, FastMCP):
|
|
140
|
+
result = await provider.render_prompt(name, arguments=arguments or {})
|
|
141
|
+
return _format_prompt_result(result)
|
|
142
|
+
|
|
143
|
+
# Fallback for plain providers - no middleware
|
|
144
|
+
prompt = await provider.get_prompt(name)
|
|
145
|
+
if prompt is None:
|
|
146
|
+
raise ValueError(f"Prompt not found: {name}")
|
|
147
|
+
|
|
148
|
+
result = await prompt._render(arguments or {})
|
|
149
|
+
return _format_prompt_result(result)
|
|
150
|
+
|
|
151
|
+
return Tool.from_function(fn=get_prompt)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _format_prompt_result(result: Any) -> str:
|
|
155
|
+
"""Format PromptResult for tool output.
|
|
156
|
+
|
|
157
|
+
Returns JSON with the messages array. Preserves embedded resources
|
|
158
|
+
as structured JSON objects.
|
|
159
|
+
"""
|
|
160
|
+
messages = []
|
|
161
|
+
for msg in result.messages:
|
|
162
|
+
if isinstance(msg.content, TextContent):
|
|
163
|
+
content = msg.content.text
|
|
164
|
+
else:
|
|
165
|
+
# Preserve structured content (e.g., EmbeddedResource) as dict
|
|
166
|
+
content = msg.content.model_dump(mode="json", exclude_none=True)
|
|
167
|
+
|
|
168
|
+
messages.append(
|
|
169
|
+
{
|
|
170
|
+
"role": msg.role,
|
|
171
|
+
"content": content,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return json.dumps({"messages": messages}, indent=2)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Transform that exposes resources as tools.
|
|
2
|
+
|
|
3
|
+
This transform generates tools for listing and reading resources, enabling
|
|
4
|
+
clients that only support tools to access resource functionality.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
```python
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
from fastmcp.server.transforms import ResourcesAsTools
|
|
10
|
+
|
|
11
|
+
mcp = FastMCP("Server")
|
|
12
|
+
mcp.add_transform(ResourcesAsTools(mcp))
|
|
13
|
+
# Now has list_resources and read_resource tools
|
|
14
|
+
```
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import base64
|
|
20
|
+
import json
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
23
|
+
|
|
24
|
+
from fastmcp.server.transforms import GetToolNext, Transform
|
|
25
|
+
from fastmcp.tools.tool import Tool
|
|
26
|
+
from fastmcp.utilities.versions import VersionSpec
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from fastmcp.server.providers.base import Provider
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ResourcesAsTools(Transform):
|
|
33
|
+
"""Transform that adds tools for listing and reading resources.
|
|
34
|
+
|
|
35
|
+
Generates two tools:
|
|
36
|
+
- `list_resources`: Lists all resources and templates from the provider
|
|
37
|
+
- `read_resource`: Reads a resource by URI
|
|
38
|
+
|
|
39
|
+
The transform captures a provider reference at construction and queries it
|
|
40
|
+
for resources when the generated tools are called. When used with FastMCP,
|
|
41
|
+
the provider's auth and visibility filtering is automatically applied.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```python
|
|
45
|
+
mcp = FastMCP("Server")
|
|
46
|
+
mcp.add_transform(ResourcesAsTools(mcp))
|
|
47
|
+
# Now has list_resources and read_resource tools
|
|
48
|
+
```
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, provider: Provider) -> None:
|
|
52
|
+
"""Initialize the transform with a provider reference.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
provider: The provider to query for resources. Typically this is
|
|
56
|
+
the same FastMCP server the transform is added to.
|
|
57
|
+
"""
|
|
58
|
+
self._provider = provider
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
return f"ResourcesAsTools({self._provider!r})"
|
|
62
|
+
|
|
63
|
+
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
|
|
64
|
+
"""Add resource tools to the tool list."""
|
|
65
|
+
return [
|
|
66
|
+
*tools,
|
|
67
|
+
self._make_list_resources_tool(),
|
|
68
|
+
self._make_read_resource_tool(),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
async def get_tool(
|
|
72
|
+
self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
|
|
73
|
+
) -> Tool | None:
|
|
74
|
+
"""Get a tool by name, including generated resource tools."""
|
|
75
|
+
# Check if it's one of our generated tools
|
|
76
|
+
if name == "list_resources":
|
|
77
|
+
return self._make_list_resources_tool()
|
|
78
|
+
if name == "read_resource":
|
|
79
|
+
return self._make_read_resource_tool()
|
|
80
|
+
|
|
81
|
+
# Otherwise delegate to downstream
|
|
82
|
+
return await call_next(name, version=version)
|
|
83
|
+
|
|
84
|
+
def _make_list_resources_tool(self) -> Tool:
|
|
85
|
+
"""Create the list_resources tool."""
|
|
86
|
+
provider = self._provider
|
|
87
|
+
|
|
88
|
+
async def list_resources() -> str:
|
|
89
|
+
"""List all available resources and resource templates.
|
|
90
|
+
|
|
91
|
+
Returns JSON with resource metadata. Static resources have a 'uri' field,
|
|
92
|
+
while templates have a 'uri_template' field with placeholders like {name}.
|
|
93
|
+
"""
|
|
94
|
+
resources = await provider.list_resources()
|
|
95
|
+
templates = await provider.list_resource_templates()
|
|
96
|
+
|
|
97
|
+
result: list[dict[str, Any]] = []
|
|
98
|
+
|
|
99
|
+
# Static resources
|
|
100
|
+
for r in resources:
|
|
101
|
+
result.append(
|
|
102
|
+
{
|
|
103
|
+
"uri": str(r.uri),
|
|
104
|
+
"name": r.name,
|
|
105
|
+
"description": r.description,
|
|
106
|
+
"mime_type": r.mime_type,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Resource templates (URI contains placeholders like {name})
|
|
111
|
+
for t in templates:
|
|
112
|
+
result.append(
|
|
113
|
+
{
|
|
114
|
+
"uri_template": t.uri_template,
|
|
115
|
+
"name": t.name,
|
|
116
|
+
"description": t.description,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return json.dumps(result, indent=2)
|
|
121
|
+
|
|
122
|
+
return Tool.from_function(fn=list_resources)
|
|
123
|
+
|
|
124
|
+
def _make_read_resource_tool(self) -> Tool:
|
|
125
|
+
"""Create the read_resource tool."""
|
|
126
|
+
provider = self._provider
|
|
127
|
+
|
|
128
|
+
async def read_resource(
|
|
129
|
+
uri: Annotated[str, "The URI of the resource to read"],
|
|
130
|
+
) -> str:
|
|
131
|
+
"""Read a resource by its URI.
|
|
132
|
+
|
|
133
|
+
For static resources, provide the exact URI. For templated resources,
|
|
134
|
+
provide the URI with template parameters filled in.
|
|
135
|
+
|
|
136
|
+
Returns the resource content as a string. Binary content is
|
|
137
|
+
base64-encoded.
|
|
138
|
+
"""
|
|
139
|
+
from fastmcp import FastMCP
|
|
140
|
+
|
|
141
|
+
# Use FastMCP.read_resource() if available - runs middleware chain
|
|
142
|
+
if isinstance(provider, FastMCP):
|
|
143
|
+
result = await provider.read_resource(uri)
|
|
144
|
+
return _format_result(result)
|
|
145
|
+
|
|
146
|
+
# Fallback for plain providers - no middleware
|
|
147
|
+
resource = await provider.get_resource(uri)
|
|
148
|
+
if resource is not None:
|
|
149
|
+
result = await resource._read()
|
|
150
|
+
return _format_result(result)
|
|
151
|
+
|
|
152
|
+
template = await provider.get_resource_template(uri)
|
|
153
|
+
if template is not None:
|
|
154
|
+
params = template.matches(uri)
|
|
155
|
+
if params is not None:
|
|
156
|
+
result = await template._read(uri, params)
|
|
157
|
+
return _format_result(result)
|
|
158
|
+
|
|
159
|
+
raise ValueError(f"Resource not found: {uri}")
|
|
160
|
+
|
|
161
|
+
return Tool.from_function(fn=read_resource)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _format_result(result: Any) -> str:
|
|
165
|
+
"""Format ResourceResult for tool output.
|
|
166
|
+
|
|
167
|
+
Single text content is returned as-is. Single binary content is base64-encoded.
|
|
168
|
+
Multiple contents are JSON-encoded with each item containing content and mime_type.
|
|
169
|
+
"""
|
|
170
|
+
# result is a ResourceResult with .contents list
|
|
171
|
+
if len(result.contents) == 1:
|
|
172
|
+
content = result.contents[0].content
|
|
173
|
+
if isinstance(content, bytes):
|
|
174
|
+
return base64.b64encode(content).decode()
|
|
175
|
+
return content
|
|
176
|
+
|
|
177
|
+
# Multiple contents - JSON encode
|
|
178
|
+
return json.dumps(
|
|
179
|
+
[
|
|
180
|
+
{
|
|
181
|
+
"content": (
|
|
182
|
+
c.content
|
|
183
|
+
if isinstance(c.content, str)
|
|
184
|
+
else base64.b64encode(c.content).decode()
|
|
185
|
+
),
|
|
186
|
+
"mime_type": c.mime_type,
|
|
187
|
+
}
|
|
188
|
+
for c in result.contents
|
|
189
|
+
]
|
|
190
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Transform for applying tool transformations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from fastmcp.server.transforms import GetToolNext, Transform
|
|
9
|
+
from fastmcp.tools.tool_transform import ToolTransformConfig
|
|
10
|
+
from fastmcp.utilities.versions import VersionSpec
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from fastmcp.tools.tool import Tool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolTransform(Transform):
|
|
17
|
+
"""Applies tool transformations to modify tool schemas.
|
|
18
|
+
|
|
19
|
+
Wraps ToolTransformConfig to apply argument renames, schema changes,
|
|
20
|
+
hidden arguments, and other transformations at the transform level.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
```python
|
|
24
|
+
transform = ToolTransform({
|
|
25
|
+
"my_tool": ToolTransformConfig(
|
|
26
|
+
name="renamed_tool",
|
|
27
|
+
arguments={"old_arg": ArgTransformConfig(name="new_arg")}
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, transforms: dict[str, ToolTransformConfig]) -> None:
|
|
34
|
+
"""Initialize ToolTransform.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
transforms: Map of original tool name → transform config.
|
|
38
|
+
"""
|
|
39
|
+
self._transforms = transforms
|
|
40
|
+
|
|
41
|
+
# Build reverse mapping: final_name → original_name
|
|
42
|
+
self._name_reverse: dict[str, str] = {}
|
|
43
|
+
for original_name, config in transforms.items():
|
|
44
|
+
final_name = config.name if config.name else original_name
|
|
45
|
+
self._name_reverse[final_name] = original_name
|
|
46
|
+
|
|
47
|
+
# Validate no duplicate target names
|
|
48
|
+
seen_targets: dict[str, str] = {}
|
|
49
|
+
for original_name, config in transforms.items():
|
|
50
|
+
target = config.name if config.name else original_name
|
|
51
|
+
if target in seen_targets:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"ToolTransform has duplicate target name {target!r}: "
|
|
54
|
+
f"both {seen_targets[target]!r} and {original_name!r} map to it"
|
|
55
|
+
)
|
|
56
|
+
seen_targets[target] = original_name
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
names = list(self._transforms.keys())
|
|
60
|
+
if len(names) <= 3:
|
|
61
|
+
return f"ToolTransform({names!r})"
|
|
62
|
+
return f"ToolTransform({names[:3]!r}... +{len(names) - 3} more)"
|
|
63
|
+
|
|
64
|
+
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
|
|
65
|
+
"""Apply transforms to matching tools."""
|
|
66
|
+
result: list[Tool] = []
|
|
67
|
+
for tool in tools:
|
|
68
|
+
if tool.name in self._transforms:
|
|
69
|
+
transformed = self._transforms[tool.name].apply(tool)
|
|
70
|
+
result.append(transformed)
|
|
71
|
+
else:
|
|
72
|
+
result.append(tool)
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
async def get_tool(
|
|
76
|
+
self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
|
|
77
|
+
) -> Tool | None:
|
|
78
|
+
"""Get tool by transformed name."""
|
|
79
|
+
# Check if this name is a transformed name
|
|
80
|
+
original_name = self._name_reverse.get(name, name)
|
|
81
|
+
|
|
82
|
+
# Get the original tool
|
|
83
|
+
tool = await call_next(original_name, version=version)
|
|
84
|
+
if tool is None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
# Apply transform if applicable
|
|
88
|
+
if original_name in self._transforms:
|
|
89
|
+
transformed = self._transforms[original_name].apply(tool)
|
|
90
|
+
# Only return if requested name matches transformed name
|
|
91
|
+
if transformed.name == name:
|
|
92
|
+
return transformed
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# No transform, return as-is only if name matches
|
|
96
|
+
return tool if tool.name == name else None
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Version filter transform for filtering components by version range."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from fastmcp.server.transforms import (
|
|
9
|
+
GetPromptNext,
|
|
10
|
+
GetResourceNext,
|
|
11
|
+
GetResourceTemplateNext,
|
|
12
|
+
GetToolNext,
|
|
13
|
+
Transform,
|
|
14
|
+
)
|
|
15
|
+
from fastmcp.utilities.versions import VersionSpec
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from fastmcp.prompts.prompt import Prompt
|
|
19
|
+
from fastmcp.resources.resource import Resource
|
|
20
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
21
|
+
from fastmcp.tools.tool import Tool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VersionFilter(Transform):
|
|
25
|
+
"""Filters components by version range.
|
|
26
|
+
|
|
27
|
+
When applied to a provider or server, only components within the version
|
|
28
|
+
range are visible. Within that filtered set, the highest version of each
|
|
29
|
+
component is exposed to clients (standard deduplication behavior).
|
|
30
|
+
|
|
31
|
+
Parameters mirror comparison operators for clarity:
|
|
32
|
+
|
|
33
|
+
# Versions < 3.0 (v1 and v2)
|
|
34
|
+
server.add_transform(VersionFilter(version_lt="3.0"))
|
|
35
|
+
|
|
36
|
+
# Versions >= 2.0 and < 3.0 (only v2.x)
|
|
37
|
+
server.add_transform(VersionFilter(version_gte="2.0", version_lt="3.0"))
|
|
38
|
+
|
|
39
|
+
Works with any version string - PEP 440 (1.0, 2.0) or dates (2025-01-01).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
version_gte: Versions >= this value pass through.
|
|
43
|
+
version_lt: Versions < this value pass through.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
version_gte: str | None = None,
|
|
50
|
+
version_lt: str | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
if version_gte is None and version_lt is None:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"At least one of version_gte or version_lt must be specified"
|
|
55
|
+
)
|
|
56
|
+
self.version_gte = version_gte
|
|
57
|
+
self.version_lt = version_lt
|
|
58
|
+
self._spec = VersionSpec(gte=version_gte, lt=version_lt)
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
parts = []
|
|
62
|
+
if self.version_gte:
|
|
63
|
+
parts.append(f"version_gte={self.version_gte!r}")
|
|
64
|
+
if self.version_lt:
|
|
65
|
+
parts.append(f"version_lt={self.version_lt!r}")
|
|
66
|
+
return f"VersionFilter({', '.join(parts)})"
|
|
67
|
+
|
|
68
|
+
# -------------------------------------------------------------------------
|
|
69
|
+
# Tools
|
|
70
|
+
# -------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
|
|
73
|
+
return [t for t in tools if self._spec.matches(t.version)]
|
|
74
|
+
|
|
75
|
+
async def get_tool(
|
|
76
|
+
self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
|
|
77
|
+
) -> Tool | None:
|
|
78
|
+
return await call_next(name, version=self._spec.intersect(version))
|
|
79
|
+
|
|
80
|
+
# -------------------------------------------------------------------------
|
|
81
|
+
# Resources
|
|
82
|
+
# -------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:
|
|
85
|
+
return [r for r in resources if self._spec.matches(r.version)]
|
|
86
|
+
|
|
87
|
+
async def get_resource(
|
|
88
|
+
self,
|
|
89
|
+
uri: str,
|
|
90
|
+
call_next: GetResourceNext,
|
|
91
|
+
*,
|
|
92
|
+
version: VersionSpec | None = None,
|
|
93
|
+
) -> Resource | None:
|
|
94
|
+
return await call_next(uri, version=self._spec.intersect(version))
|
|
95
|
+
|
|
96
|
+
# -------------------------------------------------------------------------
|
|
97
|
+
# Resource Templates
|
|
98
|
+
# -------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
async def list_resource_templates(
|
|
101
|
+
self, templates: Sequence[ResourceTemplate]
|
|
102
|
+
) -> Sequence[ResourceTemplate]:
|
|
103
|
+
return [t for t in templates if self._spec.matches(t.version)]
|
|
104
|
+
|
|
105
|
+
async def get_resource_template(
|
|
106
|
+
self,
|
|
107
|
+
uri: str,
|
|
108
|
+
call_next: GetResourceTemplateNext,
|
|
109
|
+
*,
|
|
110
|
+
version: VersionSpec | None = None,
|
|
111
|
+
) -> ResourceTemplate | None:
|
|
112
|
+
return await call_next(uri, version=self._spec.intersect(version))
|
|
113
|
+
|
|
114
|
+
# -------------------------------------------------------------------------
|
|
115
|
+
# Prompts
|
|
116
|
+
# -------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
|
|
119
|
+
return [p for p in prompts if self._spec.matches(p.version)]
|
|
120
|
+
|
|
121
|
+
async def get_prompt(
|
|
122
|
+
self, name: str, call_next: GetPromptNext, *, version: VersionSpec | None = None
|
|
123
|
+
) -> Prompt | None:
|
|
124
|
+
return await call_next(name, version=self._spec.intersect(version))
|