fastmcp 2.13.3__py3-none-any.whl → 2.14.1__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 +0 -21
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +8 -22
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/auth/oauth.py +9 -9
- fastmcp/client/client.py +739 -136
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/messages.py +7 -5
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling/__init__.py +69 -0
- fastmcp/client/sampling/handlers/__init__.py +0 -0
- fastmcp/client/sampling/handlers/anthropic.py +387 -0
- fastmcp/client/sampling/handlers/openai.py +399 -0
- fastmcp/client/tasks.py +551 -0
- fastmcp/client/transports.py +72 -21
- fastmcp/contrib/component_manager/component_service.py +4 -20
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/__init__.py +5 -0
- fastmcp/experimental/sampling/handlers/openai.py +4 -169
- fastmcp/experimental/server/openapi/__init__.py +15 -13
- fastmcp/experimental/utilities/openapi/__init__.py +12 -38
- fastmcp/prompts/prompt.py +38 -38
- fastmcp/resources/resource.py +33 -16
- fastmcp/resources/template.py +69 -59
- fastmcp/server/auth/__init__.py +0 -9
- fastmcp/server/auth/auth.py +127 -3
- fastmcp/server/auth/oauth_proxy.py +47 -97
- fastmcp/server/auth/oidc_proxy.py +7 -0
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/auth/providers/oci.py +2 -2
- fastmcp/server/context.py +509 -180
- fastmcp/server/dependencies.py +464 -6
- fastmcp/server/elicitation.py +285 -47
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +15 -3
- fastmcp/server/low_level.py +56 -12
- fastmcp/server/middleware/middleware.py +2 -2
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +4 -3
- fastmcp/{experimental/server → server}/openapi/routing.py +1 -1
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +53 -40
- fastmcp/server/sampling/__init__.py +10 -0
- fastmcp/server/sampling/run.py +301 -0
- fastmcp/server/sampling/sampling_tool.py +108 -0
- fastmcp/server/server.py +793 -552
- 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 +206 -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 +101 -103
- fastmcp/tools/tool.py +83 -49
- fastmcp/tools/tool_transform.py +1 -12
- fastmcp/utilities/components.py +3 -3
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +1 -1
- fastmcp/utilities/tests.py +11 -5
- fastmcp/utilities/types.py +8 -0
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/METADATA +7 -4
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/RECORD +79 -63
- fastmcp/client/sampling.py +0 -56
- fastmcp/experimental/sampling/handlers/base.py +0 -21
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1087
- fastmcp/server/sampling/handler.py +0 -19
- fastmcp/utilities/openapi.py +0 -1568
- /fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/director.py +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/models.py +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/parser.py +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +0 -0
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/prompts/prompt.py
CHANGED
|
@@ -5,22 +5,22 @@ from __future__ import annotations as _annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import json
|
|
7
7
|
from collections.abc import Awaitable, Callable, Sequence
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Annotated, Any
|
|
9
9
|
|
|
10
10
|
import pydantic_core
|
|
11
11
|
from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
|
|
12
|
-
from mcp.types import Prompt as
|
|
13
|
-
from mcp.types import PromptArgument as
|
|
12
|
+
from mcp.types import Prompt as SDKPrompt
|
|
13
|
+
from mcp.types import PromptArgument as SDKPromptArgument
|
|
14
14
|
from pydantic import Field, TypeAdapter
|
|
15
15
|
|
|
16
16
|
from fastmcp.exceptions import PromptError
|
|
17
|
-
from fastmcp.server.dependencies import get_context
|
|
17
|
+
from fastmcp.server.dependencies import get_context, without_injected_parameters
|
|
18
|
+
from fastmcp.server.tasks.config import TaskConfig
|
|
18
19
|
from fastmcp.utilities.components import FastMCPComponent
|
|
19
20
|
from fastmcp.utilities.json_schema import compress_schema
|
|
20
21
|
from fastmcp.utilities.logging import get_logger
|
|
21
22
|
from fastmcp.utilities.types import (
|
|
22
23
|
FastMCPBaseModel,
|
|
23
|
-
find_kwarg_by_type,
|
|
24
24
|
get_cached_typeadapter,
|
|
25
25
|
)
|
|
26
26
|
|
|
@@ -89,10 +89,10 @@ class Prompt(FastMCPComponent):
|
|
|
89
89
|
*,
|
|
90
90
|
include_fastmcp_meta: bool | None = None,
|
|
91
91
|
**overrides: Any,
|
|
92
|
-
) ->
|
|
92
|
+
) -> SDKPrompt:
|
|
93
93
|
"""Convert the prompt to an MCP prompt."""
|
|
94
94
|
arguments = [
|
|
95
|
-
|
|
95
|
+
SDKPromptArgument(
|
|
96
96
|
name=arg.name,
|
|
97
97
|
description=arg.description,
|
|
98
98
|
required=arg.required,
|
|
@@ -100,7 +100,7 @@ class Prompt(FastMCPComponent):
|
|
|
100
100
|
for arg in self.arguments or []
|
|
101
101
|
]
|
|
102
102
|
|
|
103
|
-
return
|
|
103
|
+
return SDKPrompt(
|
|
104
104
|
name=overrides.get("name", self.name),
|
|
105
105
|
description=overrides.get("description", self.description),
|
|
106
106
|
arguments=arguments,
|
|
@@ -121,6 +121,7 @@ class Prompt(FastMCPComponent):
|
|
|
121
121
|
tags: set[str] | None = None,
|
|
122
122
|
enabled: bool | None = None,
|
|
123
123
|
meta: dict[str, Any] | None = None,
|
|
124
|
+
task: bool | TaskConfig | None = None,
|
|
124
125
|
) -> FunctionPrompt:
|
|
125
126
|
"""Create a Prompt from a function.
|
|
126
127
|
|
|
@@ -139,6 +140,7 @@ class Prompt(FastMCPComponent):
|
|
|
139
140
|
tags=tags,
|
|
140
141
|
enabled=enabled,
|
|
141
142
|
meta=meta,
|
|
143
|
+
task=task,
|
|
142
144
|
)
|
|
143
145
|
|
|
144
146
|
async def render(
|
|
@@ -157,6 +159,10 @@ class FunctionPrompt(Prompt):
|
|
|
157
159
|
"""A prompt that is a function."""
|
|
158
160
|
|
|
159
161
|
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
|
162
|
+
task_config: Annotated[
|
|
163
|
+
TaskConfig,
|
|
164
|
+
Field(description="Background task execution configuration (SEP-1686)."),
|
|
165
|
+
] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
|
|
160
166
|
|
|
161
167
|
@classmethod
|
|
162
168
|
def from_function(
|
|
@@ -169,6 +175,7 @@ class FunctionPrompt(Prompt):
|
|
|
169
175
|
tags: set[str] | None = None,
|
|
170
176
|
enabled: bool | None = None,
|
|
171
177
|
meta: dict[str, Any] | None = None,
|
|
178
|
+
task: bool | TaskConfig | None = None,
|
|
172
179
|
) -> FunctionPrompt:
|
|
173
180
|
"""Create a Prompt from a function.
|
|
174
181
|
|
|
@@ -178,7 +185,6 @@ class FunctionPrompt(Prompt):
|
|
|
178
185
|
- A dict (converted to a message)
|
|
179
186
|
- A sequence of any of the above
|
|
180
187
|
"""
|
|
181
|
-
from fastmcp.server.context import Context
|
|
182
188
|
|
|
183
189
|
func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
184
190
|
|
|
@@ -194,22 +200,27 @@ class FunctionPrompt(Prompt):
|
|
|
194
200
|
|
|
195
201
|
description = description or inspect.getdoc(fn)
|
|
196
202
|
|
|
203
|
+
# Normalize task to TaskConfig and validate
|
|
204
|
+
if task is None:
|
|
205
|
+
task_config = TaskConfig(mode="forbidden")
|
|
206
|
+
elif isinstance(task, bool):
|
|
207
|
+
task_config = TaskConfig.from_bool(task)
|
|
208
|
+
else:
|
|
209
|
+
task_config = task
|
|
210
|
+
task_config.validate_function(fn, func_name)
|
|
211
|
+
|
|
197
212
|
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
198
213
|
if not inspect.isroutine(fn):
|
|
199
214
|
fn = fn.__call__
|
|
200
215
|
# if the fn is a staticmethod, we need to work with the underlying function
|
|
201
216
|
if isinstance(fn, staticmethod):
|
|
202
|
-
fn = fn.__func__
|
|
217
|
+
fn = fn.__func__ # type: ignore[assignment]
|
|
203
218
|
|
|
204
|
-
|
|
219
|
+
# Wrap fn to handle dependency resolution internally
|
|
220
|
+
wrapped_fn = without_injected_parameters(fn)
|
|
221
|
+
type_adapter = get_cached_typeadapter(wrapped_fn)
|
|
205
222
|
parameters = type_adapter.json_schema()
|
|
206
|
-
|
|
207
|
-
# Auto-detect context parameter if not provided
|
|
208
|
-
|
|
209
|
-
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
210
|
-
prune_params = [context_kwarg] if context_kwarg else None
|
|
211
|
-
|
|
212
|
-
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
223
|
+
parameters = compress_schema(parameters, prune_titles=True)
|
|
213
224
|
|
|
214
225
|
# Convert parameters to PromptArguments
|
|
215
226
|
arguments: list[PromptArgument] = []
|
|
@@ -224,7 +235,6 @@ class FunctionPrompt(Prompt):
|
|
|
224
235
|
if (
|
|
225
236
|
sig_param.annotation != inspect.Parameter.empty
|
|
226
237
|
and sig_param.annotation is not str
|
|
227
|
-
and param_name != context_kwarg
|
|
228
238
|
):
|
|
229
239
|
# Get the JSON schema for this specific parameter type
|
|
230
240
|
try:
|
|
@@ -260,29 +270,23 @@ class FunctionPrompt(Prompt):
|
|
|
260
270
|
arguments=arguments,
|
|
261
271
|
tags=tags or set(),
|
|
262
272
|
enabled=enabled if enabled is not None else True,
|
|
263
|
-
fn=
|
|
273
|
+
fn=wrapped_fn,
|
|
264
274
|
meta=meta,
|
|
275
|
+
task_config=task_config,
|
|
265
276
|
)
|
|
266
277
|
|
|
267
278
|
def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
268
279
|
"""Convert string arguments to expected types based on function signature."""
|
|
269
|
-
from fastmcp.server.
|
|
280
|
+
from fastmcp.server.dependencies import without_injected_parameters
|
|
270
281
|
|
|
271
|
-
|
|
282
|
+
wrapper_fn = without_injected_parameters(self.fn)
|
|
283
|
+
sig = inspect.signature(wrapper_fn)
|
|
272
284
|
converted_kwargs = {}
|
|
273
285
|
|
|
274
|
-
# Find context parameter name if any
|
|
275
|
-
context_param_name = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
276
|
-
|
|
277
286
|
for param_name, param_value in kwargs.items():
|
|
278
287
|
if param_name in sig.parameters:
|
|
279
288
|
param = sig.parameters[param_name]
|
|
280
289
|
|
|
281
|
-
# Skip Context parameters - they're handled separately
|
|
282
|
-
if param_name == context_param_name:
|
|
283
|
-
converted_kwargs[param_name] = param_value
|
|
284
|
-
continue
|
|
285
|
-
|
|
286
290
|
# If parameter has no annotation or annotation is str, pass as-is
|
|
287
291
|
if (
|
|
288
292
|
param.annotation == inspect.Parameter.empty
|
|
@@ -320,8 +324,6 @@ class FunctionPrompt(Prompt):
|
|
|
320
324
|
arguments: dict[str, Any] | None = None,
|
|
321
325
|
) -> list[PromptMessage]:
|
|
322
326
|
"""Render the prompt with arguments."""
|
|
323
|
-
from fastmcp.server.context import Context
|
|
324
|
-
|
|
325
327
|
# Validate required arguments
|
|
326
328
|
if self.arguments:
|
|
327
329
|
required = {arg.name for arg in self.arguments if arg.required}
|
|
@@ -331,16 +333,14 @@ class FunctionPrompt(Prompt):
|
|
|
331
333
|
raise ValueError(f"Missing required arguments: {missing}")
|
|
332
334
|
|
|
333
335
|
try:
|
|
334
|
-
# Prepare arguments
|
|
336
|
+
# Prepare arguments
|
|
335
337
|
kwargs = arguments.copy() if arguments else {}
|
|
336
|
-
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
337
|
-
if context_kwarg and context_kwarg not in kwargs:
|
|
338
|
-
kwargs[context_kwarg] = get_context()
|
|
339
338
|
|
|
340
|
-
# Convert string arguments to expected types
|
|
339
|
+
# Convert string arguments to expected types BEFORE validation
|
|
341
340
|
kwargs = self._convert_string_arguments(kwargs)
|
|
342
341
|
|
|
343
|
-
#
|
|
342
|
+
# self.fn is wrapped by without_injected_parameters which handles
|
|
343
|
+
# dependency resolution internally
|
|
344
344
|
result = self.fn(**kwargs)
|
|
345
345
|
if inspect.isawaitable(result):
|
|
346
346
|
result = await result
|
fastmcp/resources/resource.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Annotated, Any
|
|
|
8
8
|
|
|
9
9
|
import pydantic_core
|
|
10
10
|
from mcp.types import Annotations, Icon
|
|
11
|
-
from mcp.types import Resource as
|
|
11
|
+
from mcp.types import Resource as SDKResource
|
|
12
12
|
from pydantic import (
|
|
13
13
|
AnyUrl,
|
|
14
14
|
ConfigDict,
|
|
@@ -19,10 +19,10 @@ from pydantic import (
|
|
|
19
19
|
)
|
|
20
20
|
from typing_extensions import Self
|
|
21
21
|
|
|
22
|
-
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
|
|
23
24
|
from fastmcp.utilities.components import FastMCPComponent
|
|
24
25
|
from fastmcp.utilities.types import (
|
|
25
|
-
find_kwarg_by_type,
|
|
26
26
|
get_fn_name,
|
|
27
27
|
)
|
|
28
28
|
|
|
@@ -42,7 +42,6 @@ class Resource(FastMCPComponent):
|
|
|
42
42
|
mime_type: str = Field(
|
|
43
43
|
default="text/plain",
|
|
44
44
|
description="MIME type of the resource content",
|
|
45
|
-
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
|
46
45
|
)
|
|
47
46
|
annotations: Annotated[
|
|
48
47
|
Annotations | None,
|
|
@@ -78,6 +77,7 @@ class Resource(FastMCPComponent):
|
|
|
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,
|
|
@@ -91,6 +91,7 @@ class Resource(FastMCPComponent):
|
|
|
91
91
|
enabled=enabled,
|
|
92
92
|
annotations=annotations,
|
|
93
93
|
meta=meta,
|
|
94
|
+
task=task,
|
|
94
95
|
)
|
|
95
96
|
|
|
96
97
|
@field_validator("mime_type", mode="before")
|
|
@@ -125,10 +126,10 @@ class Resource(FastMCPComponent):
|
|
|
125
126
|
*,
|
|
126
127
|
include_fastmcp_meta: bool | None = None,
|
|
127
128
|
**overrides: Any,
|
|
128
|
-
) ->
|
|
129
|
-
"""Convert the resource to an
|
|
129
|
+
) -> SDKResource:
|
|
130
|
+
"""Convert the resource to an SDKResource."""
|
|
130
131
|
|
|
131
|
-
return
|
|
132
|
+
return SDKResource(
|
|
132
133
|
name=overrides.get("name", self.name),
|
|
133
134
|
uri=overrides.get("uri", self.uri),
|
|
134
135
|
description=overrides.get("description", self.description),
|
|
@@ -169,6 +170,10 @@ class FunctionResource(Resource):
|
|
|
169
170
|
"""
|
|
170
171
|
|
|
171
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"))
|
|
172
177
|
|
|
173
178
|
@classmethod
|
|
174
179
|
def from_function(
|
|
@@ -184,12 +189,28 @@ class FunctionResource(Resource):
|
|
|
184
189
|
enabled: bool | None = None,
|
|
185
190
|
annotations: Annotations | None = None,
|
|
186
191
|
meta: dict[str, Any] | None = None,
|
|
192
|
+
task: bool | TaskConfig | None = None,
|
|
187
193
|
) -> FunctionResource:
|
|
188
194
|
"""Create a FunctionResource from a function."""
|
|
189
195
|
if isinstance(uri, str):
|
|
190
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
|
+
|
|
191
212
|
return cls(
|
|
192
|
-
fn=
|
|
213
|
+
fn=wrapped_fn,
|
|
193
214
|
uri=uri,
|
|
194
215
|
name=name or get_fn_name(fn),
|
|
195
216
|
title=title,
|
|
@@ -200,18 +221,14 @@ class FunctionResource(Resource):
|
|
|
200
221
|
enabled=enabled if enabled is not None else True,
|
|
201
222
|
annotations=annotations,
|
|
202
223
|
meta=meta,
|
|
224
|
+
task_config=task_config,
|
|
203
225
|
)
|
|
204
226
|
|
|
205
227
|
async def read(self) -> str | bytes:
|
|
206
228
|
"""Read the resource by calling the wrapped function."""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
211
|
-
if context_kwarg is not None:
|
|
212
|
-
kwargs[context_kwarg] = get_context()
|
|
213
|
-
|
|
214
|
-
result = self.fn(**kwargs)
|
|
229
|
+
# self.fn is wrapped by without_injected_parameters which handles
|
|
230
|
+
# dependency resolution internally
|
|
231
|
+
result = self.fn()
|
|
215
232
|
if inspect.isawaitable(result):
|
|
216
233
|
result = await result
|
|
217
234
|
|
fastmcp/resources/template.py
CHANGED
|
@@ -5,11 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import re
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Annotated, Any
|
|
9
9
|
from urllib.parse import parse_qs, unquote
|
|
10
10
|
|
|
11
11
|
from mcp.types import Annotations, Icon
|
|
12
|
-
from mcp.types import ResourceTemplate as
|
|
12
|
+
from mcp.types import ResourceTemplate as SDKResourceTemplate
|
|
13
13
|
from pydantic import (
|
|
14
14
|
Field,
|
|
15
15
|
field_validator,
|
|
@@ -17,13 +17,11 @@ 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
|
-
find_kwarg_by_type,
|
|
25
|
-
get_cached_typeadapter,
|
|
26
|
-
)
|
|
24
|
+
from fastmcp.utilities.types import get_cached_typeadapter
|
|
27
25
|
|
|
28
26
|
|
|
29
27
|
def extract_query_params(uri_template: str) -> set[str]:
|
|
@@ -139,6 +137,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
139
137
|
enabled: bool | None = None,
|
|
140
138
|
annotations: Annotations | None = None,
|
|
141
139
|
meta: dict[str, Any] | None = None,
|
|
140
|
+
task: bool | TaskConfig | None = None,
|
|
142
141
|
) -> FunctionResourceTemplate:
|
|
143
142
|
return FunctionResourceTemplate.from_function(
|
|
144
143
|
fn=fn,
|
|
@@ -152,6 +151,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
152
151
|
enabled=enabled,
|
|
153
152
|
annotations=annotations,
|
|
154
153
|
meta=meta,
|
|
154
|
+
task=task,
|
|
155
155
|
)
|
|
156
156
|
|
|
157
157
|
@field_validator("mime_type", mode="before")
|
|
@@ -173,21 +173,14 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
173
173
|
)
|
|
174
174
|
|
|
175
175
|
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
176
|
-
"""Create a resource from the template with the given parameters.
|
|
177
|
-
|
|
178
|
-
async def resource_read_fn() -> str | bytes:
|
|
179
|
-
# Call function and check if result is a coroutine
|
|
180
|
-
result = await self.read(arguments=params)
|
|
181
|
-
return result
|
|
176
|
+
"""Create a resource from the template with the given parameters.
|
|
182
177
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
tags=self.tags,
|
|
190
|
-
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."
|
|
191
184
|
)
|
|
192
185
|
|
|
193
186
|
def to_mcp_template(
|
|
@@ -195,10 +188,10 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
195
188
|
*,
|
|
196
189
|
include_fastmcp_meta: bool | None = None,
|
|
197
190
|
**overrides: Any,
|
|
198
|
-
) ->
|
|
199
|
-
"""Convert the resource template to an
|
|
191
|
+
) -> SDKResourceTemplate:
|
|
192
|
+
"""Convert the resource template to an SDKResourceTemplate."""
|
|
200
193
|
|
|
201
|
-
return
|
|
194
|
+
return SDKResourceTemplate(
|
|
202
195
|
name=overrides.get("name", self.name),
|
|
203
196
|
uriTemplate=overrides.get("uriTemplate", self.uri_template),
|
|
204
197
|
description=overrides.get("description", self.description),
|
|
@@ -212,7 +205,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
212
205
|
)
|
|
213
206
|
|
|
214
207
|
@classmethod
|
|
215
|
-
def from_mcp_template(cls, mcp_template:
|
|
208
|
+
def from_mcp_template(cls, mcp_template: SDKResourceTemplate) -> ResourceTemplate:
|
|
216
209
|
"""Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object."""
|
|
217
210
|
# Note: This creates a simple ResourceTemplate instance. For function-based templates,
|
|
218
211
|
# the original function is lost, which is expected for remote templates.
|
|
@@ -239,45 +232,59 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
239
232
|
"""A template for dynamically creating resources."""
|
|
240
233
|
|
|
241
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"))
|
|
242
239
|
|
|
243
|
-
async def
|
|
244
|
-
"""
|
|
245
|
-
from fastmcp.server.context import Context
|
|
240
|
+
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
241
|
+
"""Create a resource from the template with the given parameters."""
|
|
246
242
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
)
|
|
252
258
|
|
|
259
|
+
async def read(self, arguments: dict[str, Any]) -> str | bytes:
|
|
260
|
+
"""Read the resource content."""
|
|
253
261
|
# Type coercion for query parameters (which arrive as strings)
|
|
254
|
-
|
|
262
|
+
kwargs = arguments.copy()
|
|
255
263
|
sig = inspect.signature(self.fn)
|
|
256
264
|
for param_name, param_value in list(kwargs.items()):
|
|
257
265
|
if param_name in sig.parameters and isinstance(param_value, str):
|
|
258
266
|
param = sig.parameters[param_name]
|
|
259
267
|
annotation = param.annotation
|
|
260
268
|
|
|
261
|
-
# Skip if no annotation or annotation is str
|
|
262
269
|
if annotation is inspect.Parameter.empty or annotation is str:
|
|
263
270
|
continue
|
|
264
271
|
|
|
265
|
-
# Handle common type coercions
|
|
266
272
|
try:
|
|
267
273
|
if annotation is int:
|
|
268
274
|
kwargs[param_name] = int(param_value)
|
|
269
275
|
elif annotation is float:
|
|
270
276
|
kwargs[param_name] = float(param_value)
|
|
271
277
|
elif annotation is bool:
|
|
272
|
-
# Handle boolean strings
|
|
273
278
|
kwargs[param_name] = param_value.lower() in ("true", "1", "yes")
|
|
274
279
|
except (ValueError, AttributeError):
|
|
275
|
-
# Let validate_call handle the error
|
|
276
280
|
pass
|
|
277
281
|
|
|
282
|
+
# self.fn is wrapped by without_injected_parameters which handles
|
|
283
|
+
# dependency resolution internally, so we call it directly
|
|
278
284
|
result = self.fn(**kwargs)
|
|
279
285
|
if inspect.isawaitable(result):
|
|
280
286
|
result = await result
|
|
287
|
+
|
|
281
288
|
return result
|
|
282
289
|
|
|
283
290
|
@classmethod
|
|
@@ -294,9 +301,9 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
294
301
|
enabled: bool | None = None,
|
|
295
302
|
annotations: Annotations | None = None,
|
|
296
303
|
meta: dict[str, Any] | None = None,
|
|
304
|
+
task: bool | TaskConfig | None = None,
|
|
297
305
|
) -> FunctionResourceTemplate:
|
|
298
306
|
"""Create a template from a function."""
|
|
299
|
-
from fastmcp.server.context import Context
|
|
300
307
|
|
|
301
308
|
func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
302
309
|
if func_name == "<lambda>":
|
|
@@ -311,10 +318,6 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
311
318
|
"Functions with *args are not supported as resource templates"
|
|
312
319
|
)
|
|
313
320
|
|
|
314
|
-
# Auto-detect context parameter if not provided
|
|
315
|
-
|
|
316
|
-
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
317
|
-
|
|
318
321
|
# Extract path and query parameters from URI template
|
|
319
322
|
path_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
|
320
323
|
query_params = extract_query_params(uri_template)
|
|
@@ -323,24 +326,23 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
323
326
|
if not all_uri_params:
|
|
324
327
|
raise ValueError("URI template must contain at least one parameter")
|
|
325
328
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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())
|
|
329
333
|
|
|
330
334
|
# Get required and optional function parameters
|
|
331
335
|
required_params = {
|
|
332
336
|
p
|
|
333
337
|
for p in func_params
|
|
334
|
-
if
|
|
335
|
-
and
|
|
336
|
-
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
|
|
337
340
|
}
|
|
338
341
|
optional_params = {
|
|
339
342
|
p
|
|
340
343
|
for p in func_params
|
|
341
|
-
if
|
|
342
|
-
and
|
|
343
|
-
and p != context_kwarg
|
|
344
|
+
if user_sig.parameters[p].default is not inspect.Parameter.empty
|
|
345
|
+
and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
|
|
344
346
|
}
|
|
345
347
|
|
|
346
348
|
# Validate RFC 6570 query parameters
|
|
@@ -370,6 +372,15 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
370
372
|
|
|
371
373
|
description = description or inspect.getdoc(fn)
|
|
372
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
|
+
|
|
373
384
|
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
374
385
|
if not inspect.isroutine(fn):
|
|
375
386
|
fn = fn.__call__
|
|
@@ -377,15 +388,13 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
377
388
|
if isinstance(fn, staticmethod):
|
|
378
389
|
fn = fn.__func__
|
|
379
390
|
|
|
380
|
-
|
|
391
|
+
wrapper_fn = without_injected_parameters(fn)
|
|
392
|
+
type_adapter = get_cached_typeadapter(wrapper_fn)
|
|
381
393
|
parameters = type_adapter.json_schema()
|
|
394
|
+
parameters = compress_schema(parameters, prune_titles=True)
|
|
382
395
|
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
386
|
-
|
|
387
|
-
# ensure the arguments are properly cast
|
|
388
|
-
fn = validate_call(fn)
|
|
396
|
+
# Use validate_call on wrapper for runtime type coercion
|
|
397
|
+
fn = validate_call(wrapper_fn)
|
|
389
398
|
|
|
390
399
|
return cls(
|
|
391
400
|
uri_template=uri_template,
|
|
@@ -400,4 +409,5 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
400
409
|
enabled=enabled if enabled is not None else True,
|
|
401
410
|
annotations=annotations,
|
|
402
411
|
meta=meta,
|
|
412
|
+
task_config=task_config,
|
|
403
413
|
)
|
fastmcp/server/auth/__init__.py
CHANGED
|
@@ -23,12 +23,3 @@ __all__ = [
|
|
|
23
23
|
"StaticTokenVerifier",
|
|
24
24
|
"TokenVerifier",
|
|
25
25
|
]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def __getattr__(name: str):
|
|
29
|
-
# Defer import because it raises a deprecation warning
|
|
30
|
-
if name == "BearerAuthProvider":
|
|
31
|
-
from .providers.bearer import BearerAuthProvider
|
|
32
|
-
|
|
33
|
-
return BearerAuthProvider
|
|
34
|
-
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|