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/template.py
CHANGED
|
@@ -5,10 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import re
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import Any
|
|
9
|
-
from urllib.parse import unquote
|
|
8
|
+
from typing import Annotated, Any
|
|
9
|
+
from urllib.parse import parse_qs, unquote
|
|
10
10
|
|
|
11
|
-
from mcp.types import Annotations
|
|
11
|
+
from mcp.types import Annotations, Icon
|
|
12
12
|
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
13
13
|
from pydantic import (
|
|
14
14
|
Field,
|
|
@@ -17,17 +17,33 @@ from pydantic import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
from fastmcp.resources.resource import Resource
|
|
20
|
-
from fastmcp.server.dependencies import get_context
|
|
20
|
+
from fastmcp.server.dependencies import get_context, without_injected_parameters
|
|
21
|
+
from fastmcp.server.tasks.config import TaskConfig
|
|
21
22
|
from fastmcp.utilities.components import FastMCPComponent
|
|
22
23
|
from fastmcp.utilities.json_schema import compress_schema
|
|
23
|
-
from fastmcp.utilities.types import
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
24
|
+
from fastmcp.utilities.types import get_cached_typeadapter
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def extract_query_params(uri_template: str) -> set[str]:
|
|
28
|
+
"""Extract query parameter names from RFC 6570 `{?param1,param2}` syntax."""
|
|
29
|
+
match = re.search(r"\{\?([^}]+)\}", uri_template)
|
|
30
|
+
if match:
|
|
31
|
+
return {p.strip() for p in match.group(1).split(",")}
|
|
32
|
+
return set()
|
|
27
33
|
|
|
28
34
|
|
|
29
35
|
def build_regex(template: str) -> re.Pattern:
|
|
30
|
-
|
|
36
|
+
"""Build regex pattern for URI template, handling RFC 6570 syntax.
|
|
37
|
+
|
|
38
|
+
Supports:
|
|
39
|
+
- `{var}` - simple path parameter
|
|
40
|
+
- `{var*}` - wildcard path parameter (captures multiple segments)
|
|
41
|
+
- `{?var1,var2}` - query parameters (ignored in path matching)
|
|
42
|
+
"""
|
|
43
|
+
# Remove query parameter syntax for path matching
|
|
44
|
+
template_without_query = re.sub(r"\{\?[^}]+\}", "", template)
|
|
45
|
+
|
|
46
|
+
parts = re.split(r"(\{[^}]+\})", template_without_query)
|
|
31
47
|
pattern = ""
|
|
32
48
|
for part in parts:
|
|
33
49
|
if part.startswith("{") and part.endswith("}"):
|
|
@@ -43,11 +59,34 @@ def build_regex(template: str) -> re.Pattern:
|
|
|
43
59
|
|
|
44
60
|
|
|
45
61
|
def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
|
|
62
|
+
"""Match URI against template and extract both path and query parameters.
|
|
63
|
+
|
|
64
|
+
Supports RFC 6570 URI templates:
|
|
65
|
+
- Path params: `{var}`, `{var*}`
|
|
66
|
+
- Query params: `{?var1,var2}`
|
|
67
|
+
"""
|
|
68
|
+
# Split URI into path and query parts
|
|
69
|
+
uri_path, _, query_string = uri.partition("?")
|
|
70
|
+
|
|
71
|
+
# Match path parameters
|
|
46
72
|
regex = build_regex(uri_template)
|
|
47
|
-
match = regex.match(
|
|
48
|
-
if match:
|
|
49
|
-
return
|
|
50
|
-
|
|
73
|
+
match = regex.match(uri_path)
|
|
74
|
+
if not match:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
params = {k: unquote(v) for k, v in match.groupdict().items()}
|
|
78
|
+
|
|
79
|
+
# Extract query parameters if present in URI and template
|
|
80
|
+
if query_string:
|
|
81
|
+
query_param_names = extract_query_params(uri_template)
|
|
82
|
+
parsed_query = parse_qs(query_string)
|
|
83
|
+
|
|
84
|
+
for name in query_param_names:
|
|
85
|
+
if name in parsed_query:
|
|
86
|
+
# Take first value if multiple provided
|
|
87
|
+
params[name] = parsed_query[name][0] # type: ignore[index]
|
|
88
|
+
|
|
89
|
+
return params
|
|
51
90
|
|
|
52
91
|
|
|
53
92
|
class ResourceTemplate(FastMCPComponent):
|
|
@@ -92,11 +131,13 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
92
131
|
name: str | None = None,
|
|
93
132
|
title: str | None = None,
|
|
94
133
|
description: str | None = None,
|
|
134
|
+
icons: list[Icon] | None = None,
|
|
95
135
|
mime_type: str | None = None,
|
|
96
136
|
tags: set[str] | None = None,
|
|
97
137
|
enabled: bool | None = None,
|
|
98
138
|
annotations: Annotations | None = None,
|
|
99
139
|
meta: dict[str, Any] | None = None,
|
|
140
|
+
task: bool | TaskConfig | None = None,
|
|
100
141
|
) -> FunctionResourceTemplate:
|
|
101
142
|
return FunctionResourceTemplate.from_function(
|
|
102
143
|
fn=fn,
|
|
@@ -104,11 +145,13 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
104
145
|
name=name,
|
|
105
146
|
title=title,
|
|
106
147
|
description=description,
|
|
148
|
+
icons=icons,
|
|
107
149
|
mime_type=mime_type,
|
|
108
150
|
tags=tags,
|
|
109
151
|
enabled=enabled,
|
|
110
152
|
annotations=annotations,
|
|
111
153
|
meta=meta,
|
|
154
|
+
task=task,
|
|
112
155
|
)
|
|
113
156
|
|
|
114
157
|
@field_validator("mime_type", mode="before")
|
|
@@ -130,21 +173,14 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
130
173
|
)
|
|
131
174
|
|
|
132
175
|
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
133
|
-
"""Create a resource from the template with the given parameters.
|
|
134
|
-
|
|
135
|
-
async def resource_read_fn() -> str | bytes:
|
|
136
|
-
# Call function and check if result is a coroutine
|
|
137
|
-
result = await self.read(arguments=params)
|
|
138
|
-
return result
|
|
176
|
+
"""Create a resource from the template with the given parameters.
|
|
139
177
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
tags=self.tags,
|
|
147
|
-
enabled=self.enabled,
|
|
178
|
+
The base implementation does not support background tasks.
|
|
179
|
+
Use FunctionResourceTemplate for task support.
|
|
180
|
+
"""
|
|
181
|
+
raise NotImplementedError(
|
|
182
|
+
"Subclasses must implement create_resource(). "
|
|
183
|
+
"Use FunctionResourceTemplate for task support."
|
|
148
184
|
)
|
|
149
185
|
|
|
150
186
|
def to_mcp_template(
|
|
@@ -161,6 +197,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
161
197
|
description=overrides.get("description", self.description),
|
|
162
198
|
mimeType=overrides.get("mimeType", self.mime_type),
|
|
163
199
|
title=overrides.get("title", self.title),
|
|
200
|
+
icons=overrides.get("icons", self.icons),
|
|
164
201
|
annotations=overrides.get("annotations", self.annotations),
|
|
165
202
|
_meta=overrides.get(
|
|
166
203
|
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
@@ -195,20 +232,59 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
195
232
|
"""A template for dynamically creating resources."""
|
|
196
233
|
|
|
197
234
|
fn: Callable[..., Any]
|
|
235
|
+
task_config: Annotated[
|
|
236
|
+
TaskConfig,
|
|
237
|
+
Field(description="Background task execution configuration (SEP-1686)."),
|
|
238
|
+
] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
|
|
239
|
+
|
|
240
|
+
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
241
|
+
"""Create a resource from the template with the given parameters."""
|
|
242
|
+
|
|
243
|
+
async def resource_read_fn() -> str | bytes:
|
|
244
|
+
# Call function and check if result is a coroutine
|
|
245
|
+
result = await self.read(arguments=params)
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
return Resource.from_function(
|
|
249
|
+
fn=resource_read_fn,
|
|
250
|
+
uri=uri,
|
|
251
|
+
name=self.name,
|
|
252
|
+
description=self.description,
|
|
253
|
+
mime_type=self.mime_type,
|
|
254
|
+
tags=self.tags,
|
|
255
|
+
enabled=self.enabled,
|
|
256
|
+
task=self.task_config,
|
|
257
|
+
)
|
|
198
258
|
|
|
199
259
|
async def read(self, arguments: dict[str, Any]) -> str | bytes:
|
|
200
260
|
"""Read the resource content."""
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
# Add context to parameters if needed
|
|
261
|
+
# Type coercion for query parameters (which arrive as strings)
|
|
204
262
|
kwargs = arguments.copy()
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
263
|
+
sig = inspect.signature(self.fn)
|
|
264
|
+
for param_name, param_value in list(kwargs.items()):
|
|
265
|
+
if param_name in sig.parameters and isinstance(param_value, str):
|
|
266
|
+
param = sig.parameters[param_name]
|
|
267
|
+
annotation = param.annotation
|
|
268
|
+
|
|
269
|
+
if annotation is inspect.Parameter.empty or annotation is str:
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
if annotation is int:
|
|
274
|
+
kwargs[param_name] = int(param_value)
|
|
275
|
+
elif annotation is float:
|
|
276
|
+
kwargs[param_name] = float(param_value)
|
|
277
|
+
elif annotation is bool:
|
|
278
|
+
kwargs[param_name] = param_value.lower() in ("true", "1", "yes")
|
|
279
|
+
except (ValueError, AttributeError):
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
# self.fn is wrapped by without_injected_parameters which handles
|
|
283
|
+
# dependency resolution internally, so we call it directly
|
|
209
284
|
result = self.fn(**kwargs)
|
|
210
285
|
if inspect.isawaitable(result):
|
|
211
286
|
result = await result
|
|
287
|
+
|
|
212
288
|
return result
|
|
213
289
|
|
|
214
290
|
@classmethod
|
|
@@ -219,14 +295,15 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
219
295
|
name: str | None = None,
|
|
220
296
|
title: str | None = None,
|
|
221
297
|
description: str | None = None,
|
|
298
|
+
icons: list[Icon] | None = None,
|
|
222
299
|
mime_type: str | None = None,
|
|
223
300
|
tags: set[str] | None = None,
|
|
224
301
|
enabled: bool | None = None,
|
|
225
302
|
annotations: Annotations | None = None,
|
|
226
303
|
meta: dict[str, Any] | None = None,
|
|
304
|
+
task: bool | TaskConfig | None = None,
|
|
227
305
|
) -> FunctionResourceTemplate:
|
|
228
306
|
"""Create a template from a function."""
|
|
229
|
-
from fastmcp.server.context import Context
|
|
230
307
|
|
|
231
308
|
func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
232
309
|
if func_name == "<lambda>":
|
|
@@ -241,46 +318,69 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
241
318
|
"Functions with *args are not supported as resource templates"
|
|
242
319
|
)
|
|
243
320
|
|
|
244
|
-
#
|
|
245
|
-
|
|
246
|
-
|
|
321
|
+
# Extract path and query parameters from URI template
|
|
322
|
+
path_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
|
323
|
+
query_params = extract_query_params(uri_template)
|
|
324
|
+
all_uri_params = path_params | query_params
|
|
247
325
|
|
|
248
|
-
|
|
249
|
-
uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
|
250
|
-
if not uri_params:
|
|
326
|
+
if not all_uri_params:
|
|
251
327
|
raise ValueError("URI template must contain at least one parameter")
|
|
252
328
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
329
|
+
# Use wrapper to get user-facing parameters (excludes injected params)
|
|
330
|
+
wrapper_fn = without_injected_parameters(fn)
|
|
331
|
+
user_sig = inspect.signature(wrapper_fn)
|
|
332
|
+
func_params = set(user_sig.parameters.keys())
|
|
256
333
|
|
|
257
|
-
#
|
|
334
|
+
# Get required and optional function parameters
|
|
258
335
|
required_params = {
|
|
259
336
|
p
|
|
260
337
|
for p in func_params
|
|
261
|
-
if
|
|
262
|
-
and
|
|
263
|
-
and p != context_kwarg
|
|
338
|
+
if user_sig.parameters[p].default is inspect.Parameter.empty
|
|
339
|
+
and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
|
|
264
340
|
}
|
|
341
|
+
optional_params = {
|
|
342
|
+
p
|
|
343
|
+
for p in func_params
|
|
344
|
+
if user_sig.parameters[p].default is not inspect.Parameter.empty
|
|
345
|
+
and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Validate RFC 6570 query parameters
|
|
349
|
+
# Query params must be optional (have defaults)
|
|
350
|
+
if query_params:
|
|
351
|
+
invalid_query_params = query_params - optional_params
|
|
352
|
+
if invalid_query_params:
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"Query parameters {invalid_query_params} must be optional function parameters with default values"
|
|
355
|
+
)
|
|
265
356
|
|
|
266
|
-
# Check if required parameters are a subset of the
|
|
267
|
-
if not required_params.issubset(
|
|
357
|
+
# Check if required parameters are a subset of the path parameters
|
|
358
|
+
if not required_params.issubset(path_params):
|
|
268
359
|
raise ValueError(
|
|
269
|
-
f"Required function arguments {required_params} must be a subset of the URI parameters {
|
|
360
|
+
f"Required function arguments {required_params} must be a subset of the URI path parameters {path_params}"
|
|
270
361
|
)
|
|
271
362
|
|
|
272
|
-
# Check if
|
|
363
|
+
# Check if all URI parameters are valid function parameters (skip if **kwargs present)
|
|
273
364
|
if not any(
|
|
274
365
|
param.kind == inspect.Parameter.VAR_KEYWORD
|
|
275
366
|
for param in sig.parameters.values()
|
|
276
367
|
):
|
|
277
|
-
if not
|
|
368
|
+
if not all_uri_params.issubset(func_params):
|
|
278
369
|
raise ValueError(
|
|
279
|
-
f"URI parameters {
|
|
370
|
+
f"URI parameters {all_uri_params} must be a subset of the function arguments: {func_params}"
|
|
280
371
|
)
|
|
281
372
|
|
|
282
373
|
description = description or inspect.getdoc(fn)
|
|
283
374
|
|
|
375
|
+
# Normalize task to TaskConfig and validate
|
|
376
|
+
if task is None:
|
|
377
|
+
task_config = TaskConfig(mode="forbidden")
|
|
378
|
+
elif isinstance(task, bool):
|
|
379
|
+
task_config = TaskConfig.from_bool(task)
|
|
380
|
+
else:
|
|
381
|
+
task_config = task
|
|
382
|
+
task_config.validate_function(fn, func_name)
|
|
383
|
+
|
|
284
384
|
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
285
385
|
if not inspect.isroutine(fn):
|
|
286
386
|
fn = fn.__call__
|
|
@@ -288,21 +388,20 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
288
388
|
if isinstance(fn, staticmethod):
|
|
289
389
|
fn = fn.__func__
|
|
290
390
|
|
|
291
|
-
|
|
391
|
+
wrapper_fn = without_injected_parameters(fn)
|
|
392
|
+
type_adapter = get_cached_typeadapter(wrapper_fn)
|
|
292
393
|
parameters = type_adapter.json_schema()
|
|
394
|
+
parameters = compress_schema(parameters, prune_titles=True)
|
|
293
395
|
|
|
294
|
-
#
|
|
295
|
-
|
|
296
|
-
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
297
|
-
|
|
298
|
-
# ensure the arguments are properly cast
|
|
299
|
-
fn = validate_call(fn)
|
|
396
|
+
# Use validate_call on wrapper for runtime type coercion
|
|
397
|
+
fn = validate_call(wrapper_fn)
|
|
300
398
|
|
|
301
399
|
return cls(
|
|
302
400
|
uri_template=uri_template,
|
|
303
401
|
name=func_name,
|
|
304
402
|
title=title,
|
|
305
403
|
description=description,
|
|
404
|
+
icons=icons,
|
|
306
405
|
mime_type=mime_type or "text/plain",
|
|
307
406
|
fn=fn,
|
|
308
407
|
parameters=parameters,
|
|
@@ -310,4 +409,5 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
310
409
|
enabled=enabled if enabled is not None else True,
|
|
311
410
|
annotations=annotations,
|
|
312
411
|
meta=meta,
|
|
412
|
+
task_config=task_config,
|
|
313
413
|
)
|
fastmcp/resources/types.py
CHANGED
|
@@ -5,11 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
import anyio
|
|
9
|
-
import anyio.to_thread
|
|
10
8
|
import httpx
|
|
11
9
|
import pydantic.json
|
|
10
|
+
from anyio import Path as AsyncPath
|
|
12
11
|
from pydantic import Field, ValidationInfo
|
|
12
|
+
from typing_extensions import override
|
|
13
13
|
|
|
14
14
|
from fastmcp.exceptions import ResourceError
|
|
15
15
|
from fastmcp.resources.resource import Resource
|
|
@@ -54,6 +54,10 @@ class FileResource(Resource):
|
|
|
54
54
|
description="MIME type of the resource content",
|
|
55
55
|
)
|
|
56
56
|
|
|
57
|
+
@property
|
|
58
|
+
def _async_path(self) -> AsyncPath:
|
|
59
|
+
return AsyncPath(self.path)
|
|
60
|
+
|
|
57
61
|
@pydantic.field_validator("path")
|
|
58
62
|
@classmethod
|
|
59
63
|
def validate_absolute_path(cls, path: Path) -> Path:
|
|
@@ -71,12 +75,13 @@ class FileResource(Resource):
|
|
|
71
75
|
mime_type = info.data.get("mime_type", "text/plain")
|
|
72
76
|
return not mime_type.startswith("text/")
|
|
73
77
|
|
|
78
|
+
@override
|
|
74
79
|
async def read(self) -> str | bytes:
|
|
75
80
|
"""Read the file content."""
|
|
76
81
|
try:
|
|
77
82
|
if self.is_binary:
|
|
78
|
-
return await
|
|
79
|
-
return await
|
|
83
|
+
return await self._async_path.read_bytes()
|
|
84
|
+
return await self._async_path.read_text()
|
|
80
85
|
except Exception as e:
|
|
81
86
|
raise ResourceError(f"Error reading file {self.path}") from e
|
|
82
87
|
|
|
@@ -89,11 +94,12 @@ class HttpResource(Resource):
|
|
|
89
94
|
default="application/json", description="MIME type of the resource content"
|
|
90
95
|
)
|
|
91
96
|
|
|
97
|
+
@override
|
|
92
98
|
async def read(self) -> str | bytes:
|
|
93
99
|
"""Read the HTTP content."""
|
|
94
100
|
async with httpx.AsyncClient() as client:
|
|
95
101
|
response = await client.get(self.url)
|
|
96
|
-
response.raise_for_status()
|
|
102
|
+
_ = response.raise_for_status()
|
|
97
103
|
return response.text
|
|
98
104
|
|
|
99
105
|
|
|
@@ -111,6 +117,10 @@ class DirectoryResource(Resource):
|
|
|
111
117
|
default="application/json", description="MIME type of the resource content"
|
|
112
118
|
)
|
|
113
119
|
|
|
120
|
+
@property
|
|
121
|
+
def _async_path(self) -> AsyncPath:
|
|
122
|
+
return AsyncPath(self.path)
|
|
123
|
+
|
|
114
124
|
@pydantic.field_validator("path")
|
|
115
125
|
@classmethod
|
|
116
126
|
def validate_absolute_path(cls, path: Path) -> Path:
|
|
@@ -119,33 +129,29 @@ class DirectoryResource(Resource):
|
|
|
119
129
|
raise ValueError("Path must be absolute")
|
|
120
130
|
return path
|
|
121
131
|
|
|
122
|
-
def list_files(self) -> list[Path]:
|
|
132
|
+
async def list_files(self) -> list[Path]:
|
|
123
133
|
"""List files in the directory."""
|
|
124
|
-
if not self.
|
|
134
|
+
if not await self._async_path.exists():
|
|
125
135
|
raise FileNotFoundError(f"Directory not found: {self.path}")
|
|
126
|
-
if not self.
|
|
136
|
+
if not await self._async_path.is_dir():
|
|
127
137
|
raise NotADirectoryError(f"Not a directory: {self.path}")
|
|
128
138
|
|
|
139
|
+
pattern = self.pattern or "*"
|
|
140
|
+
|
|
141
|
+
glob_fn = self._async_path.rglob if self.recursive else self._async_path.glob
|
|
129
142
|
try:
|
|
130
|
-
if
|
|
131
|
-
return (
|
|
132
|
-
list(self.path.glob(self.pattern))
|
|
133
|
-
if not self.recursive
|
|
134
|
-
else list(self.path.rglob(self.pattern))
|
|
135
|
-
)
|
|
136
|
-
return (
|
|
137
|
-
list(self.path.glob("*"))
|
|
138
|
-
if not self.recursive
|
|
139
|
-
else list(self.path.rglob("*"))
|
|
140
|
-
)
|
|
143
|
+
return [Path(p) async for p in glob_fn(pattern) if await p.is_file()]
|
|
141
144
|
except Exception as e:
|
|
142
|
-
raise ResourceError(f"Error listing directory {self.path}
|
|
145
|
+
raise ResourceError(f"Error listing directory {self.path}") from e
|
|
143
146
|
|
|
147
|
+
@override
|
|
144
148
|
async def read(self) -> str: # Always returns JSON string
|
|
145
149
|
"""Read the directory listing."""
|
|
146
150
|
try:
|
|
147
|
-
files = await
|
|
148
|
-
|
|
151
|
+
files: list[Path] = await self.list_files()
|
|
152
|
+
|
|
153
|
+
file_list = [str(f.relative_to(self.path)) for f in files]
|
|
154
|
+
|
|
149
155
|
return json.dumps({"files": file_list}, indent=2)
|
|
150
|
-
except Exception:
|
|
151
|
-
raise ResourceError(f"Error reading directory {self.path}")
|
|
156
|
+
except Exception as e:
|
|
157
|
+
raise ResourceError(f"Error reading directory {self.path}") from e
|
fastmcp/server/__init__.py
CHANGED
fastmcp/server/auth/__init__.py
CHANGED
|
@@ -5,26 +5,21 @@ from .auth import (
|
|
|
5
5
|
AccessToken,
|
|
6
6
|
AuthProvider,
|
|
7
7
|
)
|
|
8
|
+
from .providers.debug import DebugTokenVerifier
|
|
8
9
|
from .providers.jwt import JWTVerifier, StaticTokenVerifier
|
|
9
10
|
from .oauth_proxy import OAuthProxy
|
|
11
|
+
from .oidc_proxy import OIDCProxy
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
__all__ = [
|
|
15
|
+
"AccessToken",
|
|
13
16
|
"AuthProvider",
|
|
14
|
-
"
|
|
15
|
-
"TokenVerifier",
|
|
17
|
+
"DebugTokenVerifier",
|
|
16
18
|
"JWTVerifier",
|
|
17
|
-
"
|
|
18
|
-
"RemoteAuthProvider",
|
|
19
|
-
"AccessToken",
|
|
19
|
+
"OAuthProvider",
|
|
20
20
|
"OAuthProxy",
|
|
21
|
+
"OIDCProxy",
|
|
22
|
+
"RemoteAuthProvider",
|
|
23
|
+
"StaticTokenVerifier",
|
|
24
|
+
"TokenVerifier",
|
|
21
25
|
]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def __getattr__(name: str):
|
|
25
|
-
# Defer import because it raises a deprecation warning
|
|
26
|
-
if name == "BearerAuthProvider":
|
|
27
|
-
from .providers.bearer import BearerAuthProvider
|
|
28
|
-
|
|
29
|
-
return BearerAuthProvider
|
|
30
|
-
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|