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/template.py
CHANGED
|
@@ -5,10 +5,15 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import re
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import TYPE_CHECKING, Any, ClassVar, overload
|
|
9
9
|
from urllib.parse import parse_qs, unquote
|
|
10
10
|
|
|
11
|
+
import mcp.types
|
|
11
12
|
from mcp.types import Annotations, Icon
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from docket import Docket
|
|
16
|
+
from docket.execution import Execution
|
|
12
17
|
from mcp.types import ResourceTemplate as SDKResourceTemplate
|
|
13
18
|
from pydantic import (
|
|
14
19
|
Field,
|
|
@@ -16,9 +21,13 @@ from pydantic import (
|
|
|
16
21
|
validate_call,
|
|
17
22
|
)
|
|
18
23
|
|
|
19
|
-
from fastmcp.resources.resource import Resource
|
|
20
|
-
from fastmcp.server.dependencies import
|
|
21
|
-
|
|
24
|
+
from fastmcp.resources.resource import Resource, ResourceResult
|
|
25
|
+
from fastmcp.server.dependencies import (
|
|
26
|
+
transform_context_annotations,
|
|
27
|
+
without_injected_parameters,
|
|
28
|
+
)
|
|
29
|
+
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
|
|
30
|
+
from fastmcp.tools.tool import AuthCheckCallable
|
|
22
31
|
from fastmcp.utilities.components import FastMCPComponent
|
|
23
32
|
from fastmcp.utilities.json_schema import compress_schema
|
|
24
33
|
from fastmcp.utilities.types import get_cached_typeadapter
|
|
@@ -84,7 +93,7 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
|
|
|
84
93
|
for name in query_param_names:
|
|
85
94
|
if name in parsed_query:
|
|
86
95
|
# Take first value if multiple provided
|
|
87
|
-
params[name] = parsed_query[name][0]
|
|
96
|
+
params[name] = parsed_query[name][0]
|
|
88
97
|
|
|
89
98
|
return params
|
|
90
99
|
|
|
@@ -92,6 +101,8 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
|
|
|
92
101
|
class ResourceTemplate(FastMCPComponent):
|
|
93
102
|
"""A template for dynamically creating resources."""
|
|
94
103
|
|
|
104
|
+
KEY_PREFIX: ClassVar[str] = "template"
|
|
105
|
+
|
|
95
106
|
uri_template: str = Field(
|
|
96
107
|
description="URI template with parameters (e.g. weather://{city}/current)"
|
|
97
108
|
)
|
|
@@ -104,54 +115,45 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
104
115
|
annotations: Annotations | None = Field(
|
|
105
116
|
default=None, description="Optional annotations about the resource's behavior"
|
|
106
117
|
)
|
|
118
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = Field(
|
|
119
|
+
default=None,
|
|
120
|
+
description="Authorization checks for this resource template",
|
|
121
|
+
exclude=True,
|
|
122
|
+
)
|
|
107
123
|
|
|
108
124
|
def __repr__(self) -> str:
|
|
109
125
|
return f"{self.__class__.__name__}(uri_template={self.uri_template!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
|
|
110
126
|
|
|
111
|
-
def enable(self) -> None:
|
|
112
|
-
super().enable()
|
|
113
|
-
try:
|
|
114
|
-
context = get_context()
|
|
115
|
-
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
116
|
-
except RuntimeError:
|
|
117
|
-
pass # No context available
|
|
118
|
-
|
|
119
|
-
def disable(self) -> None:
|
|
120
|
-
super().disable()
|
|
121
|
-
try:
|
|
122
|
-
context = get_context()
|
|
123
|
-
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
124
|
-
except RuntimeError:
|
|
125
|
-
pass # No context available
|
|
126
|
-
|
|
127
127
|
@staticmethod
|
|
128
128
|
def from_function(
|
|
129
129
|
fn: Callable[..., Any],
|
|
130
130
|
uri_template: str,
|
|
131
131
|
name: str | None = None,
|
|
132
|
+
version: str | int | None = None,
|
|
132
133
|
title: str | None = None,
|
|
133
134
|
description: str | None = None,
|
|
134
135
|
icons: list[Icon] | None = None,
|
|
135
136
|
mime_type: str | None = None,
|
|
136
137
|
tags: set[str] | None = None,
|
|
137
|
-
enabled: bool | None = None,
|
|
138
138
|
annotations: Annotations | None = None,
|
|
139
139
|
meta: dict[str, Any] | None = None,
|
|
140
140
|
task: bool | TaskConfig | None = None,
|
|
141
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
141
142
|
) -> FunctionResourceTemplate:
|
|
142
143
|
return FunctionResourceTemplate.from_function(
|
|
143
144
|
fn=fn,
|
|
144
145
|
uri_template=uri_template,
|
|
145
146
|
name=name,
|
|
147
|
+
version=version,
|
|
146
148
|
title=title,
|
|
147
149
|
description=description,
|
|
148
150
|
icons=icons,
|
|
149
151
|
mime_type=mime_type,
|
|
150
152
|
tags=tags,
|
|
151
|
-
enabled=enabled,
|
|
152
153
|
annotations=annotations,
|
|
153
154
|
meta=meta,
|
|
154
155
|
task=task,
|
|
156
|
+
auth=auth,
|
|
155
157
|
)
|
|
156
158
|
|
|
157
159
|
@field_validator("mime_type", mode="before")
|
|
@@ -166,12 +168,76 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
166
168
|
"""Check if URI matches template and extract parameters."""
|
|
167
169
|
return match_uri_template(uri, self.uri_template)
|
|
168
170
|
|
|
169
|
-
async def read(self, arguments: dict[str, Any]) -> str | bytes:
|
|
171
|
+
async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
|
|
170
172
|
"""Read the resource content."""
|
|
171
173
|
raise NotImplementedError(
|
|
172
174
|
"Subclasses must implement read() or override create_resource()"
|
|
173
175
|
)
|
|
174
176
|
|
|
177
|
+
def convert_result(self, raw_value: Any) -> ResourceResult:
|
|
178
|
+
"""Convert a raw result to ResourceResult.
|
|
179
|
+
|
|
180
|
+
This is used in two contexts:
|
|
181
|
+
1. In _read() to convert user function return values to ResourceResult
|
|
182
|
+
2. In tasks_result_handler() to convert Docket task results to ResourceResult
|
|
183
|
+
|
|
184
|
+
Handles ResourceResult passthrough and converts raw values using
|
|
185
|
+
ResourceResult's normalization.
|
|
186
|
+
"""
|
|
187
|
+
if isinstance(raw_value, ResourceResult):
|
|
188
|
+
return raw_value
|
|
189
|
+
|
|
190
|
+
# ResourceResult.__init__ handles all normalization
|
|
191
|
+
return ResourceResult(raw_value)
|
|
192
|
+
|
|
193
|
+
@overload
|
|
194
|
+
async def _read(
|
|
195
|
+
self, uri: str, params: dict[str, Any], task_meta: None = None
|
|
196
|
+
) -> ResourceResult: ...
|
|
197
|
+
|
|
198
|
+
@overload
|
|
199
|
+
async def _read(
|
|
200
|
+
self, uri: str, params: dict[str, Any], task_meta: TaskMeta
|
|
201
|
+
) -> mcp.types.CreateTaskResult: ...
|
|
202
|
+
|
|
203
|
+
async def _read(
|
|
204
|
+
self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
|
|
205
|
+
) -> ResourceResult | mcp.types.CreateTaskResult:
|
|
206
|
+
"""Server entry point that handles task routing.
|
|
207
|
+
|
|
208
|
+
This allows ANY ResourceTemplate subclass to support background execution
|
|
209
|
+
by setting task_config.mode to "supported" or "required". The server calls
|
|
210
|
+
this method instead of create_resource()/read() directly.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
uri: The concrete URI being read
|
|
214
|
+
params: Template parameters extracted from the URI
|
|
215
|
+
task_meta: If provided, execute as a background task and return
|
|
216
|
+
CreateTaskResult. If None (default), execute synchronously and
|
|
217
|
+
return ResourceResult.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
ResourceResult when task_meta is None.
|
|
221
|
+
CreateTaskResult when task_meta is provided.
|
|
222
|
+
|
|
223
|
+
Subclasses can override this to customize task routing behavior.
|
|
224
|
+
For example, FastMCPProviderResourceTemplate overrides to delegate to child
|
|
225
|
+
middleware without submitting to Docket.
|
|
226
|
+
"""
|
|
227
|
+
from fastmcp.server.tasks.routing import check_background_task
|
|
228
|
+
|
|
229
|
+
task_result = await check_background_task(
|
|
230
|
+
component=self, task_type="template", arguments=params, task_meta=task_meta
|
|
231
|
+
)
|
|
232
|
+
if task_result:
|
|
233
|
+
return task_result
|
|
234
|
+
|
|
235
|
+
# Synchronous execution - create resource and read directly
|
|
236
|
+
# Call resource.read() not resource._read() to avoid task routing on ephemeral resource
|
|
237
|
+
resource = await self.create_resource(uri, params)
|
|
238
|
+
result = await resource.read()
|
|
239
|
+
return self.convert_result(result)
|
|
240
|
+
|
|
175
241
|
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
176
242
|
"""Create a resource from the template with the given parameters.
|
|
177
243
|
|
|
@@ -185,8 +251,6 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
185
251
|
|
|
186
252
|
def to_mcp_template(
|
|
187
253
|
self,
|
|
188
|
-
*,
|
|
189
|
-
include_fastmcp_meta: bool | None = None,
|
|
190
254
|
**overrides: Any,
|
|
191
255
|
) -> SDKResourceTemplate:
|
|
192
256
|
"""Convert the resource template to an SDKResourceTemplate."""
|
|
@@ -199,8 +263,8 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
199
263
|
title=overrides.get("title", self.title),
|
|
200
264
|
icons=overrides.get("icons", self.icons),
|
|
201
265
|
annotations=overrides.get("annotations", self.annotations),
|
|
202
|
-
_meta=overrides.get(
|
|
203
|
-
"_meta", self.get_meta(
|
|
266
|
+
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
|
267
|
+
"_meta", self.get_meta()
|
|
204
268
|
),
|
|
205
269
|
)
|
|
206
270
|
|
|
@@ -219,28 +283,96 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
219
283
|
|
|
220
284
|
@property
|
|
221
285
|
def key(self) -> str:
|
|
286
|
+
"""The globally unique lookup key for this template."""
|
|
287
|
+
base_key = self.make_key(self.uri_template)
|
|
288
|
+
return f"{base_key}@{self.version or ''}"
|
|
289
|
+
|
|
290
|
+
def register_with_docket(self, docket: Docket) -> None:
|
|
291
|
+
"""Register this template with docket for background execution."""
|
|
292
|
+
if not self.task_config.supports_tasks():
|
|
293
|
+
return
|
|
294
|
+
docket.register(self.read, names=[self.key])
|
|
295
|
+
|
|
296
|
+
async def add_to_docket( # type: ignore[override]
|
|
297
|
+
self,
|
|
298
|
+
docket: Docket,
|
|
299
|
+
params: dict[str, Any],
|
|
300
|
+
*,
|
|
301
|
+
fn_key: str | None = None,
|
|
302
|
+
task_key: str | None = None,
|
|
303
|
+
**kwargs: Any,
|
|
304
|
+
) -> Execution:
|
|
305
|
+
"""Schedule this template for background execution via docket.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
docket: The Docket instance
|
|
309
|
+
params: Template parameters
|
|
310
|
+
fn_key: Function lookup key in Docket registry (defaults to self.key)
|
|
311
|
+
task_key: Redis storage key for the result
|
|
312
|
+
**kwargs: Additional kwargs passed to docket.add()
|
|
222
313
|
"""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
314
|
+
lookup_key = fn_key or self.key
|
|
315
|
+
if task_key:
|
|
316
|
+
kwargs["key"] = task_key
|
|
317
|
+
return await docket.add(lookup_key, **kwargs)(params)
|
|
318
|
+
|
|
319
|
+
def get_span_attributes(self) -> dict[str, Any]:
|
|
320
|
+
return super().get_span_attributes() | {
|
|
321
|
+
"fastmcp.component.type": "resource_template",
|
|
322
|
+
"fastmcp.provider.type": "LocalProvider",
|
|
323
|
+
}
|
|
229
324
|
|
|
230
325
|
|
|
231
326
|
class FunctionResourceTemplate(ResourceTemplate):
|
|
232
327
|
"""A template for dynamically creating resources."""
|
|
233
328
|
|
|
234
329
|
fn: Callable[..., Any]
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
330
|
+
|
|
331
|
+
@overload
|
|
332
|
+
async def _read(
|
|
333
|
+
self, uri: str, params: dict[str, Any], task_meta: None = None
|
|
334
|
+
) -> ResourceResult: ...
|
|
335
|
+
|
|
336
|
+
@overload
|
|
337
|
+
async def _read(
|
|
338
|
+
self, uri: str, params: dict[str, Any], task_meta: TaskMeta
|
|
339
|
+
) -> mcp.types.CreateTaskResult: ...
|
|
340
|
+
|
|
341
|
+
async def _read(
|
|
342
|
+
self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
|
|
343
|
+
) -> ResourceResult | mcp.types.CreateTaskResult:
|
|
344
|
+
"""Optimized server entry point that skips ephemeral resource creation.
|
|
345
|
+
|
|
346
|
+
For FunctionResourceTemplate, we can call read() directly instead of
|
|
347
|
+
creating a temporary resource, which is more efficient.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
uri: The concrete URI being read
|
|
351
|
+
params: Template parameters extracted from the URI
|
|
352
|
+
task_meta: If provided, execute as a background task and return
|
|
353
|
+
CreateTaskResult. If None (default), execute synchronously and
|
|
354
|
+
return ResourceResult.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
ResourceResult when task_meta is None.
|
|
358
|
+
CreateTaskResult when task_meta is provided.
|
|
359
|
+
"""
|
|
360
|
+
from fastmcp.server.tasks.routing import check_background_task
|
|
361
|
+
|
|
362
|
+
task_result = await check_background_task(
|
|
363
|
+
component=self, task_type="template", arguments=params, task_meta=task_meta
|
|
364
|
+
)
|
|
365
|
+
if task_result:
|
|
366
|
+
return task_result
|
|
367
|
+
|
|
368
|
+
# Synchronous execution - call read() directly, skip resource creation
|
|
369
|
+
result = await self.read(arguments=params)
|
|
370
|
+
return self.convert_result(result)
|
|
239
371
|
|
|
240
372
|
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
241
373
|
"""Create a resource from the template with the given parameters."""
|
|
242
374
|
|
|
243
|
-
async def resource_read_fn() -> str | bytes:
|
|
375
|
+
async def resource_read_fn() -> str | bytes | ResourceResult:
|
|
244
376
|
# Call function and check if result is a coroutine
|
|
245
377
|
result = await self.read(arguments=params)
|
|
246
378
|
return result
|
|
@@ -252,11 +384,11 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
252
384
|
description=self.description,
|
|
253
385
|
mime_type=self.mime_type,
|
|
254
386
|
tags=self.tags,
|
|
255
|
-
enabled=self.enabled,
|
|
256
387
|
task=self.task_config,
|
|
388
|
+
auth=self.auth,
|
|
257
389
|
)
|
|
258
390
|
|
|
259
|
-
async def read(self, arguments: dict[str, Any]) -> str | bytes:
|
|
391
|
+
async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
|
|
260
392
|
"""Read the resource content."""
|
|
261
393
|
# Type coercion for query parameters (which arrive as strings)
|
|
262
394
|
kwargs = arguments.copy()
|
|
@@ -287,21 +419,57 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
287
419
|
|
|
288
420
|
return result
|
|
289
421
|
|
|
422
|
+
def register_with_docket(self, docket: Docket) -> None:
|
|
423
|
+
"""Register this template with docket for background execution.
|
|
424
|
+
|
|
425
|
+
FunctionResourceTemplate registers the underlying function, which has the
|
|
426
|
+
user's Depends parameters for docket to resolve.
|
|
427
|
+
"""
|
|
428
|
+
if not self.task_config.supports_tasks():
|
|
429
|
+
return
|
|
430
|
+
docket.register(self.fn, names=[self.key])
|
|
431
|
+
|
|
432
|
+
async def add_to_docket(
|
|
433
|
+
self,
|
|
434
|
+
docket: Docket,
|
|
435
|
+
params: dict[str, Any],
|
|
436
|
+
*,
|
|
437
|
+
fn_key: str | None = None,
|
|
438
|
+
task_key: str | None = None,
|
|
439
|
+
**kwargs: Any,
|
|
440
|
+
) -> Execution:
|
|
441
|
+
"""Schedule this template for background execution via docket.
|
|
442
|
+
|
|
443
|
+
FunctionResourceTemplate splats the params dict since .fn expects **kwargs.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
docket: The Docket instance
|
|
447
|
+
params: Template parameters
|
|
448
|
+
fn_key: Function lookup key in Docket registry (defaults to self.key)
|
|
449
|
+
task_key: Redis storage key for the result
|
|
450
|
+
**kwargs: Additional kwargs passed to docket.add()
|
|
451
|
+
"""
|
|
452
|
+
lookup_key = fn_key or self.key
|
|
453
|
+
if task_key:
|
|
454
|
+
kwargs["key"] = task_key
|
|
455
|
+
return await docket.add(lookup_key, **kwargs)(**params)
|
|
456
|
+
|
|
290
457
|
@classmethod
|
|
291
458
|
def from_function(
|
|
292
459
|
cls,
|
|
293
460
|
fn: Callable[..., Any],
|
|
294
461
|
uri_template: str,
|
|
295
462
|
name: str | None = None,
|
|
463
|
+
version: str | int | None = None,
|
|
296
464
|
title: str | None = None,
|
|
297
465
|
description: str | None = None,
|
|
298
466
|
icons: list[Icon] | None = None,
|
|
299
467
|
mime_type: str | None = None,
|
|
300
468
|
tags: set[str] | None = None,
|
|
301
|
-
enabled: bool | None = None,
|
|
302
469
|
annotations: Annotations | None = None,
|
|
303
470
|
meta: dict[str, Any] | None = None,
|
|
304
471
|
task: bool | TaskConfig | None = None,
|
|
472
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
305
473
|
) -> FunctionResourceTemplate:
|
|
306
474
|
"""Create a template from a function."""
|
|
307
475
|
|
|
@@ -388,6 +556,9 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
388
556
|
if isinstance(fn, staticmethod):
|
|
389
557
|
fn = fn.__func__
|
|
390
558
|
|
|
559
|
+
# Transform Context type annotations to Depends() for unified DI
|
|
560
|
+
fn = transform_context_annotations(fn)
|
|
561
|
+
|
|
391
562
|
wrapper_fn = without_injected_parameters(fn)
|
|
392
563
|
type_adapter = get_cached_typeadapter(wrapper_fn)
|
|
393
564
|
parameters = type_adapter.json_schema()
|
|
@@ -399,6 +570,7 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
399
570
|
return cls(
|
|
400
571
|
uri_template=uri_template,
|
|
401
572
|
name=func_name,
|
|
573
|
+
version=str(version) if version is not None else None,
|
|
402
574
|
title=title,
|
|
403
575
|
description=description,
|
|
404
576
|
icons=icons,
|
|
@@ -406,8 +578,8 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
406
578
|
fn=fn,
|
|
407
579
|
parameters=parameters,
|
|
408
580
|
tags=tags or set(),
|
|
409
|
-
enabled=enabled if enabled is not None else True,
|
|
410
581
|
annotations=annotations,
|
|
411
582
|
meta=meta,
|
|
412
583
|
task_config=task_config,
|
|
584
|
+
auth=auth,
|
|
413
585
|
)
|
fastmcp/resources/types.py
CHANGED
|
@@ -12,7 +12,7 @@ from pydantic import Field, ValidationInfo
|
|
|
12
12
|
from typing_extensions import override
|
|
13
13
|
|
|
14
14
|
from fastmcp.exceptions import ResourceError
|
|
15
|
-
from fastmcp.resources.resource import Resource
|
|
15
|
+
from fastmcp.resources.resource import Resource, ResourceContent, ResourceResult
|
|
16
16
|
from fastmcp.utilities.logging import get_logger
|
|
17
17
|
|
|
18
18
|
logger = get_logger(__name__)
|
|
@@ -23,9 +23,11 @@ class TextResource(Resource):
|
|
|
23
23
|
|
|
24
24
|
text: str = Field(description="Text content of the resource")
|
|
25
25
|
|
|
26
|
-
async def read(self) ->
|
|
26
|
+
async def read(self) -> ResourceResult:
|
|
27
27
|
"""Read the text content."""
|
|
28
|
-
return
|
|
28
|
+
return ResourceResult(
|
|
29
|
+
contents=[ResourceContent(content=self.text, mime_type=self.mime_type)]
|
|
30
|
+
)
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class BinaryResource(Resource):
|
|
@@ -33,9 +35,11 @@ class BinaryResource(Resource):
|
|
|
33
35
|
|
|
34
36
|
data: bytes = Field(description="Binary content of the resource")
|
|
35
37
|
|
|
36
|
-
async def read(self) ->
|
|
38
|
+
async def read(self) -> ResourceResult:
|
|
37
39
|
"""Read the binary content."""
|
|
38
|
-
return
|
|
40
|
+
return ResourceResult(
|
|
41
|
+
contents=[ResourceContent(content=self.data, mime_type=self.mime_type)]
|
|
42
|
+
)
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
class FileResource(Resource):
|
|
@@ -76,12 +80,16 @@ class FileResource(Resource):
|
|
|
76
80
|
return not mime_type.startswith("text/")
|
|
77
81
|
|
|
78
82
|
@override
|
|
79
|
-
async def read(self) ->
|
|
83
|
+
async def read(self) -> ResourceResult:
|
|
80
84
|
"""Read the file content."""
|
|
81
85
|
try:
|
|
82
86
|
if self.is_binary:
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
content: str | bytes = await self._async_path.read_bytes()
|
|
88
|
+
else:
|
|
89
|
+
content = await self._async_path.read_text()
|
|
90
|
+
return ResourceResult(
|
|
91
|
+
contents=[ResourceContent(content=content, mime_type=self.mime_type)]
|
|
92
|
+
)
|
|
85
93
|
except Exception as e:
|
|
86
94
|
raise ResourceError(f"Error reading file {self.path}") from e
|
|
87
95
|
|
|
@@ -95,12 +103,16 @@ class HttpResource(Resource):
|
|
|
95
103
|
)
|
|
96
104
|
|
|
97
105
|
@override
|
|
98
|
-
async def read(self) ->
|
|
106
|
+
async def read(self) -> ResourceResult:
|
|
99
107
|
"""Read the HTTP content."""
|
|
100
108
|
async with httpx.AsyncClient() as client:
|
|
101
109
|
response = await client.get(self.url)
|
|
102
110
|
_ = response.raise_for_status()
|
|
103
|
-
return
|
|
111
|
+
return ResourceResult(
|
|
112
|
+
contents=[
|
|
113
|
+
ResourceContent(content=response.text, mime_type=self.mime_type)
|
|
114
|
+
]
|
|
115
|
+
)
|
|
104
116
|
|
|
105
117
|
|
|
106
118
|
class DirectoryResource(Resource):
|
|
@@ -145,13 +157,16 @@ class DirectoryResource(Resource):
|
|
|
145
157
|
raise ResourceError(f"Error listing directory {self.path}") from e
|
|
146
158
|
|
|
147
159
|
@override
|
|
148
|
-
async def read(self) ->
|
|
160
|
+
async def read(self) -> ResourceResult:
|
|
149
161
|
"""Read the directory listing."""
|
|
150
162
|
try:
|
|
151
163
|
files: list[Path] = await self.list_files()
|
|
152
164
|
|
|
153
165
|
file_list = [str(f.relative_to(self.path)) for f in files]
|
|
154
166
|
|
|
155
|
-
|
|
167
|
+
content = json.dumps({"files": file_list}, indent=2)
|
|
168
|
+
return ResourceResult(
|
|
169
|
+
contents=[ResourceContent(content=content, mime_type=self.mime_type)]
|
|
170
|
+
)
|
|
156
171
|
except Exception as e:
|
|
157
172
|
raise ResourceError(f"Error reading directory {self.path}") from e
|
fastmcp/server/__init__.py
CHANGED
fastmcp/server/auth/__init__.py
CHANGED
|
@@ -5,6 +5,14 @@ from .auth import (
|
|
|
5
5
|
AccessToken,
|
|
6
6
|
AuthProvider,
|
|
7
7
|
)
|
|
8
|
+
from .authorization import (
|
|
9
|
+
AuthCheck,
|
|
10
|
+
AuthContext,
|
|
11
|
+
require_auth,
|
|
12
|
+
require_scopes,
|
|
13
|
+
restrict_tag,
|
|
14
|
+
run_auth_checks,
|
|
15
|
+
)
|
|
8
16
|
from .providers.debug import DebugTokenVerifier
|
|
9
17
|
from .providers.jwt import JWTVerifier, StaticTokenVerifier
|
|
10
18
|
from .oauth_proxy import OAuthProxy
|
|
@@ -13,6 +21,8 @@ from .oidc_proxy import OIDCProxy
|
|
|
13
21
|
|
|
14
22
|
__all__ = [
|
|
15
23
|
"AccessToken",
|
|
24
|
+
"AuthCheck",
|
|
25
|
+
"AuthContext",
|
|
16
26
|
"AuthProvider",
|
|
17
27
|
"DebugTokenVerifier",
|
|
18
28
|
"JWTVerifier",
|
|
@@ -22,4 +32,8 @@ __all__ = [
|
|
|
22
32
|
"RemoteAuthProvider",
|
|
23
33
|
"StaticTokenVerifier",
|
|
24
34
|
"TokenVerifier",
|
|
35
|
+
"require_auth",
|
|
36
|
+
"require_scopes",
|
|
37
|
+
"restrict_tag",
|
|
38
|
+
"run_auth_checks",
|
|
25
39
|
]
|
fastmcp/server/auth/auth.py
CHANGED
|
@@ -47,19 +47,19 @@ class AccessToken(_SDKAccessToken):
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class TokenHandler(_SDKTokenHandler):
|
|
50
|
-
"""TokenHandler that returns
|
|
50
|
+
"""TokenHandler that returns MCP-compliant error responses.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
However, per RFC 6749 Section 5.2, authentication failures should return
|
|
54
|
-
`invalid_client` with HTTP 401, not `unauthorized_client`.
|
|
52
|
+
This handler addresses two SDK issues:
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
to re-register.
|
|
54
|
+
1. Error code: The SDK returns `unauthorized_client` for client authentication
|
|
55
|
+
failures, but RFC 6749 Section 5.2 requires `invalid_client` with HTTP 401.
|
|
56
|
+
This distinction matters for client re-registration behavior.
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
2. Status code: The SDK returns HTTP 400 for all token errors including
|
|
59
|
+
`invalid_grant` (expired/invalid tokens). However, the MCP spec requires:
|
|
60
|
+
"Invalid or expired tokens MUST receive a HTTP 401 response."
|
|
61
|
+
|
|
62
|
+
This handler transforms responses to be compliant with both OAuth 2.1 and MCP specs.
|
|
63
63
|
"""
|
|
64
64
|
|
|
65
65
|
async def handle(self, request: Any):
|
|
@@ -85,6 +85,26 @@ class TokenHandler(_SDKTokenHandler):
|
|
|
85
85
|
except (json.JSONDecodeError, AttributeError):
|
|
86
86
|
pass # Not JSON or unexpected format, return as-is
|
|
87
87
|
|
|
88
|
+
# Transform 400 invalid_grant -> 401 for expired/invalid tokens
|
|
89
|
+
# Per MCP spec: "Invalid or expired tokens MUST receive a HTTP 401 response."
|
|
90
|
+
if response.status_code == 400:
|
|
91
|
+
try:
|
|
92
|
+
body = json.loads(response.body)
|
|
93
|
+
if body.get("error") == "invalid_grant":
|
|
94
|
+
return PydanticJSONResponse(
|
|
95
|
+
content=TokenErrorResponse(
|
|
96
|
+
error="invalid_grant",
|
|
97
|
+
error_description=body.get("error_description"),
|
|
98
|
+
),
|
|
99
|
+
status_code=401,
|
|
100
|
+
headers={
|
|
101
|
+
"Cache-Control": "no-store",
|
|
102
|
+
"Pragma": "no-cache",
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
except (json.JSONDecodeError, AttributeError):
|
|
106
|
+
pass # Not JSON or unexpected format, return as-is
|
|
107
|
+
|
|
88
108
|
return response
|
|
89
109
|
|
|
90
110
|
|