fastmcp 2.14.5__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.
Files changed (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.5.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -66,7 +66,7 @@ def create_elicitation_callback(
66
66
  f"{result.content!r}"
67
67
  )
68
68
  return MCPElicitResult(
69
- _meta=result.meta,
69
+ _meta=result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
70
70
  action=result.action,
71
71
  content=content,
72
72
  )
@@ -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
+ )