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
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ComponentService: Provides async management of tools, resources, and prompts for FastMCP servers.
|
|
3
|
-
Handles enabling/disabling components both locally and across mounted servers.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from fastmcp.exceptions import NotFoundError
|
|
7
|
-
from fastmcp.prompts.prompt import Prompt
|
|
8
|
-
from fastmcp.resources.resource import Resource
|
|
9
|
-
from fastmcp.resources.template import ResourceTemplate
|
|
10
|
-
from fastmcp.server.server import FastMCP, has_resource_prefix, remove_resource_prefix
|
|
11
|
-
from fastmcp.tools.tool import Tool
|
|
12
|
-
from fastmcp.utilities.logging import get_logger
|
|
13
|
-
|
|
14
|
-
logger = get_logger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ComponentService:
|
|
18
|
-
"""Service for managing components like tools, resources, and prompts."""
|
|
19
|
-
|
|
20
|
-
def __init__(self, server: FastMCP):
|
|
21
|
-
self._server = server
|
|
22
|
-
self._tool_manager = server._tool_manager
|
|
23
|
-
self._resource_manager = server._resource_manager
|
|
24
|
-
self._prompt_manager = server._prompt_manager
|
|
25
|
-
|
|
26
|
-
async def _enable_tool(self, key: str) -> Tool:
|
|
27
|
-
"""Handle 'enableTool' requests.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
key: The key of the tool to enable
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
The tool that was enabled
|
|
34
|
-
"""
|
|
35
|
-
logger.debug("Enabling tool: %s", key)
|
|
36
|
-
|
|
37
|
-
# 1. Check local tools first. The server will have already applied its filter.
|
|
38
|
-
if key in self._server._tool_manager._tools:
|
|
39
|
-
tool: Tool = await self._server.get_tool(key)
|
|
40
|
-
tool.enable()
|
|
41
|
-
return tool
|
|
42
|
-
|
|
43
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
44
|
-
for mounted in reversed(self._server._mounted_servers):
|
|
45
|
-
if mounted.prefix:
|
|
46
|
-
if key.startswith(f"{mounted.prefix}_"):
|
|
47
|
-
tool_key = key.removeprefix(f"{mounted.prefix}_")
|
|
48
|
-
mounted_service = ComponentService(mounted.server)
|
|
49
|
-
tool = await mounted_service._enable_tool(tool_key)
|
|
50
|
-
return tool
|
|
51
|
-
else:
|
|
52
|
-
continue
|
|
53
|
-
raise NotFoundError(f"Unknown tool: {key}")
|
|
54
|
-
|
|
55
|
-
async def _disable_tool(self, key: str) -> Tool:
|
|
56
|
-
"""Handle 'disableTool' requests.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
key: The key of the tool to disable
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
The tool that was disabled
|
|
63
|
-
"""
|
|
64
|
-
logger.debug("Disable tool: %s", key)
|
|
65
|
-
|
|
66
|
-
# 1. Check local tools first. The server will have already applied its filter.
|
|
67
|
-
if key in self._server._tool_manager._tools:
|
|
68
|
-
tool: Tool = await self._server.get_tool(key)
|
|
69
|
-
tool.disable()
|
|
70
|
-
return tool
|
|
71
|
-
|
|
72
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
73
|
-
for mounted in reversed(self._server._mounted_servers):
|
|
74
|
-
if mounted.prefix:
|
|
75
|
-
if key.startswith(f"{mounted.prefix}_"):
|
|
76
|
-
tool_key = key.removeprefix(f"{mounted.prefix}_")
|
|
77
|
-
mounted_service = ComponentService(mounted.server)
|
|
78
|
-
tool = await mounted_service._disable_tool(tool_key)
|
|
79
|
-
return tool
|
|
80
|
-
else:
|
|
81
|
-
continue
|
|
82
|
-
raise NotFoundError(f"Unknown tool: {key}")
|
|
83
|
-
|
|
84
|
-
async def _enable_resource(self, key: str) -> Resource | ResourceTemplate:
|
|
85
|
-
"""Handle 'enableResource' requests.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
key: The key of the resource to enable
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
The resource that was enabled
|
|
92
|
-
"""
|
|
93
|
-
logger.debug("Enabling resource: %s", key)
|
|
94
|
-
|
|
95
|
-
# 1. Check local resources first. The server will have already applied its filter.
|
|
96
|
-
if key in self._resource_manager._resources:
|
|
97
|
-
resource: Resource = await self._server.get_resource(key)
|
|
98
|
-
resource.enable()
|
|
99
|
-
return resource
|
|
100
|
-
if key in self._resource_manager._templates:
|
|
101
|
-
template: ResourceTemplate = await self._server.get_resource_template(key)
|
|
102
|
-
template.enable()
|
|
103
|
-
return template
|
|
104
|
-
|
|
105
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
106
|
-
for mounted in reversed(self._server._mounted_servers):
|
|
107
|
-
if mounted.prefix:
|
|
108
|
-
if has_resource_prefix(key, mounted.prefix):
|
|
109
|
-
key = remove_resource_prefix(key, mounted.prefix)
|
|
110
|
-
mounted_service = ComponentService(mounted.server)
|
|
111
|
-
mounted_resource: (
|
|
112
|
-
Resource | ResourceTemplate
|
|
113
|
-
) = await mounted_service._enable_resource(key)
|
|
114
|
-
return mounted_resource
|
|
115
|
-
else:
|
|
116
|
-
continue
|
|
117
|
-
raise NotFoundError(f"Unknown resource: {key}")
|
|
118
|
-
|
|
119
|
-
async def _disable_resource(self, key: str) -> Resource | ResourceTemplate:
|
|
120
|
-
"""Handle 'disableResource' requests.
|
|
121
|
-
|
|
122
|
-
Args:
|
|
123
|
-
key: The key of the resource to disable
|
|
124
|
-
|
|
125
|
-
Returns:
|
|
126
|
-
The resource that was disabled
|
|
127
|
-
"""
|
|
128
|
-
logger.debug("Disable resource: %s", key)
|
|
129
|
-
|
|
130
|
-
# 1. Check local resources first. The server will have already applied its filter.
|
|
131
|
-
if key in self._resource_manager._resources:
|
|
132
|
-
resource: Resource = await self._server.get_resource(key)
|
|
133
|
-
resource.disable()
|
|
134
|
-
return resource
|
|
135
|
-
if key in self._resource_manager._templates:
|
|
136
|
-
template: ResourceTemplate = await self._server.get_resource_template(key)
|
|
137
|
-
template.disable()
|
|
138
|
-
return template
|
|
139
|
-
|
|
140
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
141
|
-
for mounted in reversed(self._server._mounted_servers):
|
|
142
|
-
if mounted.prefix:
|
|
143
|
-
if has_resource_prefix(key, mounted.prefix):
|
|
144
|
-
key = remove_resource_prefix(key, mounted.prefix)
|
|
145
|
-
mounted_service = ComponentService(mounted.server)
|
|
146
|
-
mounted_resource: (
|
|
147
|
-
Resource | ResourceTemplate
|
|
148
|
-
) = await mounted_service._disable_resource(key)
|
|
149
|
-
return mounted_resource
|
|
150
|
-
else:
|
|
151
|
-
continue
|
|
152
|
-
raise NotFoundError(f"Unknown resource: {key}")
|
|
153
|
-
|
|
154
|
-
async def _enable_prompt(self, key: str) -> Prompt:
|
|
155
|
-
"""Handle 'enablePrompt' requests.
|
|
156
|
-
|
|
157
|
-
Args:
|
|
158
|
-
key: The key of the prompt to enable
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
The prompt that was enabled
|
|
162
|
-
"""
|
|
163
|
-
logger.debug("Enabling prompt: %s", key)
|
|
164
|
-
|
|
165
|
-
# 1. Check local prompts first. The server will have already applied its filter.
|
|
166
|
-
if key in self._server._prompt_manager._prompts:
|
|
167
|
-
prompt: Prompt = await self._server.get_prompt(key)
|
|
168
|
-
prompt.enable()
|
|
169
|
-
return prompt
|
|
170
|
-
|
|
171
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
172
|
-
for mounted in reversed(self._server._mounted_servers):
|
|
173
|
-
if mounted.prefix:
|
|
174
|
-
if key.startswith(f"{mounted.prefix}_"):
|
|
175
|
-
prompt_key = key.removeprefix(f"{mounted.prefix}_")
|
|
176
|
-
mounted_service = ComponentService(mounted.server)
|
|
177
|
-
prompt = await mounted_service._enable_prompt(prompt_key)
|
|
178
|
-
return prompt
|
|
179
|
-
else:
|
|
180
|
-
continue
|
|
181
|
-
raise NotFoundError(f"Unknown prompt: {key}")
|
|
182
|
-
|
|
183
|
-
async def _disable_prompt(self, key: str) -> Prompt:
|
|
184
|
-
"""Handle 'disablePrompt' requests.
|
|
185
|
-
|
|
186
|
-
Args:
|
|
187
|
-
key: The key of the prompt to disable
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
The prompt that was disabled
|
|
191
|
-
"""
|
|
192
|
-
|
|
193
|
-
# 1. Check local prompts first. The server will have already applied its filter.
|
|
194
|
-
if key in self._server._prompt_manager._prompts:
|
|
195
|
-
prompt: Prompt = await self._server.get_prompt(key)
|
|
196
|
-
prompt.disable()
|
|
197
|
-
return prompt
|
|
198
|
-
|
|
199
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
200
|
-
for mounted in reversed(self._server._mounted_servers):
|
|
201
|
-
if mounted.prefix:
|
|
202
|
-
if key.startswith(f"{mounted.prefix}_"):
|
|
203
|
-
prompt_key = key.removeprefix(f"{mounted.prefix}_")
|
|
204
|
-
mounted_service = ComponentService(mounted.server)
|
|
205
|
-
prompt = await mounted_service._disable_prompt(prompt_key)
|
|
206
|
-
return prompt
|
|
207
|
-
else:
|
|
208
|
-
continue
|
|
209
|
-
raise NotFoundError(f"Unknown prompt: {key}")
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations as _annotations
|
|
2
|
-
|
|
3
|
-
import warnings
|
|
4
|
-
from collections.abc import Awaitable, Callable
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from mcp import GetPromptResult
|
|
8
|
-
|
|
9
|
-
from fastmcp import settings
|
|
10
|
-
from fastmcp.exceptions import FastMCPError, NotFoundError, PromptError
|
|
11
|
-
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
|
|
12
|
-
from fastmcp.settings import DuplicateBehavior
|
|
13
|
-
from fastmcp.utilities.logging import get_logger
|
|
14
|
-
|
|
15
|
-
logger = get_logger(__name__)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class PromptManager:
|
|
19
|
-
"""Manages FastMCP prompts."""
|
|
20
|
-
|
|
21
|
-
def __init__(
|
|
22
|
-
self,
|
|
23
|
-
duplicate_behavior: DuplicateBehavior | None = None,
|
|
24
|
-
mask_error_details: bool | None = None,
|
|
25
|
-
):
|
|
26
|
-
self._prompts: dict[str, Prompt] = {}
|
|
27
|
-
self.mask_error_details = mask_error_details or settings.mask_error_details
|
|
28
|
-
|
|
29
|
-
# Default to "warn" if None is provided
|
|
30
|
-
if duplicate_behavior is None:
|
|
31
|
-
duplicate_behavior = "warn"
|
|
32
|
-
|
|
33
|
-
if duplicate_behavior not in DuplicateBehavior.__args__:
|
|
34
|
-
raise ValueError(
|
|
35
|
-
f"Invalid duplicate_behavior: {duplicate_behavior}. "
|
|
36
|
-
f"Must be one of: {', '.join(DuplicateBehavior.__args__)}"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
self.duplicate_behavior = duplicate_behavior
|
|
40
|
-
|
|
41
|
-
async def has_prompt(self, key: str) -> bool:
|
|
42
|
-
"""Check if a prompt exists."""
|
|
43
|
-
prompts = await self.get_prompts()
|
|
44
|
-
return key in prompts
|
|
45
|
-
|
|
46
|
-
async def get_prompt(self, key: str) -> Prompt:
|
|
47
|
-
"""Get prompt by key."""
|
|
48
|
-
prompts = await self.get_prompts()
|
|
49
|
-
if key in prompts:
|
|
50
|
-
return prompts[key]
|
|
51
|
-
raise NotFoundError(f"Unknown prompt: {key}")
|
|
52
|
-
|
|
53
|
-
async def get_prompts(self) -> dict[str, Prompt]:
|
|
54
|
-
"""
|
|
55
|
-
Gets the complete, unfiltered inventory of local prompts.
|
|
56
|
-
"""
|
|
57
|
-
return dict(self._prompts)
|
|
58
|
-
|
|
59
|
-
def add_prompt_from_fn(
|
|
60
|
-
self,
|
|
61
|
-
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
62
|
-
name: str | None = None,
|
|
63
|
-
description: str | None = None,
|
|
64
|
-
tags: set[str] | None = None,
|
|
65
|
-
) -> FunctionPrompt:
|
|
66
|
-
"""Create a prompt from a function."""
|
|
67
|
-
# deprecated in 2.7.0
|
|
68
|
-
if settings.deprecation_warnings:
|
|
69
|
-
warnings.warn(
|
|
70
|
-
"PromptManager.add_prompt_from_fn() is deprecated. Use Prompt.from_function() and call add_prompt() instead.",
|
|
71
|
-
DeprecationWarning,
|
|
72
|
-
stacklevel=2,
|
|
73
|
-
)
|
|
74
|
-
prompt = FunctionPrompt.from_function(
|
|
75
|
-
fn, name=name, description=description, tags=tags
|
|
76
|
-
)
|
|
77
|
-
return self.add_prompt(prompt) # type: ignore
|
|
78
|
-
|
|
79
|
-
def add_prompt(self, prompt: Prompt) -> Prompt:
|
|
80
|
-
"""Add a prompt to the manager."""
|
|
81
|
-
# Check for duplicates
|
|
82
|
-
existing = self._prompts.get(prompt.key)
|
|
83
|
-
if existing:
|
|
84
|
-
if self.duplicate_behavior == "warn":
|
|
85
|
-
logger.warning(f"Prompt already exists: {prompt.key}")
|
|
86
|
-
self._prompts[prompt.key] = prompt
|
|
87
|
-
elif self.duplicate_behavior == "replace":
|
|
88
|
-
self._prompts[prompt.key] = prompt
|
|
89
|
-
elif self.duplicate_behavior == "error":
|
|
90
|
-
raise ValueError(f"Prompt already exists: {prompt.key}")
|
|
91
|
-
elif self.duplicate_behavior == "ignore":
|
|
92
|
-
return existing
|
|
93
|
-
else:
|
|
94
|
-
self._prompts[prompt.key] = prompt
|
|
95
|
-
return prompt
|
|
96
|
-
|
|
97
|
-
async def render_prompt(
|
|
98
|
-
self,
|
|
99
|
-
name: str,
|
|
100
|
-
arguments: dict[str, Any] | None = None,
|
|
101
|
-
) -> GetPromptResult:
|
|
102
|
-
"""
|
|
103
|
-
Internal API for servers: Finds and renders a prompt, respecting the
|
|
104
|
-
filtered protocol path.
|
|
105
|
-
"""
|
|
106
|
-
prompt = await self.get_prompt(name)
|
|
107
|
-
try:
|
|
108
|
-
messages = await prompt.render(arguments)
|
|
109
|
-
return GetPromptResult(description=prompt.description, messages=messages)
|
|
110
|
-
except FastMCPError:
|
|
111
|
-
raise
|
|
112
|
-
except Exception as e:
|
|
113
|
-
logger.exception(f"Error rendering prompt {name!r}")
|
|
114
|
-
if self.mask_error_details:
|
|
115
|
-
raise PromptError(f"Error rendering prompt {name!r}") from e
|
|
116
|
-
else:
|
|
117
|
-
raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
"""Resource manager functionality."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import inspect
|
|
6
|
-
import warnings
|
|
7
|
-
from collections.abc import Callable
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from pydantic import AnyUrl
|
|
11
|
-
|
|
12
|
-
from fastmcp import settings
|
|
13
|
-
from fastmcp.exceptions import FastMCPError, NotFoundError, ResourceError
|
|
14
|
-
from fastmcp.resources.resource import Resource
|
|
15
|
-
from fastmcp.resources.template import (
|
|
16
|
-
ResourceTemplate,
|
|
17
|
-
match_uri_template,
|
|
18
|
-
)
|
|
19
|
-
from fastmcp.settings import DuplicateBehavior
|
|
20
|
-
from fastmcp.utilities.logging import get_logger
|
|
21
|
-
|
|
22
|
-
logger = get_logger(__name__)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class ResourceManager:
|
|
26
|
-
"""Manages FastMCP resources."""
|
|
27
|
-
|
|
28
|
-
def __init__(
|
|
29
|
-
self,
|
|
30
|
-
duplicate_behavior: DuplicateBehavior | None = None,
|
|
31
|
-
mask_error_details: bool | None = None,
|
|
32
|
-
):
|
|
33
|
-
"""Initialize the ResourceManager.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
duplicate_behavior: How to handle duplicate resources
|
|
37
|
-
(warn, error, replace, ignore)
|
|
38
|
-
mask_error_details: Whether to mask error details from exceptions
|
|
39
|
-
other than ResourceError
|
|
40
|
-
"""
|
|
41
|
-
self._resources: dict[str, Resource] = {}
|
|
42
|
-
self._templates: dict[str, ResourceTemplate] = {}
|
|
43
|
-
self.mask_error_details = mask_error_details or settings.mask_error_details
|
|
44
|
-
|
|
45
|
-
# Default to "warn" if None is provided
|
|
46
|
-
if duplicate_behavior is None:
|
|
47
|
-
duplicate_behavior = "warn"
|
|
48
|
-
|
|
49
|
-
if duplicate_behavior not in DuplicateBehavior.__args__:
|
|
50
|
-
raise ValueError(
|
|
51
|
-
f"Invalid duplicate_behavior: {duplicate_behavior}. "
|
|
52
|
-
f"Must be one of: {', '.join(DuplicateBehavior.__args__)}"
|
|
53
|
-
)
|
|
54
|
-
self.duplicate_behavior = duplicate_behavior
|
|
55
|
-
|
|
56
|
-
async def get_resources(self) -> dict[str, Resource]:
|
|
57
|
-
"""Get all registered resources, keyed by URI."""
|
|
58
|
-
return dict(self._resources)
|
|
59
|
-
|
|
60
|
-
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
61
|
-
"""Get all registered templates, keyed by URI template."""
|
|
62
|
-
return dict(self._templates)
|
|
63
|
-
|
|
64
|
-
def add_resource_or_template_from_fn(
|
|
65
|
-
self,
|
|
66
|
-
fn: Callable[..., Any],
|
|
67
|
-
uri: str,
|
|
68
|
-
name: str | None = None,
|
|
69
|
-
description: str | None = None,
|
|
70
|
-
mime_type: str | None = None,
|
|
71
|
-
tags: set[str] | None = None,
|
|
72
|
-
) -> Resource | ResourceTemplate:
|
|
73
|
-
"""Add a resource or template to the manager from a function.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
fn: The function to register as a resource or template
|
|
77
|
-
uri: The URI for the resource or template
|
|
78
|
-
name: Optional name for the resource or template
|
|
79
|
-
description: Optional description of the resource or template
|
|
80
|
-
mime_type: Optional MIME type for the resource or template
|
|
81
|
-
tags: Optional set of tags for categorizing the resource or template
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
The added resource or template. If a resource or template with the same URI already exists,
|
|
85
|
-
returns the existing resource or template.
|
|
86
|
-
"""
|
|
87
|
-
from fastmcp.server.context import Context
|
|
88
|
-
|
|
89
|
-
# Check if this should be a template
|
|
90
|
-
has_uri_params = "{" in uri and "}" in uri
|
|
91
|
-
# check if the function has any parameters (other than injected context)
|
|
92
|
-
has_func_params = any(
|
|
93
|
-
p
|
|
94
|
-
for p in inspect.signature(fn).parameters.values()
|
|
95
|
-
if p.annotation is not Context
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
if has_uri_params or has_func_params:
|
|
99
|
-
return self.add_template_from_fn(
|
|
100
|
-
fn, uri, name, description, mime_type, tags
|
|
101
|
-
)
|
|
102
|
-
elif not has_uri_params and not has_func_params:
|
|
103
|
-
return self.add_resource_from_fn(
|
|
104
|
-
fn, uri, name, description, mime_type, tags
|
|
105
|
-
)
|
|
106
|
-
else:
|
|
107
|
-
raise ValueError(
|
|
108
|
-
"Invalid resource or template definition due to a "
|
|
109
|
-
"mismatch between URI parameters and function parameters."
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
def add_resource_from_fn(
|
|
113
|
-
self,
|
|
114
|
-
fn: Callable[..., Any],
|
|
115
|
-
uri: str,
|
|
116
|
-
name: str | None = None,
|
|
117
|
-
description: str | None = None,
|
|
118
|
-
mime_type: str | None = None,
|
|
119
|
-
tags: set[str] | None = None,
|
|
120
|
-
) -> Resource:
|
|
121
|
-
"""Add a resource to the manager from a function.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
fn: The function to register as a resource
|
|
125
|
-
uri: The URI for the resource
|
|
126
|
-
name: Optional name for the resource
|
|
127
|
-
description: Optional description of the resource
|
|
128
|
-
mime_type: Optional MIME type for the resource
|
|
129
|
-
tags: Optional set of tags for categorizing the resource
|
|
130
|
-
|
|
131
|
-
Returns:
|
|
132
|
-
The added resource. If a resource with the same URI already exists,
|
|
133
|
-
returns the existing resource.
|
|
134
|
-
"""
|
|
135
|
-
# deprecated in 2.7.0
|
|
136
|
-
if settings.deprecation_warnings:
|
|
137
|
-
warnings.warn(
|
|
138
|
-
"add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.",
|
|
139
|
-
DeprecationWarning,
|
|
140
|
-
stacklevel=2,
|
|
141
|
-
)
|
|
142
|
-
resource = Resource.from_function(
|
|
143
|
-
fn=fn,
|
|
144
|
-
uri=uri,
|
|
145
|
-
name=name,
|
|
146
|
-
description=description,
|
|
147
|
-
mime_type=mime_type,
|
|
148
|
-
tags=tags,
|
|
149
|
-
)
|
|
150
|
-
return self.add_resource(resource)
|
|
151
|
-
|
|
152
|
-
def add_resource(self, resource: Resource) -> Resource:
|
|
153
|
-
"""Add a resource to the manager.
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
resource: A Resource instance to add. The resource's .key attribute
|
|
157
|
-
will be used as the storage key. To overwrite it, call
|
|
158
|
-
Resource.model_copy(key=new_key) before calling this method.
|
|
159
|
-
"""
|
|
160
|
-
existing = self._resources.get(resource.key)
|
|
161
|
-
if existing:
|
|
162
|
-
if self.duplicate_behavior == "warn":
|
|
163
|
-
logger.warning(f"Resource already exists: {resource.key}")
|
|
164
|
-
self._resources[resource.key] = resource
|
|
165
|
-
elif self.duplicate_behavior == "replace":
|
|
166
|
-
self._resources[resource.key] = resource
|
|
167
|
-
elif self.duplicate_behavior == "error":
|
|
168
|
-
raise ValueError(f"Resource already exists: {resource.key}")
|
|
169
|
-
elif self.duplicate_behavior == "ignore":
|
|
170
|
-
return existing
|
|
171
|
-
self._resources[resource.key] = resource
|
|
172
|
-
return resource
|
|
173
|
-
|
|
174
|
-
def add_template_from_fn(
|
|
175
|
-
self,
|
|
176
|
-
fn: Callable[..., Any],
|
|
177
|
-
uri_template: str,
|
|
178
|
-
name: str | None = None,
|
|
179
|
-
description: str | None = None,
|
|
180
|
-
mime_type: str | None = None,
|
|
181
|
-
tags: set[str] | None = None,
|
|
182
|
-
) -> ResourceTemplate:
|
|
183
|
-
"""Create a template from a function."""
|
|
184
|
-
# deprecated in 2.7.0
|
|
185
|
-
if settings.deprecation_warnings:
|
|
186
|
-
warnings.warn(
|
|
187
|
-
"add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.",
|
|
188
|
-
DeprecationWarning,
|
|
189
|
-
stacklevel=2,
|
|
190
|
-
)
|
|
191
|
-
template = ResourceTemplate.from_function(
|
|
192
|
-
fn,
|
|
193
|
-
uri_template=uri_template,
|
|
194
|
-
name=name,
|
|
195
|
-
description=description,
|
|
196
|
-
mime_type=mime_type,
|
|
197
|
-
tags=tags,
|
|
198
|
-
)
|
|
199
|
-
return self.add_template(template)
|
|
200
|
-
|
|
201
|
-
def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
|
|
202
|
-
"""Add a template to the manager.
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
template: A ResourceTemplate instance to add. The template's .key attribute
|
|
206
|
-
will be used as the storage key. To overwrite it, call
|
|
207
|
-
ResourceTemplate.model_copy(key=new_key) before calling this method.
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
The added template. If a template with the same URI already exists,
|
|
211
|
-
returns the existing template.
|
|
212
|
-
"""
|
|
213
|
-
existing = self._templates.get(template.key)
|
|
214
|
-
if existing:
|
|
215
|
-
if self.duplicate_behavior == "warn":
|
|
216
|
-
logger.warning(f"Template already exists: {template.key}")
|
|
217
|
-
self._templates[template.key] = template
|
|
218
|
-
elif self.duplicate_behavior == "replace":
|
|
219
|
-
self._templates[template.key] = template
|
|
220
|
-
elif self.duplicate_behavior == "error":
|
|
221
|
-
raise ValueError(f"Template already exists: {template.key}")
|
|
222
|
-
elif self.duplicate_behavior == "ignore":
|
|
223
|
-
return existing
|
|
224
|
-
self._templates[template.key] = template
|
|
225
|
-
return template
|
|
226
|
-
|
|
227
|
-
async def has_resource(self, uri: AnyUrl | str) -> bool:
|
|
228
|
-
"""Check if a resource exists."""
|
|
229
|
-
uri_str = str(uri)
|
|
230
|
-
|
|
231
|
-
# First check concrete resources (local and mounted)
|
|
232
|
-
resources = await self.get_resources()
|
|
233
|
-
if uri_str in resources:
|
|
234
|
-
return True
|
|
235
|
-
|
|
236
|
-
# Then check templates (local and mounted) only if not found in concrete resources
|
|
237
|
-
templates = await self.get_resource_templates()
|
|
238
|
-
for template_key in templates:
|
|
239
|
-
if match_uri_template(uri_str, template_key) is not None:
|
|
240
|
-
return True
|
|
241
|
-
|
|
242
|
-
return False
|
|
243
|
-
|
|
244
|
-
async def get_resource(self, uri: AnyUrl | str) -> Resource:
|
|
245
|
-
"""Get resource by URI, checking concrete resources first, then templates.
|
|
246
|
-
|
|
247
|
-
Args:
|
|
248
|
-
uri: The URI of the resource to get
|
|
249
|
-
|
|
250
|
-
Raises:
|
|
251
|
-
NotFoundError: If no resource or template matching the URI is found.
|
|
252
|
-
"""
|
|
253
|
-
uri_str = str(uri)
|
|
254
|
-
logger.debug("Getting resource", extra={"uri": uri_str})
|
|
255
|
-
|
|
256
|
-
# First check concrete resources
|
|
257
|
-
resources = await self.get_resources()
|
|
258
|
-
if resource := resources.get(uri_str):
|
|
259
|
-
return resource
|
|
260
|
-
|
|
261
|
-
# Then check templates
|
|
262
|
-
templates = await self.get_resource_templates()
|
|
263
|
-
for storage_key, template in templates.items():
|
|
264
|
-
# Try to match against the storage key (which might be a custom key)
|
|
265
|
-
if (params := match_uri_template(uri_str, storage_key)) is not None:
|
|
266
|
-
try:
|
|
267
|
-
return await template.create_resource(
|
|
268
|
-
uri_str,
|
|
269
|
-
params=params,
|
|
270
|
-
)
|
|
271
|
-
# Pass through FastMCPErrors as-is
|
|
272
|
-
except FastMCPError:
|
|
273
|
-
raise
|
|
274
|
-
# Handle other exceptions
|
|
275
|
-
except Exception as e:
|
|
276
|
-
logger.error(f"Error creating resource from template: {e}")
|
|
277
|
-
if self.mask_error_details:
|
|
278
|
-
# Mask internal details
|
|
279
|
-
raise ValueError("Error creating resource from template") from e
|
|
280
|
-
else:
|
|
281
|
-
# Include original error details
|
|
282
|
-
raise ValueError(
|
|
283
|
-
f"Error creating resource from template: {e}"
|
|
284
|
-
) from e
|
|
285
|
-
|
|
286
|
-
raise NotFoundError(f"Unknown resource: {uri_str}")
|
|
287
|
-
|
|
288
|
-
async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
|
|
289
|
-
"""
|
|
290
|
-
Internal API for servers: Finds and reads a resource, respecting the
|
|
291
|
-
filtered protocol path.
|
|
292
|
-
"""
|
|
293
|
-
uri_str = str(uri)
|
|
294
|
-
|
|
295
|
-
# 1. Check local resources first. The server will have already applied its filter.
|
|
296
|
-
if uri_str in self._resources:
|
|
297
|
-
resource = await self.get_resource(uri_str)
|
|
298
|
-
try:
|
|
299
|
-
return await resource.read()
|
|
300
|
-
|
|
301
|
-
# raise FastMCPErrors as-is
|
|
302
|
-
except FastMCPError:
|
|
303
|
-
raise
|
|
304
|
-
|
|
305
|
-
# Handle other exceptions
|
|
306
|
-
except Exception as e:
|
|
307
|
-
logger.exception(f"Error reading resource {uri_str!r}")
|
|
308
|
-
if self.mask_error_details:
|
|
309
|
-
# Mask internal details
|
|
310
|
-
raise ResourceError(f"Error reading resource {uri_str!r}") from e
|
|
311
|
-
else:
|
|
312
|
-
# Include original error details
|
|
313
|
-
raise ResourceError(
|
|
314
|
-
f"Error reading resource {uri_str!r}: {e}"
|
|
315
|
-
) from e
|
|
316
|
-
|
|
317
|
-
# 1b. Check local templates if not found in concrete resources
|
|
318
|
-
for key, template in self._templates.items():
|
|
319
|
-
if (params := match_uri_template(uri_str, key)) is not None:
|
|
320
|
-
try:
|
|
321
|
-
resource = await template.create_resource(uri_str, params=params)
|
|
322
|
-
return await resource.read()
|
|
323
|
-
except FastMCPError:
|
|
324
|
-
raise
|
|
325
|
-
except Exception as e:
|
|
326
|
-
logger.exception(
|
|
327
|
-
f"Error reading resource from template {uri_str!r}"
|
|
328
|
-
)
|
|
329
|
-
if self.mask_error_details:
|
|
330
|
-
raise ResourceError(
|
|
331
|
-
f"Error reading resource from template {uri_str!r}"
|
|
332
|
-
) from e
|
|
333
|
-
else:
|
|
334
|
-
raise ResourceError(
|
|
335
|
-
f"Error reading resource from template {uri_str!r}: {e}"
|
|
336
|
-
) from e
|
|
337
|
-
|
|
338
|
-
raise NotFoundError(f"Resource {uri_str!r} not found.")
|