fastmcp 2.13.2__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 +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 +665 -129
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/messages.py +7 -5
- fastmcp/client/roots.py +2 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +37 -5
- fastmcp/contrib/component_manager/component_service.py +4 -20
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +1 -1
- fastmcp/experimental/server/openapi/__init__.py +15 -13
- fastmcp/experimental/utilities/openapi/__init__.py +12 -38
- fastmcp/prompts/prompt.py +33 -33
- fastmcp/resources/resource.py +29 -12
- fastmcp/resources/template.py +64 -54
- 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 +66 -72
- 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 +50 -37
- fastmcp/server/server.py +731 -532
- 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 +101 -103
- fastmcp/tools/tool.py +80 -44
- 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.2.dist-info → fastmcp-2.14.0.dist-info}/METADATA +5 -4
- {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/RECORD +71 -59
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1087
- 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.2.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/transports.py
CHANGED
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
import shutil
|
|
7
7
|
import sys
|
|
8
8
|
import warnings
|
|
9
|
-
from collections.abc import AsyncIterator
|
|
9
|
+
from collections.abc import AsyncIterator, Callable
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Literal, TextIO, TypeVar, cast, overload
|
|
12
12
|
|
|
@@ -36,6 +36,7 @@ from fastmcp.client.auth.oauth import OAuth
|
|
|
36
36
|
from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
|
|
37
37
|
from fastmcp.server.dependencies import get_http_headers
|
|
38
38
|
from fastmcp.server.server import FastMCP
|
|
39
|
+
from fastmcp.server.tasks.capabilities import get_task_capabilities
|
|
39
40
|
from fastmcp.utilities.logging import get_logger
|
|
40
41
|
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
|
41
42
|
|
|
@@ -254,6 +255,8 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
254
255
|
sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
|
|
255
256
|
self.sse_read_timeout = sse_read_timeout
|
|
256
257
|
|
|
258
|
+
self._get_session_id_cb: Callable[[], str | None] | None = None
|
|
259
|
+
|
|
257
260
|
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
|
|
258
261
|
if auth == "oauth":
|
|
259
262
|
auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
|
|
@@ -287,12 +290,25 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
287
290
|
auth=self.auth,
|
|
288
291
|
**client_kwargs,
|
|
289
292
|
) as transport:
|
|
290
|
-
read_stream, write_stream,
|
|
293
|
+
read_stream, write_stream, get_session_id = transport
|
|
294
|
+
self._get_session_id_cb = get_session_id
|
|
291
295
|
async with ClientSession(
|
|
292
296
|
read_stream, write_stream, **session_kwargs
|
|
293
297
|
) as session:
|
|
294
298
|
yield session
|
|
295
299
|
|
|
300
|
+
def get_session_id(self) -> str | None:
|
|
301
|
+
if self._get_session_id_cb:
|
|
302
|
+
try:
|
|
303
|
+
return self._get_session_id_cb()
|
|
304
|
+
except Exception:
|
|
305
|
+
return None
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
async def close(self):
|
|
309
|
+
# Reset the session id callback
|
|
310
|
+
self._get_session_id_cb = None
|
|
311
|
+
|
|
296
312
|
def __repr__(self) -> str:
|
|
297
313
|
return f"<StreamableHttpTransport(url='{self.url}')>"
|
|
298
314
|
|
|
@@ -375,7 +391,8 @@ class StdioTransport(ClientTransport):
|
|
|
375
391
|
env=self.env,
|
|
376
392
|
cwd=self.cwd,
|
|
377
393
|
log_file=self.log_file,
|
|
378
|
-
|
|
394
|
+
# TODO(ty): remove when ty supports Unpack[TypedDict] inference
|
|
395
|
+
session_kwargs=session_kwargs, # type: ignore[arg-type]
|
|
379
396
|
ready_event=self._ready_event,
|
|
380
397
|
stop_event=self._stop_event,
|
|
381
398
|
session_future=session_future,
|
|
@@ -849,16 +866,25 @@ class FastMCPTransport(ClientTransport):
|
|
|
849
866
|
client_read, client_write = client_streams
|
|
850
867
|
server_read, server_write = server_streams
|
|
851
868
|
|
|
852
|
-
#
|
|
869
|
+
# Capture exceptions to re-raise after task group cleanup.
|
|
870
|
+
# anyio task groups can suppress exceptions when cancel_scope.cancel()
|
|
871
|
+
# is called during cleanup, so we capture and re-raise manually.
|
|
872
|
+
exception_to_raise: BaseException | None = None
|
|
873
|
+
|
|
853
874
|
async with (
|
|
854
875
|
anyio.create_task_group() as tg,
|
|
855
876
|
_enter_server_lifespan(server=self.server),
|
|
856
877
|
):
|
|
878
|
+
# Build experimental capabilities
|
|
879
|
+
experimental_capabilities = get_task_capabilities()
|
|
880
|
+
|
|
857
881
|
tg.start_soon(
|
|
858
882
|
lambda: self.server._mcp_server.run(
|
|
859
883
|
server_read,
|
|
860
884
|
server_write,
|
|
861
|
-
self.server._mcp_server.create_initialization_options(
|
|
885
|
+
self.server._mcp_server.create_initialization_options(
|
|
886
|
+
experimental_capabilities=experimental_capabilities
|
|
887
|
+
),
|
|
862
888
|
raise_exceptions=self.raise_exceptions,
|
|
863
889
|
)
|
|
864
890
|
)
|
|
@@ -870,9 +896,15 @@ class FastMCPTransport(ClientTransport):
|
|
|
870
896
|
**session_kwargs,
|
|
871
897
|
) as client_session:
|
|
872
898
|
yield client_session
|
|
899
|
+
except BaseException as e:
|
|
900
|
+
exception_to_raise = e
|
|
873
901
|
finally:
|
|
874
902
|
tg.cancel_scope.cancel()
|
|
875
903
|
|
|
904
|
+
# Re-raise after task group has exited cleanly
|
|
905
|
+
if exception_to_raise is not None:
|
|
906
|
+
raise exception_to_raise
|
|
907
|
+
|
|
876
908
|
def __repr__(self) -> str:
|
|
877
909
|
return f"<FastMCPTransport(server='{self.server.name}')>"
|
|
878
910
|
|
|
@@ -105,16 +105,8 @@ class ComponentService:
|
|
|
105
105
|
# 2. Check mounted servers using the filtered protocol path.
|
|
106
106
|
for mounted in reversed(self._server._mounted_servers):
|
|
107
107
|
if mounted.prefix:
|
|
108
|
-
if has_resource_prefix(
|
|
109
|
-
key,
|
|
110
|
-
mounted.prefix,
|
|
111
|
-
mounted.resource_prefix_format,
|
|
112
|
-
):
|
|
113
|
-
key = remove_resource_prefix(
|
|
114
|
-
key,
|
|
115
|
-
mounted.prefix,
|
|
116
|
-
mounted.resource_prefix_format,
|
|
117
|
-
)
|
|
108
|
+
if has_resource_prefix(key, mounted.prefix):
|
|
109
|
+
key = remove_resource_prefix(key, mounted.prefix)
|
|
118
110
|
mounted_service = ComponentService(mounted.server)
|
|
119
111
|
mounted_resource: (
|
|
120
112
|
Resource | ResourceTemplate
|
|
@@ -148,16 +140,8 @@ class ComponentService:
|
|
|
148
140
|
# 2. Check mounted servers using the filtered protocol path.
|
|
149
141
|
for mounted in reversed(self._server._mounted_servers):
|
|
150
142
|
if mounted.prefix:
|
|
151
|
-
if has_resource_prefix(
|
|
152
|
-
key,
|
|
153
|
-
mounted.prefix,
|
|
154
|
-
mounted.resource_prefix_format,
|
|
155
|
-
):
|
|
156
|
-
key = remove_resource_prefix(
|
|
157
|
-
key,
|
|
158
|
-
mounted.prefix,
|
|
159
|
-
mounted.resource_prefix_format,
|
|
160
|
-
)
|
|
143
|
+
if has_resource_prefix(key, mounted.prefix):
|
|
144
|
+
key = remove_resource_prefix(key, mounted.prefix)
|
|
161
145
|
mounted_service = ComponentService(mounted.server)
|
|
162
146
|
mounted_resource: (
|
|
163
147
|
Resource | ResourceTemplate
|
fastmcp/dependencies.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Dependency injection exports for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module re-exports dependency injection symbols from Docket and FastMCP
|
|
4
|
+
to provide a clean, centralized import location for all dependency-related
|
|
5
|
+
functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from docket import Depends
|
|
9
|
+
|
|
10
|
+
from fastmcp.server.dependencies import (
|
|
11
|
+
CurrentContext,
|
|
12
|
+
CurrentDocket,
|
|
13
|
+
CurrentFastMCP,
|
|
14
|
+
CurrentWorker,
|
|
15
|
+
Progress,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CurrentContext",
|
|
20
|
+
"CurrentDocket",
|
|
21
|
+
"CurrentFastMCP",
|
|
22
|
+
"CurrentWorker",
|
|
23
|
+
"Depends",
|
|
24
|
+
"Progress",
|
|
25
|
+
]
|
|
@@ -164,7 +164,7 @@ class OpenAISamplingHandler(BaseLLMSamplingHandler):
|
|
|
164
164
|
) -> ChatModel:
|
|
165
165
|
for model_option in self._iter_models_from_preferences(model_preferences):
|
|
166
166
|
if model_option in get_args(ChatModel):
|
|
167
|
-
chosen_model: ChatModel = model_option #
|
|
167
|
+
chosen_model: ChatModel = model_option # type: ignore[assignment]
|
|
168
168
|
return chosen_model
|
|
169
169
|
|
|
170
170
|
return self.default_model
|
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Deprecated: Import from fastmcp.server.openapi instead."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from .server import FastMCPOpenAPI
|
|
3
|
+
import warnings
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
from fastmcp.server.openapi import (
|
|
6
|
+
ComponentFn,
|
|
7
|
+
DEFAULT_ROUTE_MAPPINGS,
|
|
8
|
+
FastMCPOpenAPI,
|
|
8
9
|
MCPType,
|
|
10
|
+
OpenAPIResource,
|
|
11
|
+
OpenAPIResourceTemplate,
|
|
12
|
+
OpenAPITool,
|
|
9
13
|
RouteMap,
|
|
10
14
|
RouteMapFn,
|
|
11
|
-
ComponentFn,
|
|
12
|
-
DEFAULT_ROUTE_MAPPINGS,
|
|
13
15
|
_determine_route_type,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
# Deprecated in 2.14 when OpenAPI support was promoted out of experimental
|
|
19
|
+
warnings.warn(
|
|
20
|
+
"Importing from fastmcp.experimental.server.openapi is deprecated. "
|
|
21
|
+
"Import from fastmcp.server.openapi instead.",
|
|
22
|
+
DeprecationWarning,
|
|
23
|
+
stacklevel=2,
|
|
21
24
|
)
|
|
22
25
|
|
|
23
|
-
# Export public symbols - maintaining backward compatibility
|
|
24
26
|
__all__ = [
|
|
25
27
|
"DEFAULT_ROUTE_MAPPINGS",
|
|
26
28
|
"ComponentFn",
|
|
@@ -1,63 +1,37 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Deprecated: Import from fastmcp.utilities.openapi instead."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from fastmcp.utilities.openapi import (
|
|
5
6
|
HTTPRoute,
|
|
6
7
|
HttpMethod,
|
|
7
|
-
JsonSchema,
|
|
8
8
|
ParameterInfo,
|
|
9
9
|
ParameterLocation,
|
|
10
10
|
RequestBodyInfo,
|
|
11
11
|
ResponseInfo,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Import from parser
|
|
15
|
-
from .parser import parse_openapi_to_http_routes
|
|
16
|
-
|
|
17
|
-
# Import from formatters
|
|
18
|
-
from .formatters import (
|
|
19
|
-
format_array_parameter,
|
|
20
|
-
format_deep_object_parameter,
|
|
21
|
-
format_description_with_responses,
|
|
22
|
-
format_json_for_description,
|
|
12
|
+
extract_output_schema_from_responses,
|
|
23
13
|
format_simple_description,
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
# Import from schemas
|
|
28
|
-
from .schemas import (
|
|
14
|
+
parse_openapi_to_http_routes,
|
|
29
15
|
_combine_schemas,
|
|
30
|
-
extract_output_schema_from_responses,
|
|
31
|
-
clean_schema_for_display,
|
|
32
|
-
_make_optional_parameter_nullable,
|
|
33
16
|
)
|
|
34
17
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
# Deprecated in 2.14 when OpenAPI support was promoted out of experimental
|
|
19
|
+
warnings.warn(
|
|
20
|
+
"Importing from fastmcp.experimental.utilities.openapi is deprecated. "
|
|
21
|
+
"Import from fastmcp.utilities.openapi instead.",
|
|
22
|
+
DeprecationWarning,
|
|
23
|
+
stacklevel=2,
|
|
39
24
|
)
|
|
40
25
|
|
|
41
|
-
# Export public symbols - maintaining backward compatibility
|
|
42
26
|
__all__ = [
|
|
43
27
|
"HTTPRoute",
|
|
44
28
|
"HttpMethod",
|
|
45
|
-
"JsonSchema",
|
|
46
29
|
"ParameterInfo",
|
|
47
30
|
"ParameterLocation",
|
|
48
31
|
"RequestBodyInfo",
|
|
49
32
|
"ResponseInfo",
|
|
50
33
|
"_combine_schemas",
|
|
51
|
-
"_make_optional_parameter_nullable",
|
|
52
|
-
"clean_schema_for_display",
|
|
53
|
-
"convert_openapi_schema_to_json_schema",
|
|
54
|
-
"convert_schema_definitions",
|
|
55
34
|
"extract_output_schema_from_responses",
|
|
56
|
-
"format_array_parameter",
|
|
57
|
-
"format_deep_object_parameter",
|
|
58
|
-
"format_description_with_responses",
|
|
59
|
-
"format_json_for_description",
|
|
60
35
|
"format_simple_description",
|
|
61
|
-
"generate_example_from_schema",
|
|
62
36
|
"parse_openapi_to_http_routes",
|
|
63
37
|
]
|
fastmcp/prompts/prompt.py
CHANGED
|
@@ -5,7 +5,7 @@ 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
|
|
@@ -14,13 +14,13 @@ from mcp.types import PromptArgument as MCPPromptArgument
|
|
|
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
|
|
|
@@ -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
|
@@ -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")
|
|
@@ -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
|
|