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
fastmcp/resources/resource.py
CHANGED
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import base64
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
-
from typing import TYPE_CHECKING, Annotated, Any
|
|
7
|
+
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, overload
|
|
8
8
|
|
|
9
|
+
import mcp.types
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from docket import Docket
|
|
13
|
+
from docket.execution import Execution
|
|
14
|
+
|
|
15
|
+
from fastmcp.resources.function_resource import FunctionResource
|
|
16
|
+
|
|
17
|
+
import pydantic
|
|
9
18
|
import pydantic_core
|
|
10
19
|
from mcp.types import Annotations, Icon
|
|
11
20
|
from mcp.types import Resource as SDKResource
|
|
@@ -19,20 +28,189 @@ from pydantic import (
|
|
|
19
28
|
)
|
|
20
29
|
from typing_extensions import Self
|
|
21
30
|
|
|
22
|
-
from fastmcp.server.
|
|
23
|
-
from fastmcp.
|
|
31
|
+
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
|
|
32
|
+
from fastmcp.tools.tool import AuthCheckCallable
|
|
24
33
|
from fastmcp.utilities.components import FastMCPComponent
|
|
25
|
-
from fastmcp.utilities.types import (
|
|
26
|
-
get_fn_name,
|
|
27
|
-
)
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
|
|
36
|
+
class ResourceContent(pydantic.BaseModel):
|
|
37
|
+
"""Wrapper for resource content with optional MIME type and metadata.
|
|
38
|
+
|
|
39
|
+
Accepts any value for content - strings and bytes pass through directly,
|
|
40
|
+
other types (dict, list, BaseModel, etc.) are automatically JSON-serialized.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
```python
|
|
44
|
+
from fastmcp.resources import ResourceContent
|
|
45
|
+
|
|
46
|
+
# String content
|
|
47
|
+
ResourceContent("plain text")
|
|
48
|
+
|
|
49
|
+
# Binary content
|
|
50
|
+
ResourceContent(b"binary data", mime_type="application/octet-stream")
|
|
51
|
+
|
|
52
|
+
# Auto-serialized to JSON
|
|
53
|
+
ResourceContent({"key": "value"})
|
|
54
|
+
ResourceContent(["a", "b", "c"])
|
|
55
|
+
```
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
content: str | bytes
|
|
59
|
+
mime_type: str | None = None
|
|
60
|
+
meta: dict[str, Any] | None = None
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
content: Any,
|
|
65
|
+
mime_type: str | None = None,
|
|
66
|
+
meta: dict[str, Any] | None = None,
|
|
67
|
+
):
|
|
68
|
+
"""Create ResourceContent with automatic serialization.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
content: The content value. str and bytes pass through directly.
|
|
72
|
+
Other types (dict, list, BaseModel) are JSON-serialized.
|
|
73
|
+
mime_type: Optional MIME type. Defaults based on content type:
|
|
74
|
+
str → "text/plain", bytes → "application/octet-stream",
|
|
75
|
+
other → "application/json"
|
|
76
|
+
meta: Optional metadata dictionary.
|
|
77
|
+
"""
|
|
78
|
+
if isinstance(content, str):
|
|
79
|
+
normalized_content: str | bytes = content
|
|
80
|
+
mime_type = mime_type or "text/plain"
|
|
81
|
+
elif isinstance(content, bytes):
|
|
82
|
+
normalized_content = content
|
|
83
|
+
mime_type = mime_type or "application/octet-stream"
|
|
84
|
+
else:
|
|
85
|
+
# dict, list, BaseModel, etc → JSON
|
|
86
|
+
normalized_content = pydantic_core.to_json(content, fallback=str).decode()
|
|
87
|
+
mime_type = mime_type or "application/json"
|
|
88
|
+
|
|
89
|
+
super().__init__(content=normalized_content, mime_type=mime_type, meta=meta)
|
|
90
|
+
|
|
91
|
+
def to_mcp_resource_contents(
|
|
92
|
+
self, uri: AnyUrl | str
|
|
93
|
+
) -> mcp.types.TextResourceContents | mcp.types.BlobResourceContents:
|
|
94
|
+
"""Convert to MCP resource contents type.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
uri: The URI of the resource (required by MCP types)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
TextResourceContents for str content, BlobResourceContents for bytes
|
|
101
|
+
"""
|
|
102
|
+
if isinstance(self.content, str):
|
|
103
|
+
return mcp.types.TextResourceContents(
|
|
104
|
+
uri=AnyUrl(uri) if isinstance(uri, str) else uri,
|
|
105
|
+
text=self.content,
|
|
106
|
+
mimeType=self.mime_type or "text/plain",
|
|
107
|
+
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
return mcp.types.BlobResourceContents(
|
|
111
|
+
uri=AnyUrl(uri) if isinstance(uri, str) else uri,
|
|
112
|
+
blob=base64.b64encode(self.content).decode(),
|
|
113
|
+
mimeType=self.mime_type or "application/octet-stream",
|
|
114
|
+
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ResourceResult(pydantic.BaseModel):
|
|
119
|
+
"""Canonical result type for resource reads.
|
|
120
|
+
|
|
121
|
+
Provides explicit control over resource responses: multiple content items,
|
|
122
|
+
per-item MIME types, and metadata at both the item and result level.
|
|
123
|
+
|
|
124
|
+
Accepts:
|
|
125
|
+
- str: Wrapped as single ResourceContent (text/plain)
|
|
126
|
+
- bytes: Wrapped as single ResourceContent (application/octet-stream)
|
|
127
|
+
- list[ResourceContent]: Used directly for multiple items or custom MIME types
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
```python
|
|
131
|
+
from fastmcp import FastMCP
|
|
132
|
+
from fastmcp.resources import ResourceResult, ResourceContent
|
|
133
|
+
|
|
134
|
+
mcp = FastMCP()
|
|
135
|
+
|
|
136
|
+
# Simple string content
|
|
137
|
+
@mcp.resource("data://simple")
|
|
138
|
+
def get_simple() -> ResourceResult:
|
|
139
|
+
return ResourceResult("hello world")
|
|
140
|
+
|
|
141
|
+
# Multiple items with custom MIME types
|
|
142
|
+
@mcp.resource("data://items")
|
|
143
|
+
def get_items() -> ResourceResult:
|
|
144
|
+
return ResourceResult(
|
|
145
|
+
contents=[
|
|
146
|
+
ResourceContent({"key": "value"}), # auto-serialized to JSON
|
|
147
|
+
ResourceContent(b"binary data"),
|
|
148
|
+
],
|
|
149
|
+
meta={"count": 2}
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
contents: list[ResourceContent]
|
|
155
|
+
meta: dict[str, Any] | None = None
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
contents: str | bytes | list[ResourceContent],
|
|
160
|
+
meta: dict[str, Any] | None = None,
|
|
161
|
+
):
|
|
162
|
+
"""Create ResourceResult.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
contents: String, bytes, or list of ResourceContent objects.
|
|
166
|
+
meta: Optional metadata about the resource result.
|
|
167
|
+
"""
|
|
168
|
+
normalized = self._normalize_contents(contents)
|
|
169
|
+
super().__init__(contents=normalized, meta=meta)
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _normalize_contents(
|
|
173
|
+
contents: str | bytes | list[ResourceContent],
|
|
174
|
+
) -> list[ResourceContent]:
|
|
175
|
+
"""Normalize input to list[ResourceContent]."""
|
|
176
|
+
if isinstance(contents, str):
|
|
177
|
+
return [ResourceContent(contents)]
|
|
178
|
+
if isinstance(contents, bytes):
|
|
179
|
+
return [ResourceContent(contents)]
|
|
180
|
+
if isinstance(contents, list):
|
|
181
|
+
# Validate all items are ResourceContent
|
|
182
|
+
for i, item in enumerate(contents):
|
|
183
|
+
if not isinstance(item, ResourceContent):
|
|
184
|
+
raise TypeError(
|
|
185
|
+
f"contents[{i}] must be ResourceContent, got {type(item).__name__}. "
|
|
186
|
+
f"Use ResourceContent({item!r}) to wrap the value."
|
|
187
|
+
)
|
|
188
|
+
return contents
|
|
189
|
+
raise TypeError(
|
|
190
|
+
f"contents must be str, bytes, or list[ResourceContent], got {type(contents).__name__}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def to_mcp_result(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult:
|
|
194
|
+
"""Convert to MCP ReadResourceResult.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
uri: The URI of the resource (required by MCP types)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
MCP ReadResourceResult with converted contents
|
|
201
|
+
"""
|
|
202
|
+
mcp_contents = [item.to_mcp_resource_contents(uri) for item in self.contents]
|
|
203
|
+
return mcp.types.ReadResourceResult(
|
|
204
|
+
contents=mcp_contents,
|
|
205
|
+
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
|
206
|
+
)
|
|
31
207
|
|
|
32
208
|
|
|
33
209
|
class Resource(FastMCPComponent):
|
|
34
210
|
"""Base class for all resources."""
|
|
35
211
|
|
|
212
|
+
KEY_PREFIX: ClassVar[str] = "resource"
|
|
213
|
+
|
|
36
214
|
model_config = ConfigDict(validate_default=True)
|
|
37
215
|
|
|
38
216
|
uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
|
|
@@ -47,51 +225,47 @@ class Resource(FastMCPComponent):
|
|
|
47
225
|
Annotations | None,
|
|
48
226
|
Field(description="Optional annotations about the resource's behavior"),
|
|
49
227
|
] = None
|
|
228
|
+
auth: Annotated[
|
|
229
|
+
AuthCheckCallable | list[AuthCheckCallable] | None,
|
|
230
|
+
Field(description="Authorization checks for this resource", exclude=True),
|
|
231
|
+
] = None
|
|
50
232
|
|
|
51
|
-
|
|
52
|
-
super().enable()
|
|
53
|
-
try:
|
|
54
|
-
context = get_context()
|
|
55
|
-
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
56
|
-
except RuntimeError:
|
|
57
|
-
pass # No context available
|
|
58
|
-
|
|
59
|
-
def disable(self) -> None:
|
|
60
|
-
super().disable()
|
|
61
|
-
try:
|
|
62
|
-
context = get_context()
|
|
63
|
-
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
64
|
-
except RuntimeError:
|
|
65
|
-
pass # No context available
|
|
66
|
-
|
|
67
|
-
@staticmethod
|
|
233
|
+
@classmethod
|
|
68
234
|
def from_function(
|
|
235
|
+
cls,
|
|
69
236
|
fn: Callable[..., Any],
|
|
70
237
|
uri: str | AnyUrl,
|
|
238
|
+
*,
|
|
71
239
|
name: str | None = None,
|
|
240
|
+
version: str | int | None = None,
|
|
72
241
|
title: str | None = None,
|
|
73
242
|
description: str | None = None,
|
|
74
243
|
icons: list[Icon] | None = None,
|
|
75
244
|
mime_type: str | None = None,
|
|
76
245
|
tags: set[str] | None = None,
|
|
77
|
-
enabled: bool | None = None,
|
|
78
246
|
annotations: Annotations | None = None,
|
|
79
247
|
meta: dict[str, Any] | None = None,
|
|
80
248
|
task: bool | TaskConfig | None = None,
|
|
249
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
81
250
|
) -> FunctionResource:
|
|
251
|
+
from fastmcp.resources.function_resource import (
|
|
252
|
+
FunctionResource,
|
|
253
|
+
)
|
|
254
|
+
|
|
82
255
|
return FunctionResource.from_function(
|
|
83
256
|
fn=fn,
|
|
84
257
|
uri=uri,
|
|
85
258
|
name=name,
|
|
259
|
+
version=version,
|
|
86
260
|
title=title,
|
|
87
261
|
description=description,
|
|
88
262
|
icons=icons,
|
|
89
263
|
mime_type=mime_type,
|
|
90
264
|
tags=tags,
|
|
91
|
-
enabled=enabled,
|
|
92
265
|
annotations=annotations,
|
|
93
266
|
meta=meta,
|
|
94
267
|
task=task,
|
|
268
|
+
auth=auth,
|
|
95
269
|
)
|
|
96
270
|
|
|
97
271
|
@field_validator("mime_type", mode="before")
|
|
@@ -113,18 +287,76 @@ class Resource(FastMCPComponent):
|
|
|
113
287
|
raise ValueError("Either name or uri must be provided")
|
|
114
288
|
return self
|
|
115
289
|
|
|
116
|
-
async def read(
|
|
290
|
+
async def read(
|
|
291
|
+
self,
|
|
292
|
+
) -> str | bytes | ResourceResult:
|
|
117
293
|
"""Read the resource content.
|
|
118
294
|
|
|
119
|
-
|
|
120
|
-
|
|
295
|
+
Subclasses implement this to return resource data. Supported return types:
|
|
296
|
+
- str: Text content
|
|
297
|
+
- bytes: Binary content
|
|
298
|
+
- ResourceResult: Full control over contents and result-level meta
|
|
121
299
|
"""
|
|
122
300
|
raise NotImplementedError("Subclasses must implement read()")
|
|
123
301
|
|
|
302
|
+
def convert_result(self, raw_value: Any) -> ResourceResult:
|
|
303
|
+
"""Convert a raw result to ResourceResult.
|
|
304
|
+
|
|
305
|
+
This is used in two contexts:
|
|
306
|
+
1. In _read() to convert user function return values to ResourceResult
|
|
307
|
+
2. In tasks_result_handler() to convert Docket task results to ResourceResult
|
|
308
|
+
|
|
309
|
+
Handles ResourceResult passthrough and converts raw values using
|
|
310
|
+
ResourceResult's normalization.
|
|
311
|
+
"""
|
|
312
|
+
if isinstance(raw_value, ResourceResult):
|
|
313
|
+
return raw_value
|
|
314
|
+
|
|
315
|
+
# ResourceResult.__init__ handles all normalization
|
|
316
|
+
return ResourceResult(raw_value)
|
|
317
|
+
|
|
318
|
+
@overload
|
|
319
|
+
async def _read(self, task_meta: None = None) -> ResourceResult: ...
|
|
320
|
+
|
|
321
|
+
@overload
|
|
322
|
+
async def _read(self, task_meta: TaskMeta) -> mcp.types.CreateTaskResult: ...
|
|
323
|
+
|
|
324
|
+
async def _read(
|
|
325
|
+
self, task_meta: TaskMeta | None = None
|
|
326
|
+
) -> ResourceResult | mcp.types.CreateTaskResult:
|
|
327
|
+
"""Server entry point that handles task routing.
|
|
328
|
+
|
|
329
|
+
This allows ANY Resource subclass to support background execution by setting
|
|
330
|
+
task_config.mode to "supported" or "required". The server calls this
|
|
331
|
+
method instead of read() directly.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
task_meta: If provided, execute as a background task and return
|
|
335
|
+
CreateTaskResult. If None (default), execute synchronously and
|
|
336
|
+
return ResourceResult.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
ResourceResult when task_meta is None.
|
|
340
|
+
CreateTaskResult when task_meta is provided.
|
|
341
|
+
|
|
342
|
+
Subclasses can override this to customize task routing behavior.
|
|
343
|
+
For example, FastMCPProviderResource overrides to delegate to child
|
|
344
|
+
middleware without submitting to Docket.
|
|
345
|
+
"""
|
|
346
|
+
from fastmcp.server.tasks.routing import check_background_task
|
|
347
|
+
|
|
348
|
+
task_result = await check_background_task(
|
|
349
|
+
component=self, task_type="resource", arguments=None, task_meta=task_meta
|
|
350
|
+
)
|
|
351
|
+
if task_result:
|
|
352
|
+
return task_result
|
|
353
|
+
|
|
354
|
+
# Synchronous execution - convert result to ResourceResult
|
|
355
|
+
result = await self.read()
|
|
356
|
+
return self.convert_result(result)
|
|
357
|
+
|
|
124
358
|
def to_mcp_resource(
|
|
125
359
|
self,
|
|
126
|
-
*,
|
|
127
|
-
include_fastmcp_meta: bool | None = None,
|
|
128
360
|
**overrides: Any,
|
|
129
361
|
) -> SDKResource:
|
|
130
362
|
"""Convert the resource to an SDKResource."""
|
|
@@ -137,8 +369,8 @@ class Resource(FastMCPComponent):
|
|
|
137
369
|
title=overrides.get("title", self.title),
|
|
138
370
|
icons=overrides.get("icons", self.icons),
|
|
139
371
|
annotations=overrides.get("annotations", self.annotations),
|
|
140
|
-
_meta=overrides.get(
|
|
141
|
-
"_meta", self.get_meta(
|
|
372
|
+
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
|
373
|
+
"_meta", self.get_meta()
|
|
142
374
|
),
|
|
143
375
|
)
|
|
144
376
|
|
|
@@ -147,94 +379,72 @@ class Resource(FastMCPComponent):
|
|
|
147
379
|
|
|
148
380
|
@property
|
|
149
381
|
def key(self) -> str:
|
|
150
|
-
"""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
keys having a certain value, as the same tool loaded from different
|
|
154
|
-
hierarchies of servers may have different keys.
|
|
155
|
-
"""
|
|
156
|
-
return self._key or str(self.uri)
|
|
382
|
+
"""The globally unique lookup key for this resource."""
|
|
383
|
+
base_key = self.make_key(str(self.uri))
|
|
384
|
+
return f"{base_key}@{self.version or ''}"
|
|
157
385
|
|
|
386
|
+
def register_with_docket(self, docket: Docket) -> None:
|
|
387
|
+
"""Register this resource with docket for background execution."""
|
|
388
|
+
if not self.task_config.supports_tasks():
|
|
389
|
+
return
|
|
390
|
+
docket.register(self.read, names=[self.key])
|
|
158
391
|
|
|
159
|
-
|
|
160
|
-
|
|
392
|
+
async def add_to_docket( # type: ignore[override]
|
|
393
|
+
self,
|
|
394
|
+
docket: Docket,
|
|
395
|
+
*,
|
|
396
|
+
fn_key: str | None = None,
|
|
397
|
+
task_key: str | None = None,
|
|
398
|
+
**kwargs: Any,
|
|
399
|
+
) -> Execution:
|
|
400
|
+
"""Schedule this resource for background execution via docket.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
docket: The Docket instance
|
|
404
|
+
fn_key: Function lookup key in Docket registry (defaults to self.key)
|
|
405
|
+
task_key: Redis storage key for the result
|
|
406
|
+
**kwargs: Additional kwargs passed to docket.add()
|
|
407
|
+
"""
|
|
408
|
+
lookup_key = fn_key or self.key
|
|
409
|
+
if task_key:
|
|
410
|
+
kwargs["key"] = task_key
|
|
411
|
+
return await docket.add(lookup_key, **kwargs)()
|
|
161
412
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
413
|
+
def get_span_attributes(self) -> dict[str, Any]:
|
|
414
|
+
return super().get_span_attributes() | {
|
|
415
|
+
"fastmcp.component.type": "resource",
|
|
416
|
+
"fastmcp.provider.type": "LocalProvider",
|
|
417
|
+
}
|
|
165
418
|
|
|
166
|
-
The function can return:
|
|
167
|
-
- str for text content (default)
|
|
168
|
-
- bytes for binary content
|
|
169
|
-
- other types will be converted to JSON
|
|
170
|
-
"""
|
|
171
419
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
420
|
+
__all__ = [
|
|
421
|
+
"Resource",
|
|
422
|
+
"ResourceContent",
|
|
423
|
+
"ResourceResult",
|
|
424
|
+
]
|
|
177
425
|
|
|
178
|
-
@classmethod
|
|
179
|
-
def from_function(
|
|
180
|
-
cls,
|
|
181
|
-
fn: Callable[..., Any],
|
|
182
|
-
uri: str | AnyUrl,
|
|
183
|
-
name: str | None = None,
|
|
184
|
-
title: str | None = None,
|
|
185
|
-
description: str | None = None,
|
|
186
|
-
icons: list[Icon] | None = None,
|
|
187
|
-
mime_type: str | None = None,
|
|
188
|
-
tags: set[str] | None = None,
|
|
189
|
-
enabled: bool | None = None,
|
|
190
|
-
annotations: Annotations | None = None,
|
|
191
|
-
meta: dict[str, Any] | None = None,
|
|
192
|
-
task: bool | TaskConfig | None = None,
|
|
193
|
-
) -> FunctionResource:
|
|
194
|
-
"""Create a FunctionResource from a function."""
|
|
195
|
-
if isinstance(uri, str):
|
|
196
|
-
uri = AnyUrl(uri)
|
|
197
426
|
|
|
198
|
-
|
|
427
|
+
def __getattr__(name: str) -> Any:
|
|
428
|
+
"""Deprecated re-exports for backwards compatibility."""
|
|
429
|
+
deprecated_exports = {
|
|
430
|
+
"FunctionResource": "FunctionResource",
|
|
431
|
+
"resource": "resource",
|
|
432
|
+
}
|
|
199
433
|
|
|
200
|
-
|
|
201
|
-
|
|
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)
|
|
434
|
+
if name in deprecated_exports:
|
|
435
|
+
import warnings
|
|
208
436
|
|
|
209
|
-
|
|
210
|
-
wrapped_fn = without_injected_parameters(fn)
|
|
437
|
+
import fastmcp
|
|
211
438
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
tags=tags or set(),
|
|
221
|
-
enabled=enabled if enabled is not None else True,
|
|
222
|
-
annotations=annotations,
|
|
223
|
-
meta=meta,
|
|
224
|
-
task_config=task_config,
|
|
225
|
-
)
|
|
439
|
+
if fastmcp.settings.deprecation_warnings:
|
|
440
|
+
warnings.warn(
|
|
441
|
+
f"Importing {name} from fastmcp.resources.resource is deprecated. "
|
|
442
|
+
f"Import from fastmcp.resources.function_resource instead.",
|
|
443
|
+
DeprecationWarning,
|
|
444
|
+
stacklevel=2,
|
|
445
|
+
)
|
|
446
|
+
from fastmcp.resources import function_resource
|
|
226
447
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
# dependency resolution internally
|
|
231
|
-
result = self.fn()
|
|
232
|
-
if inspect.isawaitable(result):
|
|
233
|
-
result = await result
|
|
234
|
-
|
|
235
|
-
if isinstance(result, Resource):
|
|
236
|
-
return await result.read()
|
|
237
|
-
elif isinstance(result, bytes | str):
|
|
238
|
-
return result
|
|
239
|
-
else:
|
|
240
|
-
return pydantic_core.to_json(result, fallback=str).decode()
|
|
448
|
+
return getattr(function_resource, name)
|
|
449
|
+
|
|
450
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|