fastmcp 2.12.5__py3-none-any.whl → 2.13.2__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/__init__.py +2 -2
- fastmcp/cli/cli.py +11 -11
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +115 -217
- fastmcp/client/client.py +105 -39
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +80 -25
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +14 -15
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +22 -19
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +14 -9
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +70 -43
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1510 -289
- fastmcp/server/auth/oidc_proxy.py +84 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +177 -51
- fastmcp/server/dependencies.py +39 -12
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +56 -17
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +725 -242
- fastmcp/settings.py +24 -10
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +70 -23
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -10
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +85 -4
- fastmcp/utilities/types.py +78 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/prompts/prompt.py
CHANGED
|
@@ -4,12 +4,11 @@ from __future__ import annotations as _annotations
|
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
6
|
import json
|
|
7
|
-
from abc import ABC, abstractmethod
|
|
8
7
|
from collections.abc import Awaitable, Callable, Sequence
|
|
9
8
|
from typing import Any
|
|
10
9
|
|
|
11
10
|
import pydantic_core
|
|
12
|
-
from mcp.types import ContentBlock, PromptMessage, Role, TextContent
|
|
11
|
+
from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
|
|
13
12
|
from mcp.types import Prompt as MCPPrompt
|
|
14
13
|
from mcp.types import PromptArgument as MCPPromptArgument
|
|
15
14
|
from pydantic import Field, TypeAdapter
|
|
@@ -62,7 +61,7 @@ class PromptArgument(FastMCPBaseModel):
|
|
|
62
61
|
)
|
|
63
62
|
|
|
64
63
|
|
|
65
|
-
class Prompt(FastMCPComponent
|
|
64
|
+
class Prompt(FastMCPComponent):
|
|
66
65
|
"""A prompt template that can be rendered with parameters."""
|
|
67
66
|
|
|
68
67
|
arguments: list[PromptArgument] | None = Field(
|
|
@@ -106,6 +105,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
106
105
|
description=overrides.get("description", self.description),
|
|
107
106
|
arguments=arguments,
|
|
108
107
|
title=overrides.get("title", self.title),
|
|
108
|
+
icons=overrides.get("icons", self.icons),
|
|
109
109
|
_meta=overrides.get(
|
|
110
110
|
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
111
111
|
),
|
|
@@ -117,6 +117,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
117
117
|
name: str | None = None,
|
|
118
118
|
title: str | None = None,
|
|
119
119
|
description: str | None = None,
|
|
120
|
+
icons: list[Icon] | None = None,
|
|
120
121
|
tags: set[str] | None = None,
|
|
121
122
|
enabled: bool | None = None,
|
|
122
123
|
meta: dict[str, Any] | None = None,
|
|
@@ -134,18 +135,22 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
134
135
|
name=name,
|
|
135
136
|
title=title,
|
|
136
137
|
description=description,
|
|
138
|
+
icons=icons,
|
|
137
139
|
tags=tags,
|
|
138
140
|
enabled=enabled,
|
|
139
141
|
meta=meta,
|
|
140
142
|
)
|
|
141
143
|
|
|
142
|
-
@abstractmethod
|
|
143
144
|
async def render(
|
|
144
145
|
self,
|
|
145
146
|
arguments: dict[str, Any] | None = None,
|
|
146
147
|
) -> list[PromptMessage]:
|
|
147
|
-
"""Render the prompt with arguments.
|
|
148
|
-
|
|
148
|
+
"""Render the prompt with arguments.
|
|
149
|
+
|
|
150
|
+
This method is not implemented in the base Prompt class and must be
|
|
151
|
+
implemented by subclasses.
|
|
152
|
+
"""
|
|
153
|
+
raise NotImplementedError("Subclasses must implement render()")
|
|
149
154
|
|
|
150
155
|
|
|
151
156
|
class FunctionPrompt(Prompt):
|
|
@@ -160,6 +165,7 @@ class FunctionPrompt(Prompt):
|
|
|
160
165
|
name: str | None = None,
|
|
161
166
|
title: str | None = None,
|
|
162
167
|
description: str | None = None,
|
|
168
|
+
icons: list[Icon] | None = None,
|
|
163
169
|
tags: set[str] | None = None,
|
|
164
170
|
enabled: bool | None = None,
|
|
165
171
|
meta: dict[str, Any] | None = None,
|
|
@@ -201,10 +207,7 @@ class FunctionPrompt(Prompt):
|
|
|
201
207
|
# Auto-detect context parameter if not provided
|
|
202
208
|
|
|
203
209
|
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
204
|
-
if context_kwarg
|
|
205
|
-
prune_params = [context_kwarg]
|
|
206
|
-
else:
|
|
207
|
-
prune_params = None
|
|
210
|
+
prune_params = [context_kwarg] if context_kwarg else None
|
|
208
211
|
|
|
209
212
|
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
210
213
|
|
|
@@ -253,6 +256,7 @@ class FunctionPrompt(Prompt):
|
|
|
253
256
|
name=func_name,
|
|
254
257
|
title=title,
|
|
255
258
|
description=description,
|
|
259
|
+
icons=icons,
|
|
256
260
|
arguments=arguments,
|
|
257
261
|
tags=tags or set(),
|
|
258
262
|
enabled=enabled if enabled is not None else True,
|
|
@@ -283,10 +287,7 @@ class FunctionPrompt(Prompt):
|
|
|
283
287
|
if (
|
|
284
288
|
param.annotation == inspect.Parameter.empty
|
|
285
289
|
or param.annotation is str
|
|
286
|
-
):
|
|
287
|
-
converted_kwargs[param_name] = param_value
|
|
288
|
-
# If argument is not a string, pass as-is (already properly typed)
|
|
289
|
-
elif not isinstance(param_value, str):
|
|
290
|
+
) or not isinstance(param_value, str):
|
|
290
291
|
converted_kwargs[param_name] = param_value
|
|
291
292
|
else:
|
|
292
293
|
# Try to convert string argument using type adapter
|
|
@@ -307,7 +308,7 @@ class FunctionPrompt(Prompt):
|
|
|
307
308
|
raise PromptError(
|
|
308
309
|
f"Could not convert argument '{param_name}' with value '{param_value}' "
|
|
309
310
|
f"to expected type {param.annotation}. Error: {e}"
|
|
310
|
-
)
|
|
311
|
+
) from e
|
|
311
312
|
else:
|
|
312
313
|
# Parameter not in function signature, pass as-is
|
|
313
314
|
converted_kwargs[param_name] = param_value
|
|
@@ -369,10 +370,12 @@ class FunctionPrompt(Prompt):
|
|
|
369
370
|
content=TextContent(type="text", text=content),
|
|
370
371
|
)
|
|
371
372
|
)
|
|
372
|
-
except Exception:
|
|
373
|
-
raise PromptError(
|
|
373
|
+
except Exception as e:
|
|
374
|
+
raise PromptError(
|
|
375
|
+
"Could not convert prompt result to message."
|
|
376
|
+
) from e
|
|
374
377
|
|
|
375
378
|
return messages
|
|
376
|
-
except Exception:
|
|
379
|
+
except Exception as e:
|
|
377
380
|
logger.exception(f"Error rendering prompt {self.name}")
|
|
378
|
-
raise PromptError(f"Error rendering prompt {self.name}.")
|
|
381
|
+
raise PromptError(f"Error rendering prompt {self.name}.") from e
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations as _annotations
|
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
4
|
from collections.abc import Awaitable, Callable
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
from mcp import GetPromptResult
|
|
8
8
|
|
|
@@ -12,9 +12,6 @@ from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
|
|
|
12
12
|
from fastmcp.settings import DuplicateBehavior
|
|
13
13
|
from fastmcp.utilities.logging import get_logger
|
|
14
14
|
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from fastmcp.server.server import MountedServer
|
|
17
|
-
|
|
18
15
|
logger = get_logger(__name__)
|
|
19
16
|
|
|
20
17
|
|
|
@@ -27,7 +24,6 @@ class PromptManager:
|
|
|
27
24
|
mask_error_details: bool | None = None,
|
|
28
25
|
):
|
|
29
26
|
self._prompts: dict[str, Prompt] = {}
|
|
30
|
-
self._mounted_servers: list[MountedServer] = []
|
|
31
27
|
self.mask_error_details = mask_error_details or settings.mask_error_details
|
|
32
28
|
|
|
33
29
|
# Default to "warn" if None is provided
|
|
@@ -42,52 +38,6 @@ class PromptManager:
|
|
|
42
38
|
|
|
43
39
|
self.duplicate_behavior = duplicate_behavior
|
|
44
40
|
|
|
45
|
-
def mount(self, server: MountedServer) -> None:
|
|
46
|
-
"""Adds a mounted server as a source for prompts."""
|
|
47
|
-
self._mounted_servers.append(server)
|
|
48
|
-
|
|
49
|
-
async def _load_prompts(self, *, via_server: bool = False) -> dict[str, Prompt]:
|
|
50
|
-
"""
|
|
51
|
-
The single, consolidated recursive method for fetching prompts. The 'via_server'
|
|
52
|
-
parameter determines the communication path.
|
|
53
|
-
|
|
54
|
-
- via_server=False: Manager-to-manager path for complete, unfiltered inventory
|
|
55
|
-
- via_server=True: Server-to-server path for filtered MCP requests
|
|
56
|
-
"""
|
|
57
|
-
all_prompts: dict[str, Prompt] = {}
|
|
58
|
-
|
|
59
|
-
for mounted in self._mounted_servers:
|
|
60
|
-
try:
|
|
61
|
-
if via_server:
|
|
62
|
-
# Use the server-to-server filtered path
|
|
63
|
-
child_results = await mounted.server._list_prompts()
|
|
64
|
-
else:
|
|
65
|
-
# Use the manager-to-manager unfiltered path
|
|
66
|
-
child_results = await mounted.server._prompt_manager.list_prompts()
|
|
67
|
-
|
|
68
|
-
# The combination logic is the same for both paths
|
|
69
|
-
child_dict = {p.key: p for p in child_results}
|
|
70
|
-
if mounted.prefix:
|
|
71
|
-
for prompt in child_dict.values():
|
|
72
|
-
prefixed_prompt = prompt.model_copy(
|
|
73
|
-
key=f"{mounted.prefix}_{prompt.key}"
|
|
74
|
-
)
|
|
75
|
-
all_prompts[prefixed_prompt.key] = prefixed_prompt
|
|
76
|
-
else:
|
|
77
|
-
all_prompts.update(child_dict)
|
|
78
|
-
except Exception as e:
|
|
79
|
-
# Skip failed mounts silently, matches existing behavior
|
|
80
|
-
logger.warning(
|
|
81
|
-
f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
82
|
-
)
|
|
83
|
-
if settings.mounted_components_raise_on_load_error:
|
|
84
|
-
raise
|
|
85
|
-
continue
|
|
86
|
-
|
|
87
|
-
# Finally, add local prompts, which always take precedence
|
|
88
|
-
all_prompts.update(self._prompts)
|
|
89
|
-
return all_prompts
|
|
90
|
-
|
|
91
41
|
async def has_prompt(self, key: str) -> bool:
|
|
92
42
|
"""Check if a prompt exists."""
|
|
93
43
|
prompts = await self.get_prompts()
|
|
@@ -102,16 +52,9 @@ class PromptManager:
|
|
|
102
52
|
|
|
103
53
|
async def get_prompts(self) -> dict[str, Prompt]:
|
|
104
54
|
"""
|
|
105
|
-
Gets the complete, unfiltered inventory of
|
|
106
|
-
"""
|
|
107
|
-
return await self._load_prompts(via_server=False)
|
|
108
|
-
|
|
109
|
-
async def list_prompts(self) -> list[Prompt]:
|
|
110
|
-
"""
|
|
111
|
-
Lists all prompts, applying protocol filtering.
|
|
55
|
+
Gets the complete, unfiltered inventory of local prompts.
|
|
112
56
|
"""
|
|
113
|
-
|
|
114
|
-
return list(prompts_dict.values())
|
|
57
|
+
return dict(self._prompts)
|
|
115
58
|
|
|
116
59
|
def add_prompt_from_fn(
|
|
117
60
|
self,
|
|
@@ -160,44 +103,16 @@ class PromptManager:
|
|
|
160
103
|
Internal API for servers: Finds and renders a prompt, respecting the
|
|
161
104
|
filtered protocol path.
|
|
162
105
|
"""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
except PromptError as e:
|
|
177
|
-
logger.exception(f"Error rendering prompt {name!r}")
|
|
178
|
-
raise e
|
|
179
|
-
|
|
180
|
-
# Handle other exceptions
|
|
181
|
-
except Exception as e:
|
|
182
|
-
logger.exception(f"Error rendering prompt {name!r}")
|
|
183
|
-
if self.mask_error_details:
|
|
184
|
-
# Mask internal details
|
|
185
|
-
raise PromptError(f"Error rendering prompt {name!r}") from e
|
|
186
|
-
else:
|
|
187
|
-
# Include original error details
|
|
188
|
-
raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
|
|
189
|
-
|
|
190
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
191
|
-
for mounted in reversed(self._mounted_servers):
|
|
192
|
-
prompt_key = name
|
|
193
|
-
if mounted.prefix:
|
|
194
|
-
if name.startswith(f"{mounted.prefix}_"):
|
|
195
|
-
prompt_key = name.removeprefix(f"{mounted.prefix}_")
|
|
196
|
-
else:
|
|
197
|
-
continue
|
|
198
|
-
try:
|
|
199
|
-
return await mounted.server._get_prompt(prompt_key, arguments)
|
|
200
|
-
except NotFoundError:
|
|
201
|
-
continue
|
|
202
|
-
|
|
203
|
-
raise NotFoundError(f"Unknown prompt: {name}")
|
|
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 PromptError as e:
|
|
111
|
+
logger.exception(f"Error rendering prompt {name!r}")
|
|
112
|
+
raise e
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.exception(f"Error rendering prompt {name!r}")
|
|
115
|
+
if self.mask_error_details:
|
|
116
|
+
raise PromptError(f"Error rendering prompt {name!r}") from e
|
|
117
|
+
else:
|
|
118
|
+
raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
|
fastmcp/resources/__init__.py
CHANGED
|
@@ -10,13 +10,13 @@ from .types import (
|
|
|
10
10
|
from .resource_manager import ResourceManager
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
|
-
"Resource",
|
|
14
|
-
"TextResource",
|
|
15
13
|
"BinaryResource",
|
|
16
|
-
"
|
|
14
|
+
"DirectoryResource",
|
|
17
15
|
"FileResource",
|
|
16
|
+
"FunctionResource",
|
|
18
17
|
"HttpResource",
|
|
19
|
-
"
|
|
20
|
-
"ResourceTemplate",
|
|
18
|
+
"Resource",
|
|
21
19
|
"ResourceManager",
|
|
20
|
+
"ResourceTemplate",
|
|
21
|
+
"TextResource",
|
|
22
22
|
]
|
fastmcp/resources/resource.py
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import abc
|
|
6
5
|
import inspect
|
|
7
6
|
from collections.abc import Callable
|
|
8
7
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
9
8
|
|
|
10
9
|
import pydantic_core
|
|
11
|
-
from mcp.types import Annotations
|
|
10
|
+
from mcp.types import Annotations, Icon
|
|
12
11
|
from mcp.types import Resource as MCPResource
|
|
13
12
|
from pydantic import (
|
|
14
13
|
AnyUrl,
|
|
@@ -31,7 +30,7 @@ if TYPE_CHECKING:
|
|
|
31
30
|
pass
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
class Resource(FastMCPComponent
|
|
33
|
+
class Resource(FastMCPComponent):
|
|
35
34
|
"""Base class for all resources."""
|
|
36
35
|
|
|
37
36
|
model_config = ConfigDict(validate_default=True)
|
|
@@ -73,6 +72,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
73
72
|
name: str | None = None,
|
|
74
73
|
title: str | None = None,
|
|
75
74
|
description: str | None = None,
|
|
75
|
+
icons: list[Icon] | None = None,
|
|
76
76
|
mime_type: str | None = None,
|
|
77
77
|
tags: set[str] | None = None,
|
|
78
78
|
enabled: bool | None = None,
|
|
@@ -85,6 +85,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
85
85
|
name=name,
|
|
86
86
|
title=title,
|
|
87
87
|
description=description,
|
|
88
|
+
icons=icons,
|
|
88
89
|
mime_type=mime_type,
|
|
89
90
|
tags=tags,
|
|
90
91
|
enabled=enabled,
|
|
@@ -111,10 +112,13 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
111
112
|
raise ValueError("Either name or uri must be provided")
|
|
112
113
|
return self
|
|
113
114
|
|
|
114
|
-
@abc.abstractmethod
|
|
115
115
|
async def read(self) -> str | bytes:
|
|
116
|
-
"""Read the resource content.
|
|
117
|
-
|
|
116
|
+
"""Read the resource content.
|
|
117
|
+
|
|
118
|
+
This method is not implemented in the base Resource class and must be
|
|
119
|
+
implemented by subclasses.
|
|
120
|
+
"""
|
|
121
|
+
raise NotImplementedError("Subclasses must implement read()")
|
|
118
122
|
|
|
119
123
|
def to_mcp_resource(
|
|
120
124
|
self,
|
|
@@ -130,6 +134,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
130
134
|
description=overrides.get("description", self.description),
|
|
131
135
|
mimeType=overrides.get("mimeType", self.mime_type),
|
|
132
136
|
title=overrides.get("title", self.title),
|
|
137
|
+
icons=overrides.get("icons", self.icons),
|
|
133
138
|
annotations=overrides.get("annotations", self.annotations),
|
|
134
139
|
_meta=overrides.get(
|
|
135
140
|
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
@@ -173,6 +178,7 @@ class FunctionResource(Resource):
|
|
|
173
178
|
name: str | None = None,
|
|
174
179
|
title: str | None = None,
|
|
175
180
|
description: str | None = None,
|
|
181
|
+
icons: list[Icon] | None = None,
|
|
176
182
|
mime_type: str | None = None,
|
|
177
183
|
tags: set[str] | None = None,
|
|
178
184
|
enabled: bool | None = None,
|
|
@@ -188,6 +194,7 @@ class FunctionResource(Resource):
|
|
|
188
194
|
name=name or get_fn_name(fn),
|
|
189
195
|
title=title,
|
|
190
196
|
description=description or inspect.getdoc(fn),
|
|
197
|
+
icons=icons,
|
|
191
198
|
mime_type=mime_type or "text/plain",
|
|
192
199
|
tags=tags or set(),
|
|
193
200
|
enabled=enabled if enabled is not None else True,
|
|
@@ -210,9 +217,7 @@ class FunctionResource(Resource):
|
|
|
210
217
|
|
|
211
218
|
if isinstance(result, Resource):
|
|
212
219
|
return await result.read()
|
|
213
|
-
elif isinstance(result, bytes):
|
|
214
|
-
return result
|
|
215
|
-
elif isinstance(result, str):
|
|
220
|
+
elif isinstance(result, bytes | str):
|
|
216
221
|
return result
|
|
217
222
|
else:
|
|
218
223
|
return pydantic_core.to_json(result, fallback=str).decode()
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import warnings
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
from pydantic import AnyUrl
|
|
11
11
|
|
|
@@ -19,9 +19,6 @@ from fastmcp.resources.template import (
|
|
|
19
19
|
from fastmcp.settings import DuplicateBehavior
|
|
20
20
|
from fastmcp.utilities.logging import get_logger
|
|
21
21
|
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
from fastmcp.server.server import MountedServer
|
|
24
|
-
|
|
25
22
|
logger = get_logger(__name__)
|
|
26
23
|
|
|
27
24
|
|
|
@@ -43,7 +40,6 @@ class ResourceManager:
|
|
|
43
40
|
"""
|
|
44
41
|
self._resources: dict[str, Resource] = {}
|
|
45
42
|
self._templates: dict[str, ResourceTemplate] = {}
|
|
46
|
-
self._mounted_servers: list[MountedServer] = []
|
|
47
43
|
self.mask_error_details = mask_error_details or settings.mask_error_details
|
|
48
44
|
|
|
49
45
|
# Default to "warn" if None is provided
|
|
@@ -57,137 +53,13 @@ class ResourceManager:
|
|
|
57
53
|
)
|
|
58
54
|
self.duplicate_behavior = duplicate_behavior
|
|
59
55
|
|
|
60
|
-
def mount(self, server: MountedServer) -> None:
|
|
61
|
-
"""Adds a mounted server as a source for resources and templates."""
|
|
62
|
-
self._mounted_servers.append(server)
|
|
63
|
-
|
|
64
56
|
async def get_resources(self) -> dict[str, Resource]:
|
|
65
57
|
"""Get all registered resources, keyed by URI."""
|
|
66
|
-
return
|
|
58
|
+
return dict(self._resources)
|
|
67
59
|
|
|
68
60
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
69
61
|
"""Get all registered templates, keyed by URI template."""
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
async def _load_resources(self, *, via_server: bool = False) -> dict[str, Resource]:
|
|
73
|
-
"""
|
|
74
|
-
The single, consolidated recursive method for fetching resources. The 'via_server'
|
|
75
|
-
parameter determines the communication path.
|
|
76
|
-
|
|
77
|
-
- via_server=False: Manager-to-manager path for complete, unfiltered inventory
|
|
78
|
-
- via_server=True: Server-to-server path for filtered MCP requests
|
|
79
|
-
"""
|
|
80
|
-
all_resources: dict[str, Resource] = {}
|
|
81
|
-
|
|
82
|
-
for mounted in self._mounted_servers:
|
|
83
|
-
try:
|
|
84
|
-
if via_server:
|
|
85
|
-
# Use the server-to-server filtered path
|
|
86
|
-
child_resources_list = await mounted.server._list_resources()
|
|
87
|
-
child_resources = {
|
|
88
|
-
resource.key: resource for resource in child_resources_list
|
|
89
|
-
}
|
|
90
|
-
else:
|
|
91
|
-
# Use the manager-to-manager unfiltered path
|
|
92
|
-
child_resources = (
|
|
93
|
-
await mounted.server._resource_manager.get_resources()
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
# Apply prefix if needed
|
|
97
|
-
if mounted.prefix:
|
|
98
|
-
from fastmcp.server.server import add_resource_prefix
|
|
99
|
-
|
|
100
|
-
for uri, resource in child_resources.items():
|
|
101
|
-
prefixed_uri = add_resource_prefix(
|
|
102
|
-
uri, mounted.prefix, mounted.resource_prefix_format
|
|
103
|
-
)
|
|
104
|
-
# Create a copy of the resource with the prefixed key and name
|
|
105
|
-
prefixed_resource = resource.model_copy(
|
|
106
|
-
update={"name": f"{mounted.prefix}_{resource.name}"},
|
|
107
|
-
key=prefixed_uri,
|
|
108
|
-
)
|
|
109
|
-
all_resources[prefixed_uri] = prefixed_resource
|
|
110
|
-
else:
|
|
111
|
-
all_resources.update(child_resources)
|
|
112
|
-
except Exception as e:
|
|
113
|
-
# Skip failed mounts silently, matches existing behavior
|
|
114
|
-
logger.warning(
|
|
115
|
-
f"Failed to get resources from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
116
|
-
)
|
|
117
|
-
if settings.mounted_components_raise_on_load_error:
|
|
118
|
-
raise
|
|
119
|
-
continue
|
|
120
|
-
|
|
121
|
-
# Finally, add local resources, which always take precedence
|
|
122
|
-
all_resources.update(self._resources)
|
|
123
|
-
return all_resources
|
|
124
|
-
|
|
125
|
-
async def _load_resource_templates(
|
|
126
|
-
self, *, via_server: bool = False
|
|
127
|
-
) -> dict[str, ResourceTemplate]:
|
|
128
|
-
"""
|
|
129
|
-
The single, consolidated recursive method for fetching templates. The 'via_server'
|
|
130
|
-
parameter determines the communication path.
|
|
131
|
-
|
|
132
|
-
- via_server=False: Manager-to-manager path for complete, unfiltered inventory
|
|
133
|
-
- via_server=True: Server-to-server path for filtered MCP requests
|
|
134
|
-
"""
|
|
135
|
-
all_templates: dict[str, ResourceTemplate] = {}
|
|
136
|
-
|
|
137
|
-
for mounted in self._mounted_servers:
|
|
138
|
-
try:
|
|
139
|
-
if via_server:
|
|
140
|
-
# Use the server-to-server filtered path
|
|
141
|
-
child_templates = await mounted.server._list_resource_templates()
|
|
142
|
-
else:
|
|
143
|
-
# Use the manager-to-manager unfiltered path
|
|
144
|
-
child_templates = (
|
|
145
|
-
await mounted.server._resource_manager.list_resource_templates()
|
|
146
|
-
)
|
|
147
|
-
child_dict = {template.key: template for template in child_templates}
|
|
148
|
-
|
|
149
|
-
# Apply prefix if needed
|
|
150
|
-
if mounted.prefix:
|
|
151
|
-
from fastmcp.server.server import add_resource_prefix
|
|
152
|
-
|
|
153
|
-
for uri_template, template in child_dict.items():
|
|
154
|
-
prefixed_uri_template = add_resource_prefix(
|
|
155
|
-
uri_template, mounted.prefix, mounted.resource_prefix_format
|
|
156
|
-
)
|
|
157
|
-
# Create a copy of the template with the prefixed key and name
|
|
158
|
-
prefixed_template = template.model_copy(
|
|
159
|
-
update={"name": f"{mounted.prefix}_{template.name}"},
|
|
160
|
-
key=prefixed_uri_template,
|
|
161
|
-
)
|
|
162
|
-
all_templates[prefixed_uri_template] = prefixed_template
|
|
163
|
-
else:
|
|
164
|
-
all_templates.update(child_dict)
|
|
165
|
-
except Exception as e:
|
|
166
|
-
# Skip failed mounts silently, matches existing behavior
|
|
167
|
-
logger.warning(
|
|
168
|
-
f"Failed to get templates from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
169
|
-
)
|
|
170
|
-
if settings.mounted_components_raise_on_load_error:
|
|
171
|
-
raise
|
|
172
|
-
continue
|
|
173
|
-
|
|
174
|
-
# Finally, add local templates, which always take precedence
|
|
175
|
-
all_templates.update(self._templates)
|
|
176
|
-
return all_templates
|
|
177
|
-
|
|
178
|
-
async def list_resources(self) -> list[Resource]:
|
|
179
|
-
"""
|
|
180
|
-
Lists all resources, applying protocol filtering.
|
|
181
|
-
"""
|
|
182
|
-
resources_dict = await self._load_resources(via_server=True)
|
|
183
|
-
return list(resources_dict.values())
|
|
184
|
-
|
|
185
|
-
async def list_resource_templates(self) -> list[ResourceTemplate]:
|
|
186
|
-
"""
|
|
187
|
-
Lists all templates, applying protocol filtering.
|
|
188
|
-
"""
|
|
189
|
-
templates_dict = await self._load_resource_templates(via_server=True)
|
|
190
|
-
return list(templates_dict.values())
|
|
62
|
+
return dict(self._templates)
|
|
191
63
|
|
|
192
64
|
def add_resource_or_template_from_fn(
|
|
193
65
|
self,
|
|
@@ -363,8 +235,8 @@ class ResourceManager:
|
|
|
363
235
|
|
|
364
236
|
# Then check templates (local and mounted) only if not found in concrete resources
|
|
365
237
|
templates = await self.get_resource_templates()
|
|
366
|
-
for template_key in templates
|
|
367
|
-
if match_uri_template(uri_str, template_key):
|
|
238
|
+
for template_key in templates:
|
|
239
|
+
if match_uri_template(uri_str, template_key) is not None:
|
|
368
240
|
return True
|
|
369
241
|
|
|
370
242
|
return False
|
|
@@ -381,16 +253,16 @@ class ResourceManager:
|
|
|
381
253
|
uri_str = str(uri)
|
|
382
254
|
logger.debug("Getting resource", extra={"uri": uri_str})
|
|
383
255
|
|
|
384
|
-
# First check concrete resources
|
|
256
|
+
# First check concrete resources
|
|
385
257
|
resources = await self.get_resources()
|
|
386
258
|
if resource := resources.get(uri_str):
|
|
387
259
|
return resource
|
|
388
260
|
|
|
389
|
-
# Then check templates
|
|
261
|
+
# Then check templates
|
|
390
262
|
templates = await self.get_resource_templates()
|
|
391
263
|
for storage_key, template in templates.items():
|
|
392
264
|
# Try to match against the storage key (which might be a custom key)
|
|
393
|
-
if params := match_uri_template(uri_str, storage_key):
|
|
265
|
+
if (params := match_uri_template(uri_str, storage_key)) is not None:
|
|
394
266
|
try:
|
|
395
267
|
return await template.create_resource(
|
|
396
268
|
uri_str,
|
|
@@ -424,9 +296,6 @@ class ResourceManager:
|
|
|
424
296
|
# 1. Check local resources first. The server will have already applied its filter.
|
|
425
297
|
if uri_str in self._resources:
|
|
426
298
|
resource = await self.get_resource(uri_str)
|
|
427
|
-
if not resource:
|
|
428
|
-
raise NotFoundError(f"Resource {uri_str!r} not found")
|
|
429
|
-
|
|
430
299
|
try:
|
|
431
300
|
return await resource.read()
|
|
432
301
|
|
|
@@ -449,7 +318,7 @@ class ResourceManager:
|
|
|
449
318
|
|
|
450
319
|
# 1b. Check local templates if not found in concrete resources
|
|
451
320
|
for key, template in self._templates.items():
|
|
452
|
-
if params := match_uri_template(uri_str, key):
|
|
321
|
+
if (params := match_uri_template(uri_str, key)) is not None:
|
|
453
322
|
try:
|
|
454
323
|
resource = await template.create_resource(uri_str, params=params)
|
|
455
324
|
return await resource.read()
|
|
@@ -471,32 +340,4 @@ class ResourceManager:
|
|
|
471
340
|
f"Error reading resource from template {uri_str!r}: {e}"
|
|
472
341
|
) from e
|
|
473
342
|
|
|
474
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
475
|
-
from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
|
|
476
|
-
|
|
477
|
-
for mounted in reversed(self._mounted_servers):
|
|
478
|
-
key = uri_str
|
|
479
|
-
try:
|
|
480
|
-
if mounted.prefix:
|
|
481
|
-
if has_resource_prefix(
|
|
482
|
-
key,
|
|
483
|
-
mounted.prefix,
|
|
484
|
-
mounted.resource_prefix_format,
|
|
485
|
-
):
|
|
486
|
-
key = remove_resource_prefix(
|
|
487
|
-
key,
|
|
488
|
-
mounted.prefix,
|
|
489
|
-
mounted.resource_prefix_format,
|
|
490
|
-
)
|
|
491
|
-
else:
|
|
492
|
-
continue
|
|
493
|
-
|
|
494
|
-
try:
|
|
495
|
-
result = await mounted.server._read_resource(key)
|
|
496
|
-
return result[0].content
|
|
497
|
-
except NotFoundError:
|
|
498
|
-
continue
|
|
499
|
-
except NotFoundError:
|
|
500
|
-
continue
|
|
501
|
-
|
|
502
343
|
raise NotFoundError(f"Resource {uri_str!r} not found.")
|