fastmcp 2.12.5__py3-none-any.whl → 2.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. fastmcp/__init__.py +2 -23
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +19 -33
  5. fastmcp/cli/install/claude_code.py +6 -6
  6. fastmcp/cli/install/claude_desktop.py +3 -3
  7. fastmcp/cli/install/cursor.py +18 -12
  8. fastmcp/cli/install/gemini_cli.py +3 -3
  9. fastmcp/cli/install/mcp_json.py +3 -3
  10. fastmcp/cli/install/shared.py +0 -15
  11. fastmcp/cli/run.py +13 -8
  12. fastmcp/cli/tasks.py +110 -0
  13. fastmcp/client/__init__.py +9 -9
  14. fastmcp/client/auth/oauth.py +123 -225
  15. fastmcp/client/client.py +697 -95
  16. fastmcp/client/elicitation.py +11 -5
  17. fastmcp/client/logging.py +18 -14
  18. fastmcp/client/messages.py +7 -5
  19. fastmcp/client/oauth_callback.py +85 -171
  20. fastmcp/client/roots.py +2 -1
  21. fastmcp/client/sampling.py +1 -1
  22. fastmcp/client/tasks.py +614 -0
  23. fastmcp/client/transports.py +117 -30
  24. fastmcp/contrib/component_manager/__init__.py +1 -1
  25. fastmcp/contrib/component_manager/component_manager.py +2 -2
  26. fastmcp/contrib/component_manager/component_service.py +10 -26
  27. fastmcp/contrib/mcp_mixin/README.md +32 -1
  28. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  29. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  30. fastmcp/dependencies.py +25 -0
  31. fastmcp/experimental/sampling/handlers/openai.py +3 -3
  32. fastmcp/experimental/server/openapi/__init__.py +20 -21
  33. fastmcp/experimental/utilities/openapi/__init__.py +16 -47
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +54 -51
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +43 -21
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +161 -61
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -14
  45. fastmcp/server/auth/auth.py +197 -46
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1469 -298
  50. fastmcp/server/auth/oidc_proxy.py +91 -20
  51. fastmcp/server/auth/providers/auth0.py +40 -21
  52. fastmcp/server/auth/providers/aws.py +29 -3
  53. fastmcp/server/auth/providers/azure.py +312 -131
  54. fastmcp/server/auth/providers/debug.py +114 -0
  55. fastmcp/server/auth/providers/descope.py +86 -29
  56. fastmcp/server/auth/providers/discord.py +308 -0
  57. fastmcp/server/auth/providers/github.py +29 -8
  58. fastmcp/server/auth/providers/google.py +48 -9
  59. fastmcp/server/auth/providers/in_memory.py +29 -5
  60. fastmcp/server/auth/providers/introspection.py +281 -0
  61. fastmcp/server/auth/providers/jwt.py +48 -31
  62. fastmcp/server/auth/providers/oci.py +233 -0
  63. fastmcp/server/auth/providers/scalekit.py +238 -0
  64. fastmcp/server/auth/providers/supabase.py +188 -0
  65. fastmcp/server/auth/providers/workos.py +35 -17
  66. fastmcp/server/context.py +236 -116
  67. fastmcp/server/dependencies.py +503 -18
  68. fastmcp/server/elicitation.py +286 -48
  69. fastmcp/server/event_store.py +177 -0
  70. fastmcp/server/http.py +71 -20
  71. fastmcp/server/low_level.py +165 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +50 -39
  76. fastmcp/server/middleware/middleware.py +29 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi/__init__.py +35 -0
  80. fastmcp/{experimental/server → server}/openapi/components.py +15 -10
  81. fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
  82. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  83. fastmcp/server/proxy.py +72 -48
  84. fastmcp/server/server.py +1415 -733
  85. fastmcp/server/tasks/__init__.py +21 -0
  86. fastmcp/server/tasks/capabilities.py +22 -0
  87. fastmcp/server/tasks/config.py +89 -0
  88. fastmcp/server/tasks/converters.py +205 -0
  89. fastmcp/server/tasks/handlers.py +356 -0
  90. fastmcp/server/tasks/keys.py +93 -0
  91. fastmcp/server/tasks/protocol.py +355 -0
  92. fastmcp/server/tasks/subscriptions.py +205 -0
  93. fastmcp/settings.py +125 -113
  94. fastmcp/tools/__init__.py +1 -1
  95. fastmcp/tools/tool.py +138 -55
  96. fastmcp/tools/tool_manager.py +30 -112
  97. fastmcp/tools/tool_transform.py +12 -21
  98. fastmcp/utilities/cli.py +67 -28
  99. fastmcp/utilities/components.py +10 -5
  100. fastmcp/utilities/inspect.py +79 -23
  101. fastmcp/utilities/json_schema.py +4 -4
  102. fastmcp/utilities/json_schema_type.py +8 -8
  103. fastmcp/utilities/logging.py +118 -8
  104. fastmcp/utilities/mcp_config.py +1 -2
  105. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  106. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  107. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  108. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
  109. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  110. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  111. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  112. fastmcp/utilities/openapi/__init__.py +63 -0
  113. fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
  114. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  115. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
  116. fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
  117. fastmcp/utilities/tests.py +92 -5
  118. fastmcp/utilities/types.py +86 -16
  119. fastmcp/utilities/ui.py +626 -0
  120. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
  121. fastmcp-2.14.0.dist-info/RECORD +156 -0
  122. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
  123. fastmcp/cli/claude.py +0 -135
  124. fastmcp/server/auth/providers/bearer.py +0 -25
  125. fastmcp/server/openapi.py +0 -1083
  126. fastmcp/utilities/openapi.py +0 -1568
  127. fastmcp/utilities/storage.py +0 -204
  128. fastmcp-2.12.5.dist-info/RECORD +0 -134
  129. fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  130. fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
  131. fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
  132. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  133. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/client.py CHANGED
@@ -4,6 +4,8 @@ import asyncio
4
4
  import copy
5
5
  import datetime
6
6
  import secrets
7
+ import uuid
8
+ import weakref
7
9
  from contextlib import AsyncExitStack, asynccontextmanager
8
10
  from dataclasses import dataclass, field
9
11
  from pathlib import Path
@@ -14,7 +16,20 @@ import httpx
14
16
  import mcp.types
15
17
  import pydantic_core
16
18
  from exceptiongroup import catch
17
- from mcp import ClientSession
19
+ from mcp import ClientSession, McpError
20
+ from mcp.types import (
21
+ CancelTaskRequest,
22
+ CancelTaskRequestParams,
23
+ GetTaskPayloadRequest,
24
+ GetTaskPayloadRequestParams,
25
+ GetTaskPayloadResult,
26
+ GetTaskRequest,
27
+ GetTaskRequestParams,
28
+ GetTaskResult,
29
+ ListTasksRequest,
30
+ PaginatedRequestParams,
31
+ TaskStatusNotification,
32
+ )
18
33
  from pydantic import AnyUrl
19
34
 
20
35
  import fastmcp
@@ -36,6 +51,13 @@ from fastmcp.client.sampling import (
36
51
  SamplingHandler,
37
52
  create_sampling_callback,
38
53
  )
54
+ from fastmcp.client.tasks import (
55
+ PromptTask,
56
+ ResourceTask,
57
+ TaskNotificationHandler,
58
+ ToolTask,
59
+ _task_capable_initialize,
60
+ )
39
61
  from fastmcp.exceptions import ToolError
40
62
  from fastmcp.mcp_config import MCPConfig
41
63
  from fastmcp.server import FastMCP
@@ -61,15 +83,15 @@ from .transports import (
61
83
 
62
84
  __all__ = [
63
85
  "Client",
64
- "SessionKwargs",
65
- "RootsHandler",
66
- "RootsList",
67
- "LogHandler",
68
- "MessageHandler",
69
86
  "ClientSamplingHandler",
70
- "SamplingHandler",
71
87
  "ElicitationHandler",
88
+ "LogHandler",
89
+ "MessageHandler",
72
90
  "ProgressHandler",
91
+ "RootsHandler",
92
+ "RootsList",
93
+ "SamplingHandler",
94
+ "SessionKwargs",
73
95
  ]
74
96
 
75
97
  logger = get_logger(__name__)
@@ -94,6 +116,17 @@ class ClientSessionState:
94
116
  initialize_result: mcp.types.InitializeResult | None = None
95
117
 
96
118
 
119
+ @dataclass
120
+ class CallToolResult:
121
+ """Parsed result from a tool call."""
122
+
123
+ content: list[mcp.types.ContentBlock]
124
+ structured_content: dict[str, Any] | None
125
+ meta: dict[str, Any] | None
126
+ data: Any = None
127
+ is_error: bool = False
128
+
129
+
97
130
  class Client(Generic[ClientTransportT]):
98
131
  """
99
132
  MCP client that delegates connection management to a Transport instance.
@@ -155,38 +188,38 @@ class Client(Generic[ClientTransportT]):
155
188
  """
156
189
 
157
190
  @overload
158
- def __init__(self: Client[T], transport: T, *args, **kwargs) -> None: ...
191
+ def __init__(self: Client[T], transport: T, *args: Any, **kwargs: Any) -> None: ...
159
192
 
160
193
  @overload
161
194
  def __init__(
162
195
  self: Client[SSETransport | StreamableHttpTransport],
163
196
  transport: AnyUrl,
164
- *args,
165
- **kwargs,
197
+ *args: Any,
198
+ **kwargs: Any,
166
199
  ) -> None: ...
167
200
 
168
201
  @overload
169
202
  def __init__(
170
203
  self: Client[FastMCPTransport],
171
204
  transport: FastMCP | FastMCP1Server,
172
- *args,
173
- **kwargs,
205
+ *args: Any,
206
+ **kwargs: Any,
174
207
  ) -> None: ...
175
208
 
176
209
  @overload
177
210
  def __init__(
178
211
  self: Client[PythonStdioTransport | NodeStdioTransport],
179
212
  transport: Path,
180
- *args,
181
- **kwargs,
213
+ *args: Any,
214
+ **kwargs: Any,
182
215
  ) -> None: ...
183
216
 
184
217
  @overload
185
218
  def __init__(
186
219
  self: Client[MCPConfigTransport],
187
220
  transport: MCPConfig | dict[str, Any],
188
- *args,
189
- **kwargs,
221
+ *args: Any,
222
+ **kwargs: Any,
190
223
  ) -> None: ...
191
224
 
192
225
  @overload
@@ -198,8 +231,8 @@ class Client(Generic[ClientTransportT]):
198
231
  | StreamableHttpTransport
199
232
  ],
200
233
  transport: str,
201
- *args,
202
- **kwargs,
234
+ *args: Any,
235
+ **kwargs: Any,
203
236
  ) -> None: ...
204
237
 
205
238
  def __init__(
@@ -222,6 +255,7 @@ class Client(Generic[ClientTransportT]):
222
255
  message_handler: MessageHandlerT | MessageHandler | None = None,
223
256
  progress_handler: ProgressHandler | None = None,
224
257
  timeout: datetime.timedelta | float | int | None = None,
258
+ auto_initialize: bool = True,
225
259
  init_timeout: datetime.timedelta | float | int | None = None,
226
260
  client_info: mcp.types.Implementation | None = None,
227
261
  auth: httpx.Auth | Literal["oauth"] | str | None = None,
@@ -240,6 +274,7 @@ class Client(Generic[ClientTransportT]):
240
274
 
241
275
  self._progress_handler = progress_handler
242
276
 
277
+ # Convert timeout to timedelta if needed
243
278
  if isinstance(timeout, int | float):
244
279
  timeout = datetime.timedelta(seconds=float(timeout))
245
280
 
@@ -254,12 +289,14 @@ class Client(Generic[ClientTransportT]):
254
289
  init_timeout = float(init_timeout)
255
290
  self._init_timeout = init_timeout
256
291
 
292
+ self.auto_initialize = auto_initialize
293
+
257
294
  self._session_kwargs: SessionKwargs = {
258
295
  "sampling_callback": None,
259
296
  "list_roots_callback": None,
260
297
  "logging_callback": create_log_callback(log_handler),
261
- "message_handler": message_handler,
262
- "read_timeout_seconds": timeout,
298
+ "message_handler": message_handler or TaskNotificationHandler(self),
299
+ "read_timeout_seconds": timeout, # ty: ignore[invalid-argument-type]
263
300
  "client_info": client_info,
264
301
  }
265
302
 
@@ -279,6 +316,15 @@ class Client(Generic[ClientTransportT]):
279
316
  # Session context management - see class docstring for detailed explanation
280
317
  self._session_state = ClientSessionState()
281
318
 
319
+ # Track task IDs submitted by this client (for list_tasks support)
320
+ self._submitted_task_ids: set[str] = set()
321
+
322
+ # Registry for routing notifications/tasks/status to Task objects
323
+
324
+ self._task_registry: dict[
325
+ str, weakref.ref[ToolTask | PromptTask | ResourceTask]
326
+ ] = {}
327
+
282
328
  @property
283
329
  def session(self) -> ClientSession:
284
330
  """Get the current active session. Raises RuntimeError if not connected."""
@@ -290,12 +336,8 @@ class Client(Generic[ClientTransportT]):
290
336
  return self._session_state.session
291
337
 
292
338
  @property
293
- def initialize_result(self) -> mcp.types.InitializeResult:
339
+ def initialize_result(self) -> mcp.types.InitializeResult | None:
294
340
  """Get the result of the initialization request."""
295
- if self._session_state.initialize_result is None:
296
- raise RuntimeError(
297
- "Client is not connected. Use the 'async with client:' context manager first."
298
- )
299
341
  return self._session_state.initialize_result
300
342
 
301
343
  def set_roots(self, roots: RootsList | RootsHandler) -> None:
@@ -355,26 +397,84 @@ class Client(Generic[ClientTransportT]):
355
397
  **self._session_kwargs
356
398
  ) as session:
357
399
  self._session_state.session = session
358
- # Initialize the session
400
+ # Initialize the session if auto_initialize is enabled
359
401
  try:
360
- with anyio.fail_after(self._init_timeout):
361
- self._session_state.initialize_result = (
362
- await self._session_state.session.initialize()
363
- )
402
+ if self.auto_initialize:
403
+ await self.initialize()
364
404
  yield
365
- except anyio.ClosedResourceError:
366
- raise RuntimeError("Server session was closed unexpectedly")
367
- except TimeoutError:
368
- raise RuntimeError("Failed to initialize server session")
405
+ except anyio.ClosedResourceError as e:
406
+ raise RuntimeError("Server session was closed unexpectedly") from e
369
407
  finally:
370
408
  self._session_state.session = None
371
409
  self._session_state.initialize_result = None
372
410
 
411
+ async def initialize(
412
+ self,
413
+ timeout: datetime.timedelta | float | int | None = None,
414
+ ) -> mcp.types.InitializeResult:
415
+ """Send an initialize request to the server.
416
+
417
+ This method performs the MCP initialization handshake with the server,
418
+ exchanging capabilities and server information. It is idempotent - calling
419
+ it multiple times returns the cached result from the first call.
420
+
421
+ The initialization happens automatically when entering the client context
422
+ manager unless `auto_initialize=False` was set during client construction.
423
+ Manual calls to this method are only needed when auto-initialization is disabled.
424
+
425
+ Args:
426
+ timeout: Optional timeout for the initialization request (seconds or timedelta).
427
+ If None, uses the client's init_timeout setting.
428
+
429
+ Returns:
430
+ InitializeResult: The server's initialization response containing server info,
431
+ capabilities, protocol version, and optional instructions.
432
+
433
+ Raises:
434
+ RuntimeError: If the client is not connected or initialization times out.
435
+
436
+ Example:
437
+ ```python
438
+ # With auto-initialization disabled
439
+ client = Client(server, auto_initialize=False)
440
+ async with client:
441
+ result = await client.initialize()
442
+ print(f"Server: {result.serverInfo.name}")
443
+ print(f"Instructions: {result.instructions}")
444
+ ```
445
+ """
446
+
447
+ if self.initialize_result is not None:
448
+ return self.initialize_result
449
+
450
+ if timeout is None:
451
+ timeout = self._init_timeout
452
+
453
+ # Convert timeout if needed
454
+ if isinstance(timeout, datetime.timedelta):
455
+ timeout = timeout.total_seconds()
456
+ elif timeout is not None:
457
+ timeout = float(timeout)
458
+
459
+ try:
460
+ with anyio.fail_after(timeout):
461
+ self._session_state.initialize_result = await _task_capable_initialize(
462
+ self.session
463
+ )
464
+
465
+ return self._session_state.initialize_result
466
+ except TimeoutError as e:
467
+ raise RuntimeError("Failed to initialize server session") from e
468
+
373
469
  async def __aenter__(self):
374
470
  return await self._connect()
375
471
 
376
472
  async def __aexit__(self, exc_type, exc_val, exc_tb):
377
- await self._disconnect()
473
+ # Use a timeout to prevent hanging during cleanup if the connection is in a bad
474
+ # state (e.g., rate-limited). The MCP SDK's transport may try to terminate the
475
+ # session which can hang if the server is unresponsive.
476
+ with anyio.move_on_after(5):
477
+ await self._disconnect()
378
478
 
379
479
  async def _connect(self):
380
480
  """
@@ -414,7 +514,8 @@ class Client(Generic[ClientTransportT]):
414
514
  raise RuntimeError(
415
515
  "Session task completed without exception but connection failed"
416
516
  )
417
- if isinstance(exception, httpx.HTTPStatusError):
517
+ # Preserve specific exception types that clients may want to handle
518
+ if isinstance(exception, httpx.HTTPStatusError | McpError):
418
519
  raise exception
419
520
  raise RuntimeError(
420
521
  f"Client failed to connect: {exception}"
@@ -487,6 +588,28 @@ class Client(Generic[ClientTransportT]):
487
588
  # Ensure ready event is set even if context manager entry fails
488
589
  self._session_state.ready_event.set()
489
590
 
591
+ def _handle_task_status_notification(
592
+ self, notification: TaskStatusNotification
593
+ ) -> None:
594
+ """Route task status notification to appropriate Task object.
595
+
596
+ Called when notifications/tasks/status is received from server.
597
+ Updates Task object's cache and triggers events/callbacks.
598
+ """
599
+ # Extract task ID from notification params
600
+ task_id = notification.params.taskId
601
+ if not task_id:
602
+ return
603
+
604
+ # Look up task in registry (weakref)
605
+ task_ref = self._task_registry.get(task_id)
606
+ if task_ref:
607
+ task = task_ref() # Dereference weakref
608
+ if task:
609
+ # Convert notification params to GetTaskResult (they share the same fields via Task)
610
+ status = GetTaskResult.model_validate(notification.params.model_dump())
611
+ task._handle_status_notification(status)
612
+
490
613
  async def close(self):
491
614
  await self._disconnect(force=True)
492
615
  await self.transport.close()
@@ -596,12 +719,13 @@ class Client(Generic[ClientTransportT]):
596
719
  return result.resourceTemplates
597
720
 
598
721
  async def read_resource_mcp(
599
- self, uri: AnyUrl | str
722
+ self, uri: AnyUrl | str, meta: dict[str, Any] | None = None
600
723
  ) -> mcp.types.ReadResourceResult:
601
724
  """Send a resources/read request and return the complete MCP protocol result.
602
725
 
603
726
  Args:
604
727
  uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
728
+ meta (dict[str, Any] | None, optional): Request metadata (e.g., for SEP-1686 tasks). Defaults to None.
605
729
 
606
730
  Returns:
607
731
  mcp.types.ReadResourceResult: The complete response object from the protocol,
@@ -614,24 +738,73 @@ class Client(Generic[ClientTransportT]):
614
738
 
615
739
  if isinstance(uri, str):
616
740
  uri = AnyUrl(uri) # Ensure AnyUrl
617
- result = await self.session.read_resource(uri)
741
+
742
+ # If meta provided, use send_request for SEP-1686 task support
743
+ if meta:
744
+ task_dict = meta.get("modelcontextprotocol.io/task")
745
+ request = mcp.types.ReadResourceRequest(
746
+ params=mcp.types.ReadResourceRequestParams(
747
+ uri=uri,
748
+ task=mcp.types.TaskMetadata(**task_dict)
749
+ if task_dict
750
+ else None, # SEP-1686: task as direct param (spec-compliant)
751
+ )
752
+ )
753
+ result = await self.session.send_request(
754
+ request=request, # type: ignore[arg-type]
755
+ result_type=mcp.types.ReadResourceResult,
756
+ )
757
+ else:
758
+ result = await self.session.read_resource(uri)
618
759
  return result
619
760
 
761
+ @overload
620
762
  async def read_resource(
621
- self, uri: AnyUrl | str
622
- ) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]:
763
+ self,
764
+ uri: AnyUrl | str,
765
+ *,
766
+ task: Literal[False] = False,
767
+ ) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]: ...
768
+
769
+ @overload
770
+ async def read_resource(
771
+ self,
772
+ uri: AnyUrl | str,
773
+ *,
774
+ task: Literal[True],
775
+ task_id: str | None = None,
776
+ ttl: int = 60000,
777
+ ) -> ResourceTask: ...
778
+
779
+ async def read_resource(
780
+ self,
781
+ uri: AnyUrl | str,
782
+ *,
783
+ task: bool = False,
784
+ task_id: str | None = None,
785
+ ttl: int = 60000,
786
+ ) -> (
787
+ list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
788
+ | ResourceTask
789
+ ):
623
790
  """Read the contents of a resource or resolved template.
624
791
 
625
792
  Args:
626
793
  uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
794
+ task (bool): If True, execute as background task (SEP-1686). Defaults to False.
795
+ task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
796
+ ttl (int): Time to keep results available in milliseconds (default 60s).
627
797
 
628
798
  Returns:
629
- list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]: A list of content
630
- objects, typically containing either text or binary data.
799
+ list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask:
800
+ A list of content objects if task=False, or a ResourceTask object if task=True.
631
801
 
632
802
  Raises:
633
803
  RuntimeError: If called while the client is not connected.
634
804
  """
805
+ if task:
806
+ return await self._read_resource_as_task(uri, task_id, ttl)
807
+
635
808
  if isinstance(uri, str):
636
809
  try:
637
810
  uri = AnyUrl(uri) # Ensure AnyUrl
@@ -642,6 +815,62 @@ class Client(Generic[ClientTransportT]):
642
815
  result = await self.read_resource_mcp(uri)
643
816
  return result.contents
644
817
 
818
+ async def _read_resource_as_task(
819
+ self,
820
+ uri: AnyUrl | str,
821
+ task_id: str | None = None,
822
+ ttl: int = 60000,
823
+ ) -> ResourceTask:
824
+ """Read a resource for background execution (SEP-1686).
825
+
826
+ Returns a ResourceTask object that handles both background and immediate execution.
827
+
828
+ Args:
829
+ uri: Resource URI to read
830
+ task_id: Optional client-provided task ID (ignored, for backward compatibility)
831
+ ttl: Time to keep results available in milliseconds (default 60s)
832
+
833
+ Returns:
834
+ ResourceTask: Future-like object for accessing task status and results
835
+ """
836
+ # Per SEP-1686 final spec: client sends only ttl, server generates taskId
837
+ # Read resource with task metadata (no taskId sent)
838
+ result = await self.read_resource_mcp(
839
+ uri=uri,
840
+ meta={
841
+ "modelcontextprotocol.io/task": {
842
+ "ttl": ttl,
843
+ }
844
+ },
845
+ )
846
+
847
+ # Check if server accepted background execution
848
+ # If response includes task metadata with taskId, server accepted background mode
849
+ # If response includes returned_immediately=True, server declined and executed sync
850
+ task_meta = (result.meta or {}).get("modelcontextprotocol.io/task", {})
851
+ if task_meta.get("taskId"):
852
+ # Background execution accepted - extract server-generated taskId
853
+ server_task_id = task_meta["taskId"]
854
+ # Track this task ID for list_tasks()
855
+ self._submitted_task_ids.add(server_task_id)
856
+
857
+ # Create task object
858
+ task_obj = ResourceTask(
859
+ self, server_task_id, uri=str(uri), immediate_result=None
860
+ )
861
+
862
+ # Register for notification routing
863
+ self._task_registry[server_task_id] = weakref.ref(task_obj) # type: ignore[assignment]
864
+
865
+ return task_obj
866
+ else:
867
+ # Server declined background execution (graceful degradation)
868
+ # Use a synthetic task ID for the immediate result
869
+ synthetic_task_id = task_id or str(uuid.uuid4())
870
+ return ResourceTask(
871
+ self, synthetic_task_id, uri=str(uri), immediate_result=result.contents
872
+ )
873
+
645
874
  # async def subscribe_resource(self, uri: AnyUrl | str) -> None:
646
875
  # """Send a resources/subscribe request."""
647
876
  # if isinstance(uri, str):
@@ -685,13 +914,17 @@ class Client(Generic[ClientTransportT]):
685
914
 
686
915
  # --- Prompt ---
687
916
  async def get_prompt_mcp(
688
- self, name: str, arguments: dict[str, Any] | None = None
917
+ self,
918
+ name: str,
919
+ arguments: dict[str, Any] | None = None,
920
+ meta: dict[str, Any] | None = None,
689
921
  ) -> mcp.types.GetPromptResult:
690
922
  """Send a prompts/get request and return the complete MCP protocol result.
691
923
 
692
924
  Args:
693
925
  name (str): The name of the prompt to retrieve.
694
926
  arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
927
+ meta (dict[str, Any] | None, optional): Request metadata (e.g., for SEP-1686 tasks). Defaults to None.
695
928
 
696
929
  Returns:
697
930
  mcp.types.GetPromptResult: The complete response object from the protocol,
@@ -715,30 +948,138 @@ class Client(Generic[ClientTransportT]):
715
948
  "utf-8"
716
949
  )
717
950
 
718
- result = await self.session.get_prompt(
719
- name=name, arguments=serialized_arguments
720
- )
951
+ # If meta provided, use send_request for SEP-1686 task support
952
+ if meta:
953
+ task_dict = meta.get("modelcontextprotocol.io/task")
954
+ request = mcp.types.GetPromptRequest(
955
+ params=mcp.types.GetPromptRequestParams(
956
+ name=name,
957
+ arguments=serialized_arguments,
958
+ task=mcp.types.TaskMetadata(**task_dict)
959
+ if task_dict
960
+ else None, # SEP-1686: task as direct param (spec-compliant)
961
+ )
962
+ )
963
+ result = await self.session.send_request(
964
+ request=request, # type: ignore[arg-type]
965
+ result_type=mcp.types.GetPromptResult,
966
+ )
967
+ else:
968
+ result = await self.session.get_prompt(
969
+ name=name, arguments=serialized_arguments
970
+ )
721
971
  return result
722
972
 
973
+ @overload
723
974
  async def get_prompt(
724
- self, name: str, arguments: dict[str, Any] | None = None
725
- ) -> mcp.types.GetPromptResult:
975
+ self,
976
+ name: str,
977
+ arguments: dict[str, Any] | None = None,
978
+ *,
979
+ task: Literal[False] = False,
980
+ ) -> mcp.types.GetPromptResult: ...
981
+
982
+ @overload
983
+ async def get_prompt(
984
+ self,
985
+ name: str,
986
+ arguments: dict[str, Any] | None = None,
987
+ *,
988
+ task: Literal[True],
989
+ task_id: str | None = None,
990
+ ttl: int = 60000,
991
+ ) -> PromptTask: ...
992
+
993
+ async def get_prompt(
994
+ self,
995
+ name: str,
996
+ arguments: dict[str, Any] | None = None,
997
+ *,
998
+ task: bool = False,
999
+ task_id: str | None = None,
1000
+ ttl: int = 60000,
1001
+ ) -> mcp.types.GetPromptResult | PromptTask:
726
1002
  """Retrieve a rendered prompt message list from the server.
727
1003
 
728
1004
  Args:
729
1005
  name (str): The name of the prompt to retrieve.
730
1006
  arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
1007
+ task (bool): If True, execute as background task (SEP-1686). Defaults to False.
1008
+ task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
1009
+ ttl (int): Time to keep results available in milliseconds (default 60s).
731
1010
 
732
1011
  Returns:
733
- mcp.types.GetPromptResult: The complete response object from the protocol,
734
- containing the prompt messages and any additional metadata.
1012
+ mcp.types.GetPromptResult | PromptTask: The complete response object if task=False,
1013
+ or a PromptTask object if task=True.
735
1014
 
736
1015
  Raises:
737
1016
  RuntimeError: If called while the client is not connected.
738
1017
  """
1018
+ if task:
1019
+ return await self._get_prompt_as_task(name, arguments, task_id, ttl)
1020
+
739
1021
  result = await self.get_prompt_mcp(name=name, arguments=arguments)
740
1022
  return result
741
1023
 
1024
+ async def _get_prompt_as_task(
1025
+ self,
1026
+ name: str,
1027
+ arguments: dict[str, Any] | None = None,
1028
+ task_id: str | None = None,
1029
+ ttl: int = 60000,
1030
+ ) -> PromptTask:
1031
+ """Get a prompt for background execution (SEP-1686).
1032
+
1033
+ Returns a PromptTask object that handles both background and immediate execution.
1034
+
1035
+ Args:
1036
+ name: Prompt name to get
1037
+ arguments: Prompt arguments
1038
+ task_id: Optional client-provided task ID (ignored, for backward compatibility)
1039
+ ttl: Time to keep results available in milliseconds (default 60s)
1040
+
1041
+ Returns:
1042
+ PromptTask: Future-like object for accessing task status and results
1043
+ """
1044
+ # Per SEP-1686 final spec: client sends only ttl, server generates taskId
1045
+ # Call prompt with task metadata (no taskId sent)
1046
+ result = await self.get_prompt_mcp(
1047
+ name=name,
1048
+ arguments=arguments or {},
1049
+ meta={
1050
+ "modelcontextprotocol.io/task": {
1051
+ "ttl": ttl,
1052
+ }
1053
+ },
1054
+ )
1055
+
1056
+ # Check if server accepted background execution
1057
+ # If response includes task metadata with taskId, server accepted background mode
1058
+ # If response includes returned_immediately=True, server declined and executed sync
1059
+ task_meta = (result.meta or {}).get("modelcontextprotocol.io/task", {})
1060
+ if task_meta.get("taskId"):
1061
+ # Background execution accepted - extract server-generated taskId
1062
+ server_task_id = task_meta["taskId"]
1063
+ # Track this task ID for list_tasks()
1064
+ self._submitted_task_ids.add(server_task_id)
1065
+
1066
+ # Create task object
1067
+ task_obj = PromptTask(
1068
+ self, server_task_id, prompt_name=name, immediate_result=None
1069
+ )
1070
+
1071
+ # Register for notification routing
1072
+ self._task_registry[server_task_id] = weakref.ref(task_obj) # type: ignore[assignment]
1073
+
1074
+ return task_obj
1075
+ else:
1076
+ # Server declined background execution (graceful degradation)
1077
+ # Use a synthetic task ID for the immediate result
1078
+ synthetic_task_id = task_id or str(uuid.uuid4())
1079
+ return PromptTask(
1080
+ self, synthetic_task_id, prompt_name=name, immediate_result=result
1081
+ )
1082
+
742
1083
  # --- Completion ---
743
1084
 
744
1085
  async def complete_mcp(
@@ -831,6 +1172,7 @@ class Client(Generic[ClientTransportT]):
831
1172
  arguments: dict[str, Any],
832
1173
  progress_handler: ProgressHandler | None = None,
833
1174
  timeout: datetime.timedelta | float | int | None = None,
1175
+ meta: dict[str, Any] | None = None,
834
1176
  ) -> mcp.types.CallToolResult:
835
1177
  """Send a tools/call request and return the complete MCP protocol result.
836
1178
 
@@ -842,6 +1184,10 @@ class Client(Generic[ClientTransportT]):
842
1184
  arguments (dict[str, Any]): Arguments to pass to the tool.
843
1185
  timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
844
1186
  progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
1187
+ meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
1188
+ This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
1189
+ that shouldn't be tool arguments but may influence server-side processing. The server
1190
+ can access this via `context.request_context.meta`. Defaults to None.
845
1191
 
846
1192
  Returns:
847
1193
  mcp.types.CallToolResult: The complete response object from the protocol,
@@ -852,53 +1198,52 @@ class Client(Generic[ClientTransportT]):
852
1198
  """
853
1199
  logger.debug(f"[{self.name}] called call_tool: {name}")
854
1200
 
1201
+ # Convert timeout to timedelta if needed
855
1202
  if isinstance(timeout, int | float):
856
1203
  timeout = datetime.timedelta(seconds=float(timeout))
857
- result = await self.session.call_tool(
858
- name=name,
859
- arguments=arguments,
860
- read_timeout_seconds=timeout,
861
- progress_callback=progress_handler or self._progress_handler,
862
- )
1204
+
1205
+ # For task submissions, use send_request to bypass SDK validation
1206
+ # Task acknowledgments don't have structured content, which would fail validation
1207
+ if meta and "modelcontextprotocol.io/task" in meta:
1208
+ task_dict = meta.get("modelcontextprotocol.io/task")
1209
+ request = mcp.types.CallToolRequest(
1210
+ params=mcp.types.CallToolRequestParams(
1211
+ name=name,
1212
+ arguments=arguments,
1213
+ task=mcp.types.TaskMetadata(**task_dict)
1214
+ if task_dict
1215
+ else None, # SEP-1686: task as direct param (spec-compliant)
1216
+ )
1217
+ )
1218
+ result = await self.session.send_request(
1219
+ request=request, # type: ignore[arg-type]
1220
+ result_type=mcp.types.CallToolResult,
1221
+ request_read_timeout_seconds=timeout, # type: ignore[arg-type]
1222
+ progress_callback=progress_handler or self._progress_handler,
1223
+ )
1224
+ else:
1225
+ result = await self.session.call_tool(
1226
+ name=name,
1227
+ arguments=arguments,
1228
+ read_timeout_seconds=timeout, # ty: ignore[invalid-argument-type]
1229
+ progress_callback=progress_handler or self._progress_handler,
1230
+ meta=meta,
1231
+ )
863
1232
  return result
864
1233
 
865
- async def call_tool(
866
- self,
867
- name: str,
868
- arguments: dict[str, Any] | None = None,
869
- timeout: datetime.timedelta | float | int | None = None,
870
- progress_handler: ProgressHandler | None = None,
871
- raise_on_error: bool = True,
1234
+ async def _parse_call_tool_result(
1235
+ self, name: str, result: mcp.types.CallToolResult, raise_on_error: bool = False
872
1236
  ) -> CallToolResult:
873
- """Call a tool on the server.
874
-
875
- Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.
1237
+ """Parse an mcp.types.CallToolResult into our CallToolResult dataclass.
876
1238
 
877
1239
  Args:
878
- name (str): The name of the tool to call.
879
- arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
880
- timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
881
- progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
1240
+ name: Tool name (for schema lookup)
1241
+ result: Raw MCP protocol result
1242
+ raise_on_error: Whether to raise ToolError on errors
882
1243
 
883
1244
  Returns:
884
- CallToolResult:
885
- The content returned by the tool. If the tool returns structured
886
- outputs, they are returned as a dataclass (if an output schema
887
- is available) or a dictionary; otherwise, a list of content
888
- blocks is returned. Note: to receive both structured and
889
- unstructured outputs, use call_tool_mcp instead and access the
890
- raw result object.
891
-
892
- Raises:
893
- ToolError: If the tool call results in an error.
894
- RuntimeError: If called while the client is not connected.
1245
+ CallToolResult: Parsed result with structured data
895
1246
  """
896
- result = await self.call_tool_mcp(
897
- name=name,
898
- arguments=arguments or {},
899
- timeout=timeout,
900
- progress_handler=progress_handler,
901
- )
902
1247
  data = None
903
1248
  if result.isError and raise_on_error:
904
1249
  msg = cast(mcp.types.TextContent, result.content[0]).text
@@ -928,10 +1273,275 @@ class Client(Generic[ClientTransportT]):
928
1273
  return CallToolResult(
929
1274
  content=result.content,
930
1275
  structured_content=result.structuredContent,
1276
+ meta=result.meta,
931
1277
  data=data,
932
1278
  is_error=result.isError,
933
1279
  )
934
1280
 
1281
+ @overload
1282
+ async def call_tool(
1283
+ self,
1284
+ name: str,
1285
+ arguments: dict[str, Any] | None = None,
1286
+ *,
1287
+ timeout: datetime.timedelta | float | int | None = None,
1288
+ progress_handler: ProgressHandler | None = None,
1289
+ raise_on_error: bool = True,
1290
+ meta: dict[str, Any] | None = None,
1291
+ task: Literal[False] = False,
1292
+ ) -> CallToolResult: ...
1293
+
1294
+ @overload
1295
+ async def call_tool(
1296
+ self,
1297
+ name: str,
1298
+ arguments: dict[str, Any] | None = None,
1299
+ *,
1300
+ timeout: datetime.timedelta | float | int | None = None,
1301
+ progress_handler: ProgressHandler | None = None,
1302
+ raise_on_error: bool = True,
1303
+ meta: dict[str, Any] | None = None,
1304
+ task: Literal[True],
1305
+ task_id: str | None = None,
1306
+ ttl: int = 60000,
1307
+ ) -> ToolTask: ...
1308
+
1309
+ async def call_tool(
1310
+ self,
1311
+ name: str,
1312
+ arguments: dict[str, Any] | None = None,
1313
+ *,
1314
+ timeout: datetime.timedelta | float | int | None = None,
1315
+ progress_handler: ProgressHandler | None = None,
1316
+ raise_on_error: bool = True,
1317
+ meta: dict[str, Any] | None = None,
1318
+ task: bool = False,
1319
+ task_id: str | None = None,
1320
+ ttl: int = 60000,
1321
+ ) -> CallToolResult | ToolTask:
1322
+ """Call a tool on the server.
1323
+
1324
+ Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.
1325
+
1326
+ Args:
1327
+ name (str): The name of the tool to call.
1328
+ arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
1329
+ timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
1330
+ progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
1331
+ raise_on_error (bool, optional): Whether to raise an exception if the tool call results in an error. Defaults to True.
1332
+ meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
1333
+ This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
1334
+ that shouldn't be tool arguments but may influence server-side processing. The server
1335
+ can access this via `context.request_context.meta`. Defaults to None.
1336
+ task (bool): If True, execute as background task (SEP-1686). Defaults to False.
1337
+ task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
1338
+ ttl (int): Time to keep results available in milliseconds (default 60s).
1339
+
1340
+ Returns:
1341
+ CallToolResult | ToolTask: The content returned by the tool if task=False,
1342
+ or a ToolTask object if task=True. If the tool returns structured
1343
+ outputs, they are returned as a dataclass (if an output schema
1344
+ is available) or a dictionary; otherwise, a list of content
1345
+ blocks is returned. Note: to receive both structured and
1346
+ unstructured outputs, use call_tool_mcp instead and access the
1347
+ raw result object.
1348
+
1349
+ Raises:
1350
+ ToolError: If the tool call results in an error.
1351
+ RuntimeError: If called while the client is not connected.
1352
+ """
1353
+ if task:
1354
+ return await self._call_tool_as_task(name, arguments, task_id, ttl)
1355
+
1356
+ result = await self.call_tool_mcp(
1357
+ name=name,
1358
+ arguments=arguments or {},
1359
+ timeout=timeout,
1360
+ progress_handler=progress_handler,
1361
+ meta=meta,
1362
+ )
1363
+ return await self._parse_call_tool_result(
1364
+ name, result, raise_on_error=raise_on_error
1365
+ )
1366
+
1367
+ async def _call_tool_as_task(
1368
+ self,
1369
+ name: str,
1370
+ arguments: dict[str, Any] | None = None,
1371
+ task_id: str | None = None,
1372
+ ttl: int = 60000,
1373
+ ) -> ToolTask:
1374
+ """Call a tool for background execution (SEP-1686).
1375
+
1376
+ Returns a ToolTask object that handles both background and immediate execution.
1377
+ If the server accepts background execution, ToolTask will poll for results.
1378
+ If the server declines (graceful degradation), ToolTask wraps the immediate result.
1379
+
1380
+ Args:
1381
+ name: Tool name to call
1382
+ arguments: Tool arguments
1383
+ task_id: Optional client-provided task ID (ignored, for backward compatibility)
1384
+ ttl: Time to keep results available in milliseconds (default 60s)
1385
+
1386
+ Returns:
1387
+ ToolTask: Future-like object for accessing task status and results
1388
+ """
1389
+ # Per SEP-1686 final spec: client sends only ttl, server generates taskId
1390
+ # Call tool with task metadata (no taskId sent)
1391
+ result = await self.call_tool_mcp(
1392
+ name=name,
1393
+ arguments=arguments or {},
1394
+ meta={
1395
+ "modelcontextprotocol.io/task": {
1396
+ "ttl": ttl,
1397
+ }
1398
+ },
1399
+ )
1400
+
1401
+ # Check if server accepted background execution
1402
+ # If response includes task metadata with taskId, server accepted background mode
1403
+ # If response includes returned_immediately=True, server declined and executed sync
1404
+ task_meta = (result.meta or {}).get("modelcontextprotocol.io/task", {})
1405
+ if task_meta.get("taskId"):
1406
+ # Background execution accepted - extract server-generated taskId
1407
+ server_task_id = task_meta["taskId"]
1408
+ # Track this task ID for list_tasks()
1409
+ self._submitted_task_ids.add(server_task_id)
1410
+
1411
+ # Create task object
1412
+ task_obj = ToolTask(
1413
+ self, server_task_id, tool_name=name, immediate_result=None
1414
+ )
1415
+
1416
+ # Register for notification routing
1417
+ self._task_registry[server_task_id] = weakref.ref(task_obj) # type: ignore[assignment]
1418
+
1419
+ return task_obj
1420
+ else:
1421
+ # Server declined background execution (graceful degradation)
1422
+ # or returned_immediately=True - executed synchronously
1423
+ # Wrap the immediate result
1424
+ parsed_result = await self._parse_call_tool_result(name, result)
1425
+ # Use a synthetic task ID for the immediate result
1426
+ synthetic_task_id = task_id or str(uuid.uuid4())
1427
+ return ToolTask(
1428
+ self, synthetic_task_id, tool_name=name, immediate_result=parsed_result
1429
+ )
1430
+
1431
+ async def get_task_status(self, task_id: str) -> GetTaskResult:
1432
+ """Query the status of a background task.
1433
+
1434
+ Sends a 'tasks/get' MCP protocol request over the existing transport.
1435
+
1436
+ Args:
1437
+ task_id: The task ID returned from call_tool_as_task
1438
+
1439
+ Returns:
1440
+ GetTaskResult: Status information including taskId, status, pollInterval, etc.
1441
+
1442
+ Raises:
1443
+ RuntimeError: If client not connected
1444
+ """
1445
+ request = GetTaskRequest(params=GetTaskRequestParams(taskId=task_id))
1446
+ return await self.session.send_request(
1447
+ request=request, # type: ignore[arg-type]
1448
+ result_type=GetTaskResult, # type: ignore[arg-type]
1449
+ )
1450
+
1451
+ async def get_task_result(self, task_id: str) -> Any:
1452
+ """Retrieve the raw result of a completed background task.
1453
+
1454
+ Sends a 'tasks/result' MCP protocol request over the existing transport.
1455
+ Returns the raw result - callers should parse it appropriately.
1456
+
1457
+ Args:
1458
+ task_id: The task ID returned from call_tool_as_task
1459
+
1460
+ Returns:
1461
+ Any: The raw result (could be tool, prompt, or resource result)
1462
+
1463
+ Raises:
1464
+ RuntimeError: If client not connected, task not found, or task failed
1465
+ """
1466
+ request = GetTaskPayloadRequest(
1467
+ params=GetTaskPayloadRequestParams(taskId=task_id)
1468
+ )
1469
+ # Return raw result - Task classes handle type-specific parsing
1470
+ result = await self.session.send_request(
1471
+ request=request, # type: ignore[arg-type]
1472
+ result_type=GetTaskPayloadResult, # type: ignore[arg-type]
1473
+ )
1474
+ # Return as dict for compatibility with Task class parsing
1475
+ return result.model_dump(exclude_none=True, by_alias=True)
1476
+
1477
+ async def list_tasks(
1478
+ self,
1479
+ cursor: str | None = None,
1480
+ limit: int = 50,
1481
+ ) -> dict[str, Any]:
1482
+ """List background tasks.
1483
+
1484
+ Sends a 'tasks/list' MCP protocol request to the server. If the server
1485
+ returns an empty list (indicating client-side tracking), falls back to
1486
+ querying status for locally tracked task IDs.
1487
+
1488
+ Args:
1489
+ cursor: Optional pagination cursor
1490
+ limit: Maximum number of tasks to return (default 50)
1491
+
1492
+ Returns:
1493
+ dict: Response with structure:
1494
+ - tasks: List of task status dicts with taskId, status, etc.
1495
+ - nextCursor: Optional cursor for next page
1496
+
1497
+ Raises:
1498
+ RuntimeError: If client not connected
1499
+ """
1500
+ # Send protocol request
1501
+ params = PaginatedRequestParams(cursor=cursor, limit=limit)
1502
+ request = ListTasksRequest(params=params)
1503
+ server_response = await self.session.send_request(
1504
+ request=request, # type: ignore[invalid-argument-type]
1505
+ result_type=mcp.types.ListTasksResult,
1506
+ )
1507
+
1508
+ # If server returned tasks, use those
1509
+ if server_response.tasks:
1510
+ return server_response.model_dump(by_alias=True)
1511
+
1512
+ # Server returned empty - fall back to client-side tracking
1513
+ tasks = []
1514
+ for task_id in list(self._submitted_task_ids)[:limit]:
1515
+ try:
1516
+ status = await self.get_task_status(task_id)
1517
+ tasks.append(status.model_dump(by_alias=True))
1518
+ except Exception:
1519
+ # Task may have expired or been deleted, skip it
1520
+ continue
1521
+
1522
+ return {"tasks": tasks, "nextCursor": None}
1523
+
1524
+ async def cancel_task(self, task_id: str) -> mcp.types.CancelTaskResult:
1525
+ """Cancel a task, transitioning it to cancelled state.
1526
+
1527
+ Sends a 'tasks/cancel' MCP protocol request. Task will halt execution
1528
+ and transition to cancelled state.
1529
+
1530
+ Args:
1531
+ task_id: The task ID to cancel
1532
+
1533
+ Returns:
1534
+ CancelTaskResult: The task status showing cancelled state
1535
+
1536
+ Raises:
1537
+ RuntimeError: If task doesn't exist
1538
+ """
1539
+ request = CancelTaskRequest(params=CancelTaskRequestParams(taskId=task_id))
1540
+ return await self.session.send_request(
1541
+ request=request, # type: ignore[invalid-argument-type]
1542
+ result_type=mcp.types.CancelTaskResult,
1543
+ )
1544
+
935
1545
  @classmethod
936
1546
  def generate_name(cls, name: str | None = None) -> str:
937
1547
  class_name = cls.__name__
@@ -939,11 +1549,3 @@ class Client(Generic[ClientTransportT]):
939
1549
  return f"{class_name}-{secrets.token_hex(2)}"
940
1550
  else:
941
1551
  return f"{class_name}-{name}-{secrets.token_hex(2)}"
942
-
943
-
944
- @dataclass
945
- class CallToolResult:
946
- content: list[mcp.types.ContentBlock]
947
- structured_content: dict[str, Any] | None
948
- data: Any = None
949
- is_error: bool = False