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/client/elicitation.py
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Client mixins for FastMCP."""
|
|
2
|
+
|
|
3
|
+
from fastmcp.client.mixins.prompts import ClientPromptsMixin
|
|
4
|
+
from fastmcp.client.mixins.resources import ClientResourcesMixin
|
|
5
|
+
from fastmcp.client.mixins.task_management import ClientTaskManagementMixin
|
|
6
|
+
from fastmcp.client.mixins.tools import ClientToolsMixin
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ClientPromptsMixin",
|
|
10
|
+
"ClientResourcesMixin",
|
|
11
|
+
"ClientTaskManagementMixin",
|
|
12
|
+
"ClientToolsMixin",
|
|
13
|
+
]
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Prompt-related methods for FastMCP Client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
import weakref
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
8
|
+
|
|
9
|
+
import mcp.types
|
|
10
|
+
import pydantic_core
|
|
11
|
+
from pydantic import RootModel
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from fastmcp.client.client import Client
|
|
15
|
+
|
|
16
|
+
from fastmcp.client.tasks import PromptTask
|
|
17
|
+
from fastmcp.client.telemetry import client_span
|
|
18
|
+
from fastmcp.telemetry import inject_trace_context
|
|
19
|
+
from fastmcp.utilities.logging import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
# Type alias for task response union (SEP-1686 graceful degradation)
|
|
24
|
+
PromptTaskResponseUnion = RootModel[
|
|
25
|
+
mcp.types.CreateTaskResult | mcp.types.GetPromptResult
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ClientPromptsMixin:
|
|
30
|
+
"""Mixin providing prompt-related methods for Client."""
|
|
31
|
+
|
|
32
|
+
# --- Prompts ---
|
|
33
|
+
|
|
34
|
+
async def list_prompts_mcp(
|
|
35
|
+
self: Client, *, cursor: str | None = None
|
|
36
|
+
) -> mcp.types.ListPromptsResult:
|
|
37
|
+
"""Send a prompts/list request and return the complete MCP protocol result.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
cursor: Optional pagination cursor from a previous request's nextCursor.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
mcp.types.ListPromptsResult: The complete response object from the protocol,
|
|
44
|
+
containing the list of prompts and any additional metadata.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
RuntimeError: If called while the client is not connected.
|
|
48
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
49
|
+
"""
|
|
50
|
+
logger.debug(f"[{self.name}] called list_prompts")
|
|
51
|
+
|
|
52
|
+
result = await self._await_with_session_monitoring(
|
|
53
|
+
self.session.list_prompts(cursor=cursor)
|
|
54
|
+
)
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
async def list_prompts(self: Client) -> list[mcp.types.Prompt]:
|
|
58
|
+
"""Retrieve all prompts available on the server.
|
|
59
|
+
|
|
60
|
+
This method automatically fetches all pages if the server paginates results,
|
|
61
|
+
returning the complete list. For manual pagination control (e.g., to handle
|
|
62
|
+
large result sets incrementally), use list_prompts_mcp() with the cursor parameter.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
list[mcp.types.Prompt]: A list of all Prompt objects.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
RuntimeError: If called while the client is not connected.
|
|
69
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
70
|
+
"""
|
|
71
|
+
all_prompts: list[mcp.types.Prompt] = []
|
|
72
|
+
cursor: str | None = None
|
|
73
|
+
|
|
74
|
+
while True:
|
|
75
|
+
result = await self.list_prompts_mcp(cursor=cursor)
|
|
76
|
+
all_prompts.extend(result.prompts)
|
|
77
|
+
if result.nextCursor is None:
|
|
78
|
+
break
|
|
79
|
+
cursor = result.nextCursor
|
|
80
|
+
|
|
81
|
+
return all_prompts
|
|
82
|
+
|
|
83
|
+
# --- Prompt ---
|
|
84
|
+
async def get_prompt_mcp(
|
|
85
|
+
self: Client,
|
|
86
|
+
name: str,
|
|
87
|
+
arguments: dict[str, Any] | None = None,
|
|
88
|
+
meta: dict[str, Any] | None = None,
|
|
89
|
+
) -> mcp.types.GetPromptResult:
|
|
90
|
+
"""Send a prompts/get request and return the complete MCP protocol result.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name (str): The name of the prompt to retrieve.
|
|
94
|
+
arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
|
|
95
|
+
meta (dict[str, Any] | None, optional): Request metadata (e.g., for SEP-1686 tasks). Defaults to None.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
mcp.types.GetPromptResult: The complete response object from the protocol,
|
|
99
|
+
containing the prompt messages and any additional metadata.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
RuntimeError: If called while the client is not connected.
|
|
103
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
104
|
+
"""
|
|
105
|
+
with client_span(
|
|
106
|
+
f"prompts/get {name}",
|
|
107
|
+
"prompts/get",
|
|
108
|
+
name,
|
|
109
|
+
session_id=self.transport.get_session_id(),
|
|
110
|
+
):
|
|
111
|
+
logger.debug(f"[{self.name}] called get_prompt: {name}")
|
|
112
|
+
|
|
113
|
+
# Serialize arguments for MCP protocol - convert non-string values to JSON
|
|
114
|
+
serialized_arguments: dict[str, str] | None = None
|
|
115
|
+
if arguments:
|
|
116
|
+
serialized_arguments = {}
|
|
117
|
+
for key, value in arguments.items():
|
|
118
|
+
if isinstance(value, str):
|
|
119
|
+
serialized_arguments[key] = value
|
|
120
|
+
else:
|
|
121
|
+
# Use pydantic_core.to_json for consistent serialization
|
|
122
|
+
serialized_arguments[key] = pydantic_core.to_json(value).decode(
|
|
123
|
+
"utf-8"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Inject trace context into meta for propagation to server
|
|
127
|
+
propagated_meta = inject_trace_context(meta)
|
|
128
|
+
|
|
129
|
+
# If meta provided, use send_request for SEP-1686 task support
|
|
130
|
+
if propagated_meta:
|
|
131
|
+
task_dict = propagated_meta.get("modelcontextprotocol.io/task")
|
|
132
|
+
request = mcp.types.GetPromptRequest(
|
|
133
|
+
params=mcp.types.GetPromptRequestParams(
|
|
134
|
+
name=name,
|
|
135
|
+
arguments=serialized_arguments,
|
|
136
|
+
task=mcp.types.TaskMetadata(**task_dict) if task_dict else None,
|
|
137
|
+
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
result = await self._await_with_session_monitoring(
|
|
141
|
+
self.session.send_request(
|
|
142
|
+
request=request, # type: ignore[arg-type]
|
|
143
|
+
result_type=mcp.types.GetPromptResult,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
result = await self._await_with_session_monitoring(
|
|
148
|
+
self.session.get_prompt(name=name, arguments=serialized_arguments)
|
|
149
|
+
)
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
@overload
|
|
153
|
+
async def get_prompt(
|
|
154
|
+
self: Client,
|
|
155
|
+
name: str,
|
|
156
|
+
arguments: dict[str, Any] | None = None,
|
|
157
|
+
*,
|
|
158
|
+
version: str | None = None,
|
|
159
|
+
meta: dict[str, Any] | None = None,
|
|
160
|
+
task: Literal[False] = False,
|
|
161
|
+
) -> mcp.types.GetPromptResult: ...
|
|
162
|
+
|
|
163
|
+
@overload
|
|
164
|
+
async def get_prompt(
|
|
165
|
+
self: Client,
|
|
166
|
+
name: str,
|
|
167
|
+
arguments: dict[str, Any] | None = None,
|
|
168
|
+
*,
|
|
169
|
+
version: str | None = None,
|
|
170
|
+
meta: dict[str, Any] | None = None,
|
|
171
|
+
task: Literal[True],
|
|
172
|
+
task_id: str | None = None,
|
|
173
|
+
ttl: int = 60000,
|
|
174
|
+
) -> PromptTask: ...
|
|
175
|
+
|
|
176
|
+
async def get_prompt(
|
|
177
|
+
self: Client,
|
|
178
|
+
name: str,
|
|
179
|
+
arguments: dict[str, Any] | None = None,
|
|
180
|
+
*,
|
|
181
|
+
version: str | None = None,
|
|
182
|
+
meta: dict[str, Any] | None = None,
|
|
183
|
+
task: bool = False,
|
|
184
|
+
task_id: str | None = None,
|
|
185
|
+
ttl: int = 60000,
|
|
186
|
+
) -> mcp.types.GetPromptResult | PromptTask:
|
|
187
|
+
"""Retrieve a rendered prompt message list from the server.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
name (str): The name of the prompt to retrieve.
|
|
191
|
+
arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
|
|
192
|
+
version (str | None, optional): Specific prompt version to get. If None, gets highest version.
|
|
193
|
+
meta (dict[str, Any] | None): Optional request-level metadata.
|
|
194
|
+
task (bool): If True, execute as background task (SEP-1686). Defaults to False.
|
|
195
|
+
task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
|
|
196
|
+
ttl (int): Time to keep results available in milliseconds (default 60s).
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
mcp.types.GetPromptResult | PromptTask: The complete response object if task=False,
|
|
200
|
+
or a PromptTask object if task=True.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
RuntimeError: If called while the client is not connected.
|
|
204
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
205
|
+
"""
|
|
206
|
+
# Merge version into request-level meta (not arguments)
|
|
207
|
+
request_meta = dict(meta) if meta else {}
|
|
208
|
+
if version is not None:
|
|
209
|
+
request_meta["fastmcp"] = {
|
|
210
|
+
**request_meta.get("fastmcp", {}),
|
|
211
|
+
"version": version,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if task:
|
|
215
|
+
return await self._get_prompt_as_task(
|
|
216
|
+
name, arguments, task_id, ttl, meta=request_meta or None
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
result = await self.get_prompt_mcp(
|
|
220
|
+
name=name, arguments=arguments, meta=request_meta or None
|
|
221
|
+
)
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
async def _get_prompt_as_task(
|
|
225
|
+
self: Client,
|
|
226
|
+
name: str,
|
|
227
|
+
arguments: dict[str, Any] | None = None,
|
|
228
|
+
task_id: str | None = None,
|
|
229
|
+
ttl: int = 60000,
|
|
230
|
+
meta: dict[str, Any] | None = None,
|
|
231
|
+
) -> PromptTask:
|
|
232
|
+
"""Get a prompt for background execution (SEP-1686).
|
|
233
|
+
|
|
234
|
+
Returns a PromptTask object that handles both background and immediate execution.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
name: Prompt name to get
|
|
238
|
+
arguments: Prompt arguments
|
|
239
|
+
task_id: Optional client-provided task ID (ignored, for backward compatibility)
|
|
240
|
+
ttl: Time to keep results available in milliseconds (default 60s)
|
|
241
|
+
meta: Optional request metadata (e.g., version info)
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
PromptTask: Future-like object for accessing task status and results
|
|
245
|
+
"""
|
|
246
|
+
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
|
|
247
|
+
# Inject trace context into meta for propagation to server
|
|
248
|
+
propagated_meta = inject_trace_context(meta)
|
|
249
|
+
|
|
250
|
+
# Serialize arguments for MCP protocol
|
|
251
|
+
serialized_arguments: dict[str, str] | None = None
|
|
252
|
+
if arguments:
|
|
253
|
+
serialized_arguments = {}
|
|
254
|
+
for key, value in arguments.items():
|
|
255
|
+
if isinstance(value, str):
|
|
256
|
+
serialized_arguments[key] = value
|
|
257
|
+
else:
|
|
258
|
+
serialized_arguments[key] = pydantic_core.to_json(value).decode(
|
|
259
|
+
"utf-8"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
request = mcp.types.GetPromptRequest(
|
|
263
|
+
params=mcp.types.GetPromptRequestParams(
|
|
264
|
+
name=name,
|
|
265
|
+
arguments=serialized_arguments,
|
|
266
|
+
task=mcp.types.TaskMetadata(ttl=ttl),
|
|
267
|
+
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Server returns CreateTaskResult (task accepted) or GetPromptResult (graceful degradation)
|
|
272
|
+
wrapped_result = await self._await_with_session_monitoring(
|
|
273
|
+
self.session.send_request(
|
|
274
|
+
request=request, # type: ignore[arg-type]
|
|
275
|
+
result_type=PromptTaskResponseUnion,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
raw_result = wrapped_result.root
|
|
279
|
+
|
|
280
|
+
if isinstance(raw_result, mcp.types.CreateTaskResult):
|
|
281
|
+
# Task was accepted - extract task info from CreateTaskResult
|
|
282
|
+
server_task_id = raw_result.task.taskId
|
|
283
|
+
self._submitted_task_ids.add(server_task_id)
|
|
284
|
+
|
|
285
|
+
task_obj = PromptTask(
|
|
286
|
+
self, server_task_id, prompt_name=name, immediate_result=None
|
|
287
|
+
)
|
|
288
|
+
self._task_registry[server_task_id] = weakref.ref(task_obj)
|
|
289
|
+
return task_obj
|
|
290
|
+
else:
|
|
291
|
+
# Graceful degradation - server returned GetPromptResult
|
|
292
|
+
synthetic_task_id = task_id or str(uuid.uuid4())
|
|
293
|
+
return PromptTask(
|
|
294
|
+
self, synthetic_task_id, prompt_name=name, immediate_result=raw_result
|
|
295
|
+
)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Resource-related methods for FastMCP Client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
import weakref
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
8
|
+
|
|
9
|
+
import mcp.types
|
|
10
|
+
from pydantic import AnyUrl, RootModel
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from fastmcp.client.client import Client
|
|
14
|
+
|
|
15
|
+
from fastmcp.client.tasks import ResourceTask
|
|
16
|
+
from fastmcp.client.telemetry import client_span
|
|
17
|
+
from fastmcp.telemetry import inject_trace_context
|
|
18
|
+
from fastmcp.utilities.logging import get_logger
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
# Type alias for task response union (SEP-1686 graceful degradation)
|
|
23
|
+
ResourceTaskResponseUnion = RootModel[
|
|
24
|
+
mcp.types.CreateTaskResult | mcp.types.ReadResourceResult
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ClientResourcesMixin:
|
|
29
|
+
"""Mixin providing resource-related methods for Client."""
|
|
30
|
+
|
|
31
|
+
# --- Resources ---
|
|
32
|
+
|
|
33
|
+
async def list_resources_mcp(
|
|
34
|
+
self: Client, *, cursor: str | None = None
|
|
35
|
+
) -> mcp.types.ListResourcesResult:
|
|
36
|
+
"""Send a resources/list request and return the complete MCP protocol result.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
cursor: Optional pagination cursor from a previous request's nextCursor.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
mcp.types.ListResourcesResult: The complete response object from the protocol,
|
|
43
|
+
containing the list of resources and any additional metadata.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
RuntimeError: If called while the client is not connected.
|
|
47
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
48
|
+
"""
|
|
49
|
+
logger.debug(f"[{self.name}] called list_resources")
|
|
50
|
+
|
|
51
|
+
result = await self._await_with_session_monitoring(
|
|
52
|
+
self.session.list_resources(cursor=cursor)
|
|
53
|
+
)
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
async def list_resources(self: Client) -> list[mcp.types.Resource]:
|
|
57
|
+
"""Retrieve all resources available on the server.
|
|
58
|
+
|
|
59
|
+
This method automatically fetches all pages if the server paginates results,
|
|
60
|
+
returning the complete list. For manual pagination control (e.g., to handle
|
|
61
|
+
large result sets incrementally), use list_resources_mcp() with the cursor parameter.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
list[mcp.types.Resource]: A list of all Resource objects.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
RuntimeError: If called while the client is not connected.
|
|
68
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
69
|
+
"""
|
|
70
|
+
all_resources: list[mcp.types.Resource] = []
|
|
71
|
+
cursor: str | None = None
|
|
72
|
+
|
|
73
|
+
while True:
|
|
74
|
+
result = await self.list_resources_mcp(cursor=cursor)
|
|
75
|
+
all_resources.extend(result.resources)
|
|
76
|
+
if result.nextCursor is None:
|
|
77
|
+
break
|
|
78
|
+
cursor = result.nextCursor
|
|
79
|
+
|
|
80
|
+
return all_resources
|
|
81
|
+
|
|
82
|
+
async def list_resource_templates_mcp(
|
|
83
|
+
self: Client, *, cursor: str | None = None
|
|
84
|
+
) -> mcp.types.ListResourceTemplatesResult:
|
|
85
|
+
"""Send a resources/listResourceTemplates request and return the complete MCP protocol result.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
cursor: Optional pagination cursor from a previous request's nextCursor.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
mcp.types.ListResourceTemplatesResult: The complete response object from the protocol,
|
|
92
|
+
containing the list of resource templates and any additional metadata.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
RuntimeError: If called while the client is not connected.
|
|
96
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
97
|
+
"""
|
|
98
|
+
logger.debug(f"[{self.name}] called list_resource_templates")
|
|
99
|
+
|
|
100
|
+
result = await self._await_with_session_monitoring(
|
|
101
|
+
self.session.list_resource_templates(cursor=cursor)
|
|
102
|
+
)
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
async def list_resource_templates(self: Client) -> list[mcp.types.ResourceTemplate]:
|
|
106
|
+
"""Retrieve all resource templates available on the server.
|
|
107
|
+
|
|
108
|
+
This method automatically fetches all pages if the server paginates results,
|
|
109
|
+
returning the complete list. For manual pagination control (e.g., to handle
|
|
110
|
+
large result sets incrementally), use list_resource_templates_mcp() with the
|
|
111
|
+
cursor parameter.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
list[mcp.types.ResourceTemplate]: A list of all ResourceTemplate objects.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
RuntimeError: If called while the client is not connected.
|
|
118
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
119
|
+
"""
|
|
120
|
+
all_templates: list[mcp.types.ResourceTemplate] = []
|
|
121
|
+
cursor: str | None = None
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
result = await self.list_resource_templates_mcp(cursor=cursor)
|
|
125
|
+
all_templates.extend(result.resourceTemplates)
|
|
126
|
+
if result.nextCursor is None:
|
|
127
|
+
break
|
|
128
|
+
cursor = result.nextCursor
|
|
129
|
+
|
|
130
|
+
return all_templates
|
|
131
|
+
|
|
132
|
+
async def read_resource_mcp(
|
|
133
|
+
self: Client, uri: AnyUrl | str, meta: dict[str, Any] | None = None
|
|
134
|
+
) -> mcp.types.ReadResourceResult:
|
|
135
|
+
"""Send a resources/read request and return the complete MCP protocol result.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
|
|
139
|
+
meta (dict[str, Any] | None, optional): Request metadata (e.g., for SEP-1686 tasks). Defaults to None.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
mcp.types.ReadResourceResult: The complete response object from the protocol,
|
|
143
|
+
containing the resource contents and any additional metadata.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
RuntimeError: If called while the client is not connected.
|
|
147
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
148
|
+
"""
|
|
149
|
+
uri_str = str(uri)
|
|
150
|
+
with client_span(
|
|
151
|
+
f"resources/read {uri_str}",
|
|
152
|
+
"resources/read",
|
|
153
|
+
uri_str,
|
|
154
|
+
session_id=self.transport.get_session_id(),
|
|
155
|
+
resource_uri=uri_str,
|
|
156
|
+
):
|
|
157
|
+
logger.debug(f"[{self.name}] called read_resource: {uri}")
|
|
158
|
+
|
|
159
|
+
if isinstance(uri, str):
|
|
160
|
+
uri = AnyUrl(uri) # Ensure AnyUrl
|
|
161
|
+
|
|
162
|
+
# Inject trace context into meta for propagation to server
|
|
163
|
+
propagated_meta = inject_trace_context(meta)
|
|
164
|
+
|
|
165
|
+
# If meta provided, use send_request for SEP-1686 task support
|
|
166
|
+
if propagated_meta:
|
|
167
|
+
task_dict = propagated_meta.get("modelcontextprotocol.io/task")
|
|
168
|
+
request = mcp.types.ReadResourceRequest(
|
|
169
|
+
params=mcp.types.ReadResourceRequestParams(
|
|
170
|
+
uri=uri,
|
|
171
|
+
task=mcp.types.TaskMetadata(**task_dict) if task_dict else None,
|
|
172
|
+
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
result = await self._await_with_session_monitoring(
|
|
176
|
+
self.session.send_request(
|
|
177
|
+
request=request, # type: ignore[arg-type]
|
|
178
|
+
result_type=mcp.types.ReadResourceResult,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
result = await self._await_with_session_monitoring(
|
|
183
|
+
self.session.read_resource(uri)
|
|
184
|
+
)
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
@overload
|
|
188
|
+
async def read_resource(
|
|
189
|
+
self: Client,
|
|
190
|
+
uri: AnyUrl | str,
|
|
191
|
+
*,
|
|
192
|
+
version: str | None = None,
|
|
193
|
+
meta: dict[str, Any] | None = None,
|
|
194
|
+
task: Literal[False] = False,
|
|
195
|
+
) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]: ...
|
|
196
|
+
|
|
197
|
+
@overload
|
|
198
|
+
async def read_resource(
|
|
199
|
+
self: Client,
|
|
200
|
+
uri: AnyUrl | str,
|
|
201
|
+
*,
|
|
202
|
+
version: str | None = None,
|
|
203
|
+
meta: dict[str, Any] | None = None,
|
|
204
|
+
task: Literal[True],
|
|
205
|
+
task_id: str | None = None,
|
|
206
|
+
ttl: int = 60000,
|
|
207
|
+
) -> ResourceTask: ...
|
|
208
|
+
|
|
209
|
+
async def read_resource(
|
|
210
|
+
self: Client,
|
|
211
|
+
uri: AnyUrl | str,
|
|
212
|
+
*,
|
|
213
|
+
version: str | None = None,
|
|
214
|
+
meta: dict[str, Any] | None = None,
|
|
215
|
+
task: bool = False,
|
|
216
|
+
task_id: str | None = None,
|
|
217
|
+
ttl: int = 60000,
|
|
218
|
+
) -> (
|
|
219
|
+
list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
|
|
220
|
+
| ResourceTask
|
|
221
|
+
):
|
|
222
|
+
"""Read the contents of a resource or resolved template.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
|
|
226
|
+
version (str | None): Specific version to read. If None, reads highest version.
|
|
227
|
+
meta (dict[str, Any] | None): Optional request-level metadata.
|
|
228
|
+
task (bool): If True, execute as background task (SEP-1686). Defaults to False.
|
|
229
|
+
task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
|
|
230
|
+
ttl (int): Time to keep results available in milliseconds (default 60s).
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask:
|
|
234
|
+
A list of content objects if task=False, or a ResourceTask object if task=True.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
RuntimeError: If called while the client is not connected.
|
|
238
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
239
|
+
"""
|
|
240
|
+
# Merge version into request-level meta (not arguments)
|
|
241
|
+
request_meta = dict(meta) if meta else {}
|
|
242
|
+
if version is not None:
|
|
243
|
+
request_meta["fastmcp"] = {
|
|
244
|
+
**request_meta.get("fastmcp", {}),
|
|
245
|
+
"version": version,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if task:
|
|
249
|
+
return await self._read_resource_as_task(
|
|
250
|
+
uri, task_id, ttl, meta=request_meta or None
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if isinstance(uri, str):
|
|
254
|
+
try:
|
|
255
|
+
uri = AnyUrl(uri) # Ensure AnyUrl
|
|
256
|
+
except Exception as e:
|
|
257
|
+
raise ValueError(
|
|
258
|
+
f"Provided resource URI is invalid: {str(uri)!r}"
|
|
259
|
+
) from e
|
|
260
|
+
result = await self.read_resource_mcp(uri, meta=request_meta or None)
|
|
261
|
+
return result.contents
|
|
262
|
+
|
|
263
|
+
async def _read_resource_as_task(
|
|
264
|
+
self: Client,
|
|
265
|
+
uri: AnyUrl | str,
|
|
266
|
+
task_id: str | None = None,
|
|
267
|
+
ttl: int = 60000,
|
|
268
|
+
meta: dict[str, Any] | None = None,
|
|
269
|
+
) -> ResourceTask:
|
|
270
|
+
"""Read a resource for background execution (SEP-1686).
|
|
271
|
+
|
|
272
|
+
Returns a ResourceTask object that handles both background and immediate execution.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
uri: Resource URI to read
|
|
276
|
+
task_id: Optional client-provided task ID (ignored, for backward compatibility)
|
|
277
|
+
ttl: Time to keep results available in milliseconds (default 60s)
|
|
278
|
+
meta: Optional metadata to pass with the request (e.g., version info)
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
ResourceTask: Future-like object for accessing task status and results
|
|
282
|
+
"""
|
|
283
|
+
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
|
|
284
|
+
# Inject trace context into meta for propagation to server
|
|
285
|
+
propagated_meta = inject_trace_context(meta)
|
|
286
|
+
|
|
287
|
+
if isinstance(uri, str):
|
|
288
|
+
uri = AnyUrl(uri)
|
|
289
|
+
|
|
290
|
+
request = mcp.types.ReadResourceRequest(
|
|
291
|
+
params=mcp.types.ReadResourceRequestParams(
|
|
292
|
+
uri=uri,
|
|
293
|
+
task=mcp.types.TaskMetadata(ttl=ttl),
|
|
294
|
+
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Server returns CreateTaskResult (task accepted) or ReadResourceResult (graceful degradation)
|
|
299
|
+
wrapped_result = await self._await_with_session_monitoring(
|
|
300
|
+
self.session.send_request(
|
|
301
|
+
request=request, # type: ignore[arg-type]
|
|
302
|
+
result_type=ResourceTaskResponseUnion,
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
raw_result = wrapped_result.root
|
|
306
|
+
|
|
307
|
+
if isinstance(raw_result, mcp.types.CreateTaskResult):
|
|
308
|
+
# Task was accepted - extract task info from CreateTaskResult
|
|
309
|
+
server_task_id = raw_result.task.taskId
|
|
310
|
+
self._submitted_task_ids.add(server_task_id)
|
|
311
|
+
|
|
312
|
+
task_obj = ResourceTask(
|
|
313
|
+
self, server_task_id, uri=str(uri), immediate_result=None
|
|
314
|
+
)
|
|
315
|
+
self._task_registry[server_task_id] = weakref.ref(task_obj)
|
|
316
|
+
return task_obj
|
|
317
|
+
else:
|
|
318
|
+
# Graceful degradation - server returned ReadResourceResult
|
|
319
|
+
synthetic_task_id = task_id or str(uuid.uuid4())
|
|
320
|
+
return ResourceTask(
|
|
321
|
+
self,
|
|
322
|
+
synthetic_task_id,
|
|
323
|
+
uri=str(uri),
|
|
324
|
+
immediate_result=raw_result.contents,
|
|
325
|
+
)
|