fastmcp 2.12.5__py3-none-any.whl → 2.14.0__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 -23
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +19 -33
- 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/install/shared.py +0 -15
- fastmcp/cli/run.py +13 -8
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +123 -225
- fastmcp/client/client.py +697 -95
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/logging.py +18 -14
- fastmcp/client/messages.py +7 -5
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +117 -30
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +10 -26
- 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/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +3 -3
- fastmcp/experimental/server/openapi/__init__.py +20 -21
- fastmcp/experimental/utilities/openapi/__init__.py +16 -47
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +54 -51
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +43 -21
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +161 -61
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -14
- fastmcp/server/auth/auth.py +197 -46
- 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 +1469 -298
- fastmcp/server/auth/oidc_proxy.py +91 -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/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 +29 -5
- 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 +236 -116
- fastmcp/server/dependencies.py +503 -18
- fastmcp/server/elicitation.py +286 -48
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +71 -20
- fastmcp/server/low_level.py +165 -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/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +15 -10
- fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +72 -48
- fastmcp/server/server.py +1415 -733
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +125 -113
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +138 -55
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -21
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +10 -5
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +8 -8
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_config.py +1 -2
- 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 +5 -5
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
- fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
- fastmcp/utilities/tests.py +92 -5
- fastmcp/utilities/types.py +86 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
- fastmcp-2.14.0.dist-info/RECORD +156 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1083
- fastmcp/utilities/openapi.py +0 -1568
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
- fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
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,
|
|
@@ -20,10 +19,10 @@ from pydantic import (
|
|
|
20
19
|
)
|
|
21
20
|
from typing_extensions import Self
|
|
22
21
|
|
|
23
|
-
from fastmcp.server.dependencies import get_context
|
|
22
|
+
from fastmcp.server.dependencies import get_context, without_injected_parameters
|
|
23
|
+
from fastmcp.server.tasks.config import TaskConfig
|
|
24
24
|
from fastmcp.utilities.components import FastMCPComponent
|
|
25
25
|
from fastmcp.utilities.types import (
|
|
26
|
-
find_kwarg_by_type,
|
|
27
26
|
get_fn_name,
|
|
28
27
|
)
|
|
29
28
|
|
|
@@ -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)
|
|
@@ -43,7 +42,6 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
43
42
|
mime_type: str = Field(
|
|
44
43
|
default="text/plain",
|
|
45
44
|
description="MIME type of the resource content",
|
|
46
|
-
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
|
47
45
|
)
|
|
48
46
|
annotations: Annotated[
|
|
49
47
|
Annotations | None,
|
|
@@ -73,11 +71,13 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
73
71
|
name: str | None = None,
|
|
74
72
|
title: str | None = None,
|
|
75
73
|
description: str | None = None,
|
|
74
|
+
icons: list[Icon] | None = None,
|
|
76
75
|
mime_type: str | None = None,
|
|
77
76
|
tags: set[str] | None = None,
|
|
78
77
|
enabled: bool | None = None,
|
|
79
78
|
annotations: Annotations | None = None,
|
|
80
79
|
meta: dict[str, Any] | None = None,
|
|
80
|
+
task: bool | TaskConfig | None = None,
|
|
81
81
|
) -> FunctionResource:
|
|
82
82
|
return FunctionResource.from_function(
|
|
83
83
|
fn=fn,
|
|
@@ -85,11 +85,13 @@ 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,
|
|
91
92
|
annotations=annotations,
|
|
92
93
|
meta=meta,
|
|
94
|
+
task=task,
|
|
93
95
|
)
|
|
94
96
|
|
|
95
97
|
@field_validator("mime_type", mode="before")
|
|
@@ -111,10 +113,13 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
111
113
|
raise ValueError("Either name or uri must be provided")
|
|
112
114
|
return self
|
|
113
115
|
|
|
114
|
-
@abc.abstractmethod
|
|
115
116
|
async def read(self) -> str | bytes:
|
|
116
|
-
"""Read the resource content.
|
|
117
|
-
|
|
117
|
+
"""Read the resource content.
|
|
118
|
+
|
|
119
|
+
This method is not implemented in the base Resource class and must be
|
|
120
|
+
implemented by subclasses.
|
|
121
|
+
"""
|
|
122
|
+
raise NotImplementedError("Subclasses must implement read()")
|
|
118
123
|
|
|
119
124
|
def to_mcp_resource(
|
|
120
125
|
self,
|
|
@@ -130,6 +135,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
130
135
|
description=overrides.get("description", self.description),
|
|
131
136
|
mimeType=overrides.get("mimeType", self.mime_type),
|
|
132
137
|
title=overrides.get("title", self.title),
|
|
138
|
+
icons=overrides.get("icons", self.icons),
|
|
133
139
|
annotations=overrides.get("annotations", self.annotations),
|
|
134
140
|
_meta=overrides.get(
|
|
135
141
|
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
@@ -164,6 +170,10 @@ class FunctionResource(Resource):
|
|
|
164
170
|
"""
|
|
165
171
|
|
|
166
172
|
fn: Callable[..., Any]
|
|
173
|
+
task_config: Annotated[
|
|
174
|
+
TaskConfig,
|
|
175
|
+
Field(description="Background task execution configuration (SEP-1686)."),
|
|
176
|
+
] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
|
|
167
177
|
|
|
168
178
|
@classmethod
|
|
169
179
|
def from_function(
|
|
@@ -173,46 +183,58 @@ class FunctionResource(Resource):
|
|
|
173
183
|
name: str | None = None,
|
|
174
184
|
title: str | None = None,
|
|
175
185
|
description: str | None = None,
|
|
186
|
+
icons: list[Icon] | None = None,
|
|
176
187
|
mime_type: str | None = None,
|
|
177
188
|
tags: set[str] | None = None,
|
|
178
189
|
enabled: bool | None = None,
|
|
179
190
|
annotations: Annotations | None = None,
|
|
180
191
|
meta: dict[str, Any] | None = None,
|
|
192
|
+
task: bool | TaskConfig | None = None,
|
|
181
193
|
) -> FunctionResource:
|
|
182
194
|
"""Create a FunctionResource from a function."""
|
|
183
195
|
if isinstance(uri, str):
|
|
184
196
|
uri = AnyUrl(uri)
|
|
197
|
+
|
|
198
|
+
func_name = name or get_fn_name(fn)
|
|
199
|
+
|
|
200
|
+
# Normalize task to TaskConfig and validate
|
|
201
|
+
if task is None:
|
|
202
|
+
task_config = TaskConfig(mode="forbidden")
|
|
203
|
+
elif isinstance(task, bool):
|
|
204
|
+
task_config = TaskConfig.from_bool(task)
|
|
205
|
+
else:
|
|
206
|
+
task_config = task
|
|
207
|
+
task_config.validate_function(fn, func_name)
|
|
208
|
+
|
|
209
|
+
# Wrap fn to handle dependency resolution internally
|
|
210
|
+
wrapped_fn = without_injected_parameters(fn)
|
|
211
|
+
|
|
185
212
|
return cls(
|
|
186
|
-
fn=
|
|
213
|
+
fn=wrapped_fn,
|
|
187
214
|
uri=uri,
|
|
188
215
|
name=name or get_fn_name(fn),
|
|
189
216
|
title=title,
|
|
190
217
|
description=description or inspect.getdoc(fn),
|
|
218
|
+
icons=icons,
|
|
191
219
|
mime_type=mime_type or "text/plain",
|
|
192
220
|
tags=tags or set(),
|
|
193
221
|
enabled=enabled if enabled is not None else True,
|
|
194
222
|
annotations=annotations,
|
|
195
223
|
meta=meta,
|
|
224
|
+
task_config=task_config,
|
|
196
225
|
)
|
|
197
226
|
|
|
198
227
|
async def read(self) -> str | bytes:
|
|
199
228
|
"""Read the resource by calling the wrapped function."""
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
204
|
-
if context_kwarg is not None:
|
|
205
|
-
kwargs[context_kwarg] = get_context()
|
|
206
|
-
|
|
207
|
-
result = self.fn(**kwargs)
|
|
229
|
+
# self.fn is wrapped by without_injected_parameters which handles
|
|
230
|
+
# dependency resolution internally
|
|
231
|
+
result = self.fn()
|
|
208
232
|
if inspect.isawaitable(result):
|
|
209
233
|
result = await result
|
|
210
234
|
|
|
211
235
|
if isinstance(result, Resource):
|
|
212
236
|
return await result.read()
|
|
213
|
-
elif isinstance(result, bytes):
|
|
214
|
-
return result
|
|
215
|
-
elif isinstance(result, str):
|
|
237
|
+
elif isinstance(result, bytes | str):
|
|
216
238
|
return result
|
|
217
239
|
else:
|
|
218
240
|
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.")
|