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
fastmcp/client/client.py CHANGED
@@ -4,9 +4,9 @@ import asyncio
4
4
  import copy
5
5
  import datetime
6
6
  import secrets
7
- import uuid
8
7
  import weakref
9
- from contextlib import AsyncExitStack, asynccontextmanager
8
+ from collections.abc import Coroutine
9
+ from contextlib import AsyncExitStack, asynccontextmanager, suppress
10
10
  from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
12
  from typing import Any, Generic, Literal, TypeVar, cast, overload
@@ -14,22 +14,9 @@ from typing import Any, Generic, Literal, TypeVar, cast, overload
14
14
  import anyio
15
15
  import httpx
16
16
  import mcp.types
17
- import pydantic_core
18
17
  from exceptiongroup import catch
19
18
  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
- )
19
+ from mcp.types import GetTaskResult, TaskStatusNotification
33
20
  from pydantic import AnyUrl
34
21
 
35
22
  import fastmcp
@@ -40,6 +27,12 @@ from fastmcp.client.logging import (
40
27
  default_log_handler,
41
28
  )
42
29
  from fastmcp.client.messages import MessageHandler, MessageHandlerT
30
+ from fastmcp.client.mixins import (
31
+ ClientPromptsMixin,
32
+ ClientResourcesMixin,
33
+ ClientTaskManagementMixin,
34
+ ClientToolsMixin,
35
+ )
43
36
  from fastmcp.client.progress import ProgressHandler, default_progress_handler
44
37
  from fastmcp.client.roots import (
45
38
  RootsHandler,
@@ -56,13 +49,14 @@ from fastmcp.client.tasks import (
56
49
  TaskNotificationHandler,
57
50
  ToolTask,
58
51
  )
59
- from fastmcp.exceptions import ToolError
60
52
  from fastmcp.mcp_config import MCPConfig
61
53
  from fastmcp.server import FastMCP
62
54
  from fastmcp.utilities.exceptions import get_catch_handlers
63
- from fastmcp.utilities.json_schema_type import json_schema_to_type
64
55
  from fastmcp.utilities.logging import get_logger
65
- from fastmcp.utilities.types import get_cached_typeadapter
56
+ from fastmcp.utilities.timeout import (
57
+ normalize_timeout_to_seconds,
58
+ normalize_timeout_to_timedelta,
59
+ )
66
60
 
67
61
  from .transports import (
68
62
  ClientTransport,
@@ -94,6 +88,7 @@ __all__ = [
94
88
  logger = get_logger(__name__)
95
89
 
96
90
  T = TypeVar("T", bound="ClientTransport")
91
+ ResultT = TypeVar("ResultT")
97
92
 
98
93
 
99
94
  @dataclass
@@ -124,7 +119,13 @@ class CallToolResult:
124
119
  is_error: bool = False
125
120
 
126
121
 
127
- class Client(Generic[ClientTransportT]):
122
+ class Client(
123
+ Generic[ClientTransportT],
124
+ ClientResourcesMixin,
125
+ ClientPromptsMixin,
126
+ ClientToolsMixin,
127
+ ClientTaskManagementMixin,
128
+ ):
128
129
  """
129
130
  MCP client that delegates connection management to a Transport instance.
130
131
 
@@ -273,19 +274,12 @@ class Client(Generic[ClientTransportT]):
273
274
  self._progress_handler = progress_handler
274
275
 
275
276
  # Convert timeout to timedelta if needed
276
- if isinstance(timeout, int | float):
277
- timeout = datetime.timedelta(seconds=float(timeout))
277
+ timeout = normalize_timeout_to_timedelta(timeout)
278
278
 
279
- # handle init handshake timeout
279
+ # handle init handshake timeout (0 means disabled)
280
280
  if init_timeout is None:
281
281
  init_timeout = fastmcp.settings.client_init_timeout
282
- if isinstance(init_timeout, datetime.timedelta):
283
- init_timeout = init_timeout.total_seconds()
284
- elif not init_timeout:
285
- init_timeout = None
286
- else:
287
- init_timeout = float(init_timeout)
288
- self._init_timeout = init_timeout
282
+ self._init_timeout = normalize_timeout_to_seconds(init_timeout)
289
283
 
290
284
  self.auto_initialize = auto_initialize
291
285
 
@@ -294,7 +288,7 @@ class Client(Generic[ClientTransportT]):
294
288
  "list_roots_callback": None,
295
289
  "logging_callback": create_log_callback(log_handler),
296
290
  "message_handler": message_handler or TaskNotificationHandler(self),
297
- "read_timeout_seconds": timeout, # ty: ignore[invalid-argument-type]
291
+ "read_timeout_seconds": timeout,
298
292
  "client_info": client_info,
299
293
  }
300
294
 
@@ -478,12 +472,8 @@ class Client(Generic[ClientTransportT]):
478
472
 
479
473
  if timeout is None:
480
474
  timeout = self._init_timeout
481
-
482
- # Convert timeout if needed
483
- if isinstance(timeout, datetime.timedelta):
484
- timeout = timeout.total_seconds()
485
- elif timeout is not None:
486
- timeout = float(timeout)
475
+ else:
476
+ timeout = normalize_timeout_to_seconds(timeout)
487
477
 
488
478
  try:
489
479
  with anyio.fail_after(timeout):
@@ -655,6 +645,71 @@ class Client(Generic[ClientTransportT]):
655
645
  # Ensure ready event is set even if context manager entry fails
656
646
  self._session_state.ready_event.set()
657
647
 
648
+ async def _await_with_session_monitoring(
649
+ self, coro: Coroutine[Any, Any, ResultT]
650
+ ) -> ResultT:
651
+ """Await a coroutine while monitoring the session task for errors.
652
+
653
+ When using HTTP transports, server errors (4xx/5xx) are raised in the
654
+ background session task, not in the coroutine waiting for a response.
655
+ This causes the client to hang indefinitely since the response never
656
+ arrives. This method monitors the session task and propagates any
657
+ exceptions that occur, preventing the client from hanging.
658
+
659
+ Args:
660
+ coro: The coroutine to await (typically a session method call)
661
+
662
+ Returns:
663
+ The result of the coroutine
664
+
665
+ Raises:
666
+ The exception from the session task if it fails, or RuntimeError
667
+ if the session task completes unexpectedly without an exception.
668
+ """
669
+ session_task = self._session_state.session_task
670
+
671
+ # If no session task, just await directly
672
+ if session_task is None:
673
+ return await coro
674
+
675
+ # If session task already failed, raise immediately
676
+ if session_task.done():
677
+ # Close the coroutine to avoid "was never awaited" warning
678
+ coro.close()
679
+ exc = session_task.exception()
680
+ if exc:
681
+ raise exc
682
+ raise RuntimeError("Session task completed unexpectedly")
683
+
684
+ # Create task for our call
685
+ call_task = asyncio.create_task(coro)
686
+
687
+ try:
688
+ done, _ = await asyncio.wait(
689
+ {call_task, session_task},
690
+ return_when=asyncio.FIRST_COMPLETED,
691
+ )
692
+
693
+ if session_task in done:
694
+ # Session task completed (likely errored) before our call finished
695
+ call_task.cancel()
696
+ with anyio.CancelScope(shield=True), suppress(asyncio.CancelledError):
697
+ await call_task
698
+
699
+ # Raise the session task exception
700
+ exc = session_task.exception()
701
+ if exc:
702
+ raise exc
703
+ raise RuntimeError("Session task completed unexpectedly")
704
+
705
+ # Our call completed first - get the result
706
+ return call_task.result()
707
+ except asyncio.CancelledError:
708
+ call_task.cancel()
709
+ with anyio.CancelScope(shield=True), suppress(asyncio.CancelledError):
710
+ await call_task
711
+ raise
712
+
658
713
  def _handle_task_status_notification(
659
714
  self, notification: TaskStatusNotification
660
715
  ) -> None:
@@ -685,7 +740,7 @@ class Client(Generic[ClientTransportT]):
685
740
 
686
741
  async def ping(self) -> bool:
687
742
  """Send a ping request."""
688
- result = await self.session.send_ping()
743
+ result = await self._await_with_session_monitoring(self.session.send_ping())
689
744
  return isinstance(result, mcp.types.EmptyResult)
690
745
 
691
746
  async def cancel(
@@ -719,434 +774,12 @@ class Client(Generic[ClientTransportT]):
719
774
 
720
775
  async def set_logging_level(self, level: mcp.types.LoggingLevel) -> None:
721
776
  """Send a logging/setLevel request."""
722
- await self.session.set_logging_level(level)
777
+ await self._await_with_session_monitoring(self.session.set_logging_level(level))
723
778
 
724
779
  async def send_roots_list_changed(self) -> None:
725
780
  """Send a roots/list_changed notification."""
726
781
  await self.session.send_roots_list_changed()
727
782
 
728
- # --- Resources ---
729
-
730
- async def list_resources_mcp(self) -> mcp.types.ListResourcesResult:
731
- """Send a resources/list request and return the complete MCP protocol result.
732
-
733
- Returns:
734
- mcp.types.ListResourcesResult: The complete response object from the protocol,
735
- containing the list of resources and any additional metadata.
736
-
737
- Raises:
738
- RuntimeError: If called while the client is not connected.
739
- """
740
- logger.debug(f"[{self.name}] called list_resources")
741
-
742
- result = await self.session.list_resources()
743
- return result
744
-
745
- async def list_resources(self) -> list[mcp.types.Resource]:
746
- """Retrieve a list of resources available on the server.
747
-
748
- Returns:
749
- list[mcp.types.Resource]: A list of Resource objects.
750
-
751
- Raises:
752
- RuntimeError: If called while the client is not connected.
753
- """
754
- result = await self.list_resources_mcp()
755
- return result.resources
756
-
757
- async def list_resource_templates_mcp(
758
- self,
759
- ) -> mcp.types.ListResourceTemplatesResult:
760
- """Send a resources/listResourceTemplates request and return the complete MCP protocol result.
761
-
762
- Returns:
763
- mcp.types.ListResourceTemplatesResult: The complete response object from the protocol,
764
- containing the list of resource templates and any additional metadata.
765
-
766
- Raises:
767
- RuntimeError: If called while the client is not connected.
768
- """
769
- logger.debug(f"[{self.name}] called list_resource_templates")
770
-
771
- result = await self.session.list_resource_templates()
772
- return result
773
-
774
- async def list_resource_templates(
775
- self,
776
- ) -> list[mcp.types.ResourceTemplate]:
777
- """Retrieve a list of resource templates available on the server.
778
-
779
- Returns:
780
- list[mcp.types.ResourceTemplate]: A list of ResourceTemplate objects.
781
-
782
- Raises:
783
- RuntimeError: If called while the client is not connected.
784
- """
785
- result = await self.list_resource_templates_mcp()
786
- return result.resourceTemplates
787
-
788
- async def read_resource_mcp(
789
- self, uri: AnyUrl | str, meta: dict[str, Any] | None = None
790
- ) -> mcp.types.ReadResourceResult:
791
- """Send a resources/read request and return the complete MCP protocol result.
792
-
793
- Args:
794
- uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
795
- meta (dict[str, Any] | None, optional): Request metadata (e.g., for SEP-1686 tasks). Defaults to None.
796
-
797
- Returns:
798
- mcp.types.ReadResourceResult: The complete response object from the protocol,
799
- containing the resource contents and any additional metadata.
800
-
801
- Raises:
802
- RuntimeError: If called while the client is not connected.
803
- """
804
- logger.debug(f"[{self.name}] called read_resource: {uri}")
805
-
806
- if isinstance(uri, str):
807
- uri = AnyUrl(uri) # Ensure AnyUrl
808
-
809
- # If meta provided, use send_request for SEP-1686 task support
810
- if meta:
811
- task_dict = meta.get("modelcontextprotocol.io/task")
812
- request = mcp.types.ReadResourceRequest(
813
- params=mcp.types.ReadResourceRequestParams(
814
- uri=uri,
815
- task=mcp.types.TaskMetadata(**task_dict)
816
- if task_dict
817
- else None, # SEP-1686: task as direct param (spec-compliant)
818
- )
819
- )
820
- result = await self.session.send_request(
821
- request=request, # type: ignore[arg-type]
822
- result_type=mcp.types.ReadResourceResult,
823
- )
824
- else:
825
- result = await self.session.read_resource(uri)
826
- return result
827
-
828
- @overload
829
- async def read_resource(
830
- self,
831
- uri: AnyUrl | str,
832
- *,
833
- task: Literal[False] = False,
834
- ) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]: ...
835
-
836
- @overload
837
- async def read_resource(
838
- self,
839
- uri: AnyUrl | str,
840
- *,
841
- task: Literal[True],
842
- task_id: str | None = None,
843
- ttl: int = 60000,
844
- ) -> ResourceTask: ...
845
-
846
- async def read_resource(
847
- self,
848
- uri: AnyUrl | str,
849
- *,
850
- task: bool = False,
851
- task_id: str | None = None,
852
- ttl: int = 60000,
853
- ) -> (
854
- list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
855
- | ResourceTask
856
- ):
857
- """Read the contents of a resource or resolved template.
858
-
859
- Args:
860
- uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
861
- task (bool): If True, execute as background task (SEP-1686). Defaults to False.
862
- task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
863
- ttl (int): Time to keep results available in milliseconds (default 60s).
864
-
865
- Returns:
866
- list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask:
867
- A list of content objects if task=False, or a ResourceTask object if task=True.
868
-
869
- Raises:
870
- RuntimeError: If called while the client is not connected.
871
- """
872
- if task:
873
- return await self._read_resource_as_task(uri, task_id, ttl)
874
-
875
- if isinstance(uri, str):
876
- try:
877
- uri = AnyUrl(uri) # Ensure AnyUrl
878
- except Exception as e:
879
- raise ValueError(
880
- f"Provided resource URI is invalid: {str(uri)!r}"
881
- ) from e
882
- result = await self.read_resource_mcp(uri)
883
- return result.contents
884
-
885
- async def _read_resource_as_task(
886
- self,
887
- uri: AnyUrl | str,
888
- task_id: str | None = None,
889
- ttl: int = 60000,
890
- ) -> ResourceTask:
891
- """Read a resource for background execution (SEP-1686).
892
-
893
- Returns a ResourceTask object that handles both background and immediate execution.
894
-
895
- Args:
896
- uri: Resource URI to read
897
- task_id: Optional client-provided task ID (ignored, for backward compatibility)
898
- ttl: Time to keep results available in milliseconds (default 60s)
899
-
900
- Returns:
901
- ResourceTask: Future-like object for accessing task status and results
902
- """
903
- # Per SEP-1686 final spec: client sends only ttl, server generates taskId
904
- # Read resource with task metadata (no taskId sent)
905
- result = await self.read_resource_mcp(
906
- uri=uri,
907
- meta={
908
- "modelcontextprotocol.io/task": {
909
- "ttl": ttl,
910
- }
911
- },
912
- )
913
-
914
- # Check if server accepted background execution
915
- # If response includes task metadata with taskId, server accepted background mode
916
- # If response includes returned_immediately=True, server declined and executed sync
917
- task_meta = (result.meta or {}).get("modelcontextprotocol.io/task", {})
918
- if task_meta.get("taskId"):
919
- # Background execution accepted - extract server-generated taskId
920
- server_task_id = task_meta["taskId"]
921
- # Track this task ID for list_tasks()
922
- self._submitted_task_ids.add(server_task_id)
923
-
924
- # Create task object
925
- task_obj = ResourceTask(
926
- self, server_task_id, uri=str(uri), immediate_result=None
927
- )
928
-
929
- # Register for notification routing
930
- self._task_registry[server_task_id] = weakref.ref(task_obj) # type: ignore[assignment]
931
-
932
- return task_obj
933
- else:
934
- # Server declined background execution (graceful degradation)
935
- # Use a synthetic task ID for the immediate result
936
- synthetic_task_id = task_id or str(uuid.uuid4())
937
- return ResourceTask(
938
- self, synthetic_task_id, uri=str(uri), immediate_result=result.contents
939
- )
940
-
941
- # async def subscribe_resource(self, uri: AnyUrl | str) -> None:
942
- # """Send a resources/subscribe request."""
943
- # if isinstance(uri, str):
944
- # uri = AnyUrl(uri)
945
- # await self.session.subscribe_resource(uri)
946
-
947
- # async def unsubscribe_resource(self, uri: AnyUrl | str) -> None:
948
- # """Send a resources/unsubscribe request."""
949
- # if isinstance(uri, str):
950
- # uri = AnyUrl(uri)
951
- # await self.session.unsubscribe_resource(uri)
952
-
953
- # --- Prompts ---
954
-
955
- async def list_prompts_mcp(self) -> mcp.types.ListPromptsResult:
956
- """Send a prompts/list request and return the complete MCP protocol result.
957
-
958
- Returns:
959
- mcp.types.ListPromptsResult: The complete response object from the protocol,
960
- containing the list of prompts and any additional metadata.
961
-
962
- Raises:
963
- RuntimeError: If called while the client is not connected.
964
- """
965
- logger.debug(f"[{self.name}] called list_prompts")
966
-
967
- result = await self.session.list_prompts()
968
- return result
969
-
970
- async def list_prompts(self) -> list[mcp.types.Prompt]:
971
- """Retrieve a list of prompts available on the server.
972
-
973
- Returns:
974
- list[mcp.types.Prompt]: A list of Prompt objects.
975
-
976
- Raises:
977
- RuntimeError: If called while the client is not connected.
978
- """
979
- result = await self.list_prompts_mcp()
980
- return result.prompts
981
-
982
- # --- Prompt ---
983
- async def get_prompt_mcp(
984
- self,
985
- name: str,
986
- arguments: dict[str, Any] | None = None,
987
- meta: dict[str, Any] | None = None,
988
- ) -> mcp.types.GetPromptResult:
989
- """Send a prompts/get request and return the complete MCP protocol result.
990
-
991
- Args:
992
- name (str): The name of the prompt to retrieve.
993
- arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
994
- meta (dict[str, Any] | None, optional): Request metadata (e.g., for SEP-1686 tasks). Defaults to None.
995
-
996
- Returns:
997
- mcp.types.GetPromptResult: The complete response object from the protocol,
998
- containing the prompt messages and any additional metadata.
999
-
1000
- Raises:
1001
- RuntimeError: If called while the client is not connected.
1002
- """
1003
- logger.debug(f"[{self.name}] called get_prompt: {name}")
1004
-
1005
- # Serialize arguments for MCP protocol - convert non-string values to JSON
1006
- serialized_arguments: dict[str, str] | None = None
1007
- if arguments:
1008
- serialized_arguments = {}
1009
- for key, value in arguments.items():
1010
- if isinstance(value, str):
1011
- serialized_arguments[key] = value
1012
- else:
1013
- # Use pydantic_core.to_json for consistent serialization
1014
- serialized_arguments[key] = pydantic_core.to_json(value).decode(
1015
- "utf-8"
1016
- )
1017
-
1018
- # If meta provided, use send_request for SEP-1686 task support
1019
- if meta:
1020
- task_dict = meta.get("modelcontextprotocol.io/task")
1021
- request = mcp.types.GetPromptRequest(
1022
- params=mcp.types.GetPromptRequestParams(
1023
- name=name,
1024
- arguments=serialized_arguments,
1025
- task=mcp.types.TaskMetadata(**task_dict)
1026
- if task_dict
1027
- else None, # SEP-1686: task as direct param (spec-compliant)
1028
- )
1029
- )
1030
- result = await self.session.send_request(
1031
- request=request, # type: ignore[arg-type]
1032
- result_type=mcp.types.GetPromptResult,
1033
- )
1034
- else:
1035
- result = await self.session.get_prompt(
1036
- name=name, arguments=serialized_arguments
1037
- )
1038
- return result
1039
-
1040
- @overload
1041
- async def get_prompt(
1042
- self,
1043
- name: str,
1044
- arguments: dict[str, Any] | None = None,
1045
- *,
1046
- task: Literal[False] = False,
1047
- ) -> mcp.types.GetPromptResult: ...
1048
-
1049
- @overload
1050
- async def get_prompt(
1051
- self,
1052
- name: str,
1053
- arguments: dict[str, Any] | None = None,
1054
- *,
1055
- task: Literal[True],
1056
- task_id: str | None = None,
1057
- ttl: int = 60000,
1058
- ) -> PromptTask: ...
1059
-
1060
- async def get_prompt(
1061
- self,
1062
- name: str,
1063
- arguments: dict[str, Any] | None = None,
1064
- *,
1065
- task: bool = False,
1066
- task_id: str | None = None,
1067
- ttl: int = 60000,
1068
- ) -> mcp.types.GetPromptResult | PromptTask:
1069
- """Retrieve a rendered prompt message list from the server.
1070
-
1071
- Args:
1072
- name (str): The name of the prompt to retrieve.
1073
- arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
1074
- task (bool): If True, execute as background task (SEP-1686). Defaults to False.
1075
- task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
1076
- ttl (int): Time to keep results available in milliseconds (default 60s).
1077
-
1078
- Returns:
1079
- mcp.types.GetPromptResult | PromptTask: The complete response object if task=False,
1080
- or a PromptTask object if task=True.
1081
-
1082
- Raises:
1083
- RuntimeError: If called while the client is not connected.
1084
- """
1085
- if task:
1086
- return await self._get_prompt_as_task(name, arguments, task_id, ttl)
1087
-
1088
- result = await self.get_prompt_mcp(name=name, arguments=arguments)
1089
- return result
1090
-
1091
- async def _get_prompt_as_task(
1092
- self,
1093
- name: str,
1094
- arguments: dict[str, Any] | None = None,
1095
- task_id: str | None = None,
1096
- ttl: int = 60000,
1097
- ) -> PromptTask:
1098
- """Get a prompt for background execution (SEP-1686).
1099
-
1100
- Returns a PromptTask object that handles both background and immediate execution.
1101
-
1102
- Args:
1103
- name: Prompt name to get
1104
- arguments: Prompt arguments
1105
- task_id: Optional client-provided task ID (ignored, for backward compatibility)
1106
- ttl: Time to keep results available in milliseconds (default 60s)
1107
-
1108
- Returns:
1109
- PromptTask: Future-like object for accessing task status and results
1110
- """
1111
- # Per SEP-1686 final spec: client sends only ttl, server generates taskId
1112
- # Call prompt with task metadata (no taskId sent)
1113
- result = await self.get_prompt_mcp(
1114
- name=name,
1115
- arguments=arguments or {},
1116
- meta={
1117
- "modelcontextprotocol.io/task": {
1118
- "ttl": ttl,
1119
- }
1120
- },
1121
- )
1122
-
1123
- # Check if server accepted background execution
1124
- # If response includes task metadata with taskId, server accepted background mode
1125
- # If response includes returned_immediately=True, server declined and executed sync
1126
- task_meta = (result.meta or {}).get("modelcontextprotocol.io/task", {})
1127
- if task_meta.get("taskId"):
1128
- # Background execution accepted - extract server-generated taskId
1129
- server_task_id = task_meta["taskId"]
1130
- # Track this task ID for list_tasks()
1131
- self._submitted_task_ids.add(server_task_id)
1132
-
1133
- # Create task object
1134
- task_obj = PromptTask(
1135
- self, server_task_id, prompt_name=name, immediate_result=None
1136
- )
1137
-
1138
- # Register for notification routing
1139
- self._task_registry[server_task_id] = weakref.ref(task_obj) # type: ignore[assignment]
1140
-
1141
- return task_obj
1142
- else:
1143
- # Server declined background execution (graceful degradation)
1144
- # Use a synthetic task ID for the immediate result
1145
- synthetic_task_id = task_id or str(uuid.uuid4())
1146
- return PromptTask(
1147
- self, synthetic_task_id, prompt_name=name, immediate_result=result
1148
- )
1149
-
1150
783
  # --- Completion ---
1151
784
 
1152
785
  async def complete_mcp(
@@ -1169,11 +802,14 @@ class Client(Generic[ClientTransportT]):
1169
802
 
1170
803
  Raises:
1171
804
  RuntimeError: If called while the client is not connected.
805
+ McpError: If the request results in a TimeoutError | JSONRPCError
1172
806
  """
1173
807
  logger.debug(f"[{self.name}] called complete: {ref}")
1174
808
 
1175
- result = await self.session.complete(
1176
- ref=ref, argument=argument, context_arguments=context_arguments
809
+ result = await self._await_with_session_monitoring(
810
+ self.session.complete(
811
+ ref=ref, argument=argument, context_arguments=context_arguments
812
+ )
1177
813
  )
1178
814
  return result
1179
815
 
@@ -1196,419 +832,13 @@ class Client(Generic[ClientTransportT]):
1196
832
 
1197
833
  Raises:
1198
834
  RuntimeError: If called while the client is not connected.
835
+ McpError: If the request results in a TimeoutError | JSONRPCError
1199
836
  """
1200
837
  result = await self.complete_mcp(
1201
838
  ref=ref, argument=argument, context_arguments=context_arguments
1202
839
  )
1203
840
  return result.completion
1204
841
 
1205
- # --- Tools ---
1206
-
1207
- async def list_tools_mcp(self) -> mcp.types.ListToolsResult:
1208
- """Send a tools/list request and return the complete MCP protocol result.
1209
-
1210
- Returns:
1211
- mcp.types.ListToolsResult: The complete response object from the protocol,
1212
- containing the list of tools and any additional metadata.
1213
-
1214
- Raises:
1215
- RuntimeError: If called while the client is not connected.
1216
- """
1217
- logger.debug(f"[{self.name}] called list_tools")
1218
-
1219
- result = await self.session.list_tools()
1220
- return result
1221
-
1222
- async def list_tools(self) -> list[mcp.types.Tool]:
1223
- """Retrieve a list of tools available on the server.
1224
-
1225
- Returns:
1226
- list[mcp.types.Tool]: A list of Tool objects.
1227
-
1228
- Raises:
1229
- RuntimeError: If called while the client is not connected.
1230
- """
1231
- result = await self.list_tools_mcp()
1232
- return result.tools
1233
-
1234
- # --- Call Tool ---
1235
-
1236
- async def call_tool_mcp(
1237
- self,
1238
- name: str,
1239
- arguments: dict[str, Any],
1240
- progress_handler: ProgressHandler | None = None,
1241
- timeout: datetime.timedelta | float | int | None = None,
1242
- meta: dict[str, Any] | None = None,
1243
- ) -> mcp.types.CallToolResult:
1244
- """Send a tools/call request and return the complete MCP protocol result.
1245
-
1246
- This method returns the raw CallToolResult object, which includes an isError flag
1247
- and other metadata. It does not raise an exception if the tool call results in an error.
1248
-
1249
- Args:
1250
- name (str): The name of the tool to call.
1251
- arguments (dict[str, Any]): Arguments to pass to the tool.
1252
- timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
1253
- progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
1254
- meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
1255
- This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
1256
- that shouldn't be tool arguments but may influence server-side processing. The server
1257
- can access this via `context.request_context.meta`. Defaults to None.
1258
-
1259
- Returns:
1260
- mcp.types.CallToolResult: The complete response object from the protocol,
1261
- containing the tool result and any additional metadata.
1262
-
1263
- Raises:
1264
- RuntimeError: If called while the client is not connected.
1265
- """
1266
- logger.debug(f"[{self.name}] called call_tool: {name}")
1267
-
1268
- # Convert timeout to timedelta if needed
1269
- if isinstance(timeout, int | float):
1270
- timeout = datetime.timedelta(seconds=float(timeout))
1271
-
1272
- # For task submissions, use send_request to bypass SDK validation
1273
- # Task acknowledgments don't have structured content, which would fail validation
1274
- if meta and "modelcontextprotocol.io/task" in meta:
1275
- task_dict = meta.get("modelcontextprotocol.io/task")
1276
- request = mcp.types.CallToolRequest(
1277
- params=mcp.types.CallToolRequestParams(
1278
- name=name,
1279
- arguments=arguments,
1280
- task=mcp.types.TaskMetadata(**task_dict)
1281
- if task_dict
1282
- else None, # SEP-1686: task as direct param (spec-compliant)
1283
- )
1284
- )
1285
- result = await self.session.send_request(
1286
- request=request, # type: ignore[arg-type]
1287
- result_type=mcp.types.CallToolResult,
1288
- request_read_timeout_seconds=timeout, # type: ignore[arg-type]
1289
- progress_callback=progress_handler or self._progress_handler,
1290
- )
1291
- else:
1292
- result = await self.session.call_tool(
1293
- name=name,
1294
- arguments=arguments,
1295
- read_timeout_seconds=timeout, # ty: ignore[invalid-argument-type]
1296
- progress_callback=progress_handler or self._progress_handler,
1297
- meta=meta,
1298
- )
1299
- return result
1300
-
1301
- async def _parse_call_tool_result(
1302
- self, name: str, result: mcp.types.CallToolResult, raise_on_error: bool = False
1303
- ) -> CallToolResult:
1304
- """Parse an mcp.types.CallToolResult into our CallToolResult dataclass.
1305
-
1306
- Args:
1307
- name: Tool name (for schema lookup)
1308
- result: Raw MCP protocol result
1309
- raise_on_error: Whether to raise ToolError on errors
1310
-
1311
- Returns:
1312
- CallToolResult: Parsed result with structured data
1313
- """
1314
- data = None
1315
- if result.isError and raise_on_error:
1316
- msg = cast(mcp.types.TextContent, result.content[0]).text
1317
- raise ToolError(msg)
1318
- elif result.structuredContent:
1319
- try:
1320
- if name not in self.session._tool_output_schemas:
1321
- await self.session.list_tools()
1322
- if name in self.session._tool_output_schemas:
1323
- output_schema = self.session._tool_output_schemas.get(name)
1324
- if output_schema:
1325
- if output_schema.get("x-fastmcp-wrap-result"):
1326
- output_schema = output_schema.get("properties", {}).get(
1327
- "result"
1328
- )
1329
- structured_content = result.structuredContent.get("result")
1330
- else:
1331
- structured_content = result.structuredContent
1332
- output_type = json_schema_to_type(output_schema)
1333
- type_adapter = get_cached_typeadapter(output_type)
1334
- data = type_adapter.validate_python(structured_content)
1335
- else:
1336
- data = result.structuredContent
1337
- except Exception as e:
1338
- logger.error(f"[{self.name}] Error parsing structured content: {e}")
1339
-
1340
- return CallToolResult(
1341
- content=result.content,
1342
- structured_content=result.structuredContent,
1343
- meta=result.meta,
1344
- data=data,
1345
- is_error=result.isError,
1346
- )
1347
-
1348
- @overload
1349
- async def call_tool(
1350
- self,
1351
- name: str,
1352
- arguments: dict[str, Any] | None = None,
1353
- *,
1354
- timeout: datetime.timedelta | float | int | None = None,
1355
- progress_handler: ProgressHandler | None = None,
1356
- raise_on_error: bool = True,
1357
- meta: dict[str, Any] | None = None,
1358
- task: Literal[False] = False,
1359
- ) -> CallToolResult: ...
1360
-
1361
- @overload
1362
- async def call_tool(
1363
- self,
1364
- name: str,
1365
- arguments: dict[str, Any] | None = None,
1366
- *,
1367
- timeout: datetime.timedelta | float | int | None = None,
1368
- progress_handler: ProgressHandler | None = None,
1369
- raise_on_error: bool = True,
1370
- meta: dict[str, Any] | None = None,
1371
- task: Literal[True],
1372
- task_id: str | None = None,
1373
- ttl: int = 60000,
1374
- ) -> ToolTask: ...
1375
-
1376
- async def call_tool(
1377
- self,
1378
- name: str,
1379
- arguments: dict[str, Any] | None = None,
1380
- *,
1381
- timeout: datetime.timedelta | float | int | None = None,
1382
- progress_handler: ProgressHandler | None = None,
1383
- raise_on_error: bool = True,
1384
- meta: dict[str, Any] | None = None,
1385
- task: bool = False,
1386
- task_id: str | None = None,
1387
- ttl: int = 60000,
1388
- ) -> CallToolResult | ToolTask:
1389
- """Call a tool on the server.
1390
-
1391
- Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.
1392
-
1393
- Args:
1394
- name (str): The name of the tool to call.
1395
- arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
1396
- timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
1397
- progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
1398
- raise_on_error (bool, optional): Whether to raise an exception if the tool call results in an error. Defaults to True.
1399
- meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
1400
- This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
1401
- that shouldn't be tool arguments but may influence server-side processing. The server
1402
- can access this via `context.request_context.meta`. Defaults to None.
1403
- task (bool): If True, execute as background task (SEP-1686). Defaults to False.
1404
- task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
1405
- ttl (int): Time to keep results available in milliseconds (default 60s).
1406
-
1407
- Returns:
1408
- CallToolResult | ToolTask: The content returned by the tool if task=False,
1409
- or a ToolTask object if task=True. If the tool returns structured
1410
- outputs, they are returned as a dataclass (if an output schema
1411
- is available) or a dictionary; otherwise, a list of content
1412
- blocks is returned. Note: to receive both structured and
1413
- unstructured outputs, use call_tool_mcp instead and access the
1414
- raw result object.
1415
-
1416
- Raises:
1417
- ToolError: If the tool call results in an error.
1418
- RuntimeError: If called while the client is not connected.
1419
- """
1420
- if task:
1421
- return await self._call_tool_as_task(name, arguments, task_id, ttl)
1422
-
1423
- result = await self.call_tool_mcp(
1424
- name=name,
1425
- arguments=arguments or {},
1426
- timeout=timeout,
1427
- progress_handler=progress_handler,
1428
- meta=meta,
1429
- )
1430
- return await self._parse_call_tool_result(
1431
- name, result, raise_on_error=raise_on_error
1432
- )
1433
-
1434
- async def _call_tool_as_task(
1435
- self,
1436
- name: str,
1437
- arguments: dict[str, Any] | None = None,
1438
- task_id: str | None = None,
1439
- ttl: int = 60000,
1440
- ) -> ToolTask:
1441
- """Call a tool for background execution (SEP-1686).
1442
-
1443
- Returns a ToolTask object that handles both background and immediate execution.
1444
- If the server accepts background execution, ToolTask will poll for results.
1445
- If the server declines (graceful degradation), ToolTask wraps the immediate result.
1446
-
1447
- Args:
1448
- name: Tool name to call
1449
- arguments: Tool arguments
1450
- task_id: Optional client-provided task ID (ignored, for backward compatibility)
1451
- ttl: Time to keep results available in milliseconds (default 60s)
1452
-
1453
- Returns:
1454
- ToolTask: Future-like object for accessing task status and results
1455
- """
1456
- # Per SEP-1686 final spec: client sends only ttl, server generates taskId
1457
- # Call tool with task metadata (no taskId sent)
1458
- result = await self.call_tool_mcp(
1459
- name=name,
1460
- arguments=arguments or {},
1461
- meta={
1462
- "modelcontextprotocol.io/task": {
1463
- "ttl": ttl,
1464
- }
1465
- },
1466
- )
1467
-
1468
- # Check if server accepted background execution
1469
- # If response includes task metadata with taskId, server accepted background mode
1470
- # If response includes returned_immediately=True, server declined and executed sync
1471
- task_meta = (result.meta or {}).get("modelcontextprotocol.io/task", {})
1472
- if task_meta.get("taskId"):
1473
- # Background execution accepted - extract server-generated taskId
1474
- server_task_id = task_meta["taskId"]
1475
- # Track this task ID for list_tasks()
1476
- self._submitted_task_ids.add(server_task_id)
1477
-
1478
- # Create task object
1479
- task_obj = ToolTask(
1480
- self, server_task_id, tool_name=name, immediate_result=None
1481
- )
1482
-
1483
- # Register for notification routing
1484
- self._task_registry[server_task_id] = weakref.ref(task_obj) # type: ignore[assignment]
1485
-
1486
- return task_obj
1487
- else:
1488
- # Server declined background execution (graceful degradation)
1489
- # or returned_immediately=True - executed synchronously
1490
- # Wrap the immediate result
1491
- parsed_result = await self._parse_call_tool_result(name, result)
1492
- # Use a synthetic task ID for the immediate result
1493
- synthetic_task_id = task_id or str(uuid.uuid4())
1494
- return ToolTask(
1495
- self, synthetic_task_id, tool_name=name, immediate_result=parsed_result
1496
- )
1497
-
1498
- async def get_task_status(self, task_id: str) -> GetTaskResult:
1499
- """Query the status of a background task.
1500
-
1501
- Sends a 'tasks/get' MCP protocol request over the existing transport.
1502
-
1503
- Args:
1504
- task_id: The task ID returned from call_tool_as_task
1505
-
1506
- Returns:
1507
- GetTaskResult: Status information including taskId, status, pollInterval, etc.
1508
-
1509
- Raises:
1510
- RuntimeError: If client not connected
1511
- """
1512
- request = GetTaskRequest(params=GetTaskRequestParams(taskId=task_id))
1513
- return await self.session.send_request(
1514
- request=request, # type: ignore[arg-type]
1515
- result_type=GetTaskResult, # type: ignore[arg-type]
1516
- )
1517
-
1518
- async def get_task_result(self, task_id: str) -> Any:
1519
- """Retrieve the raw result of a completed background task.
1520
-
1521
- Sends a 'tasks/result' MCP protocol request over the existing transport.
1522
- Returns the raw result - callers should parse it appropriately.
1523
-
1524
- Args:
1525
- task_id: The task ID returned from call_tool_as_task
1526
-
1527
- Returns:
1528
- Any: The raw result (could be tool, prompt, or resource result)
1529
-
1530
- Raises:
1531
- RuntimeError: If client not connected, task not found, or task failed
1532
- """
1533
- request = GetTaskPayloadRequest(
1534
- params=GetTaskPayloadRequestParams(taskId=task_id)
1535
- )
1536
- # Return raw result - Task classes handle type-specific parsing
1537
- result = await self.session.send_request(
1538
- request=request, # type: ignore[arg-type]
1539
- result_type=GetTaskPayloadResult, # type: ignore[arg-type]
1540
- )
1541
- # Return as dict for compatibility with Task class parsing
1542
- return result.model_dump(exclude_none=True, by_alias=True)
1543
-
1544
- async def list_tasks(
1545
- self,
1546
- cursor: str | None = None,
1547
- limit: int = 50,
1548
- ) -> dict[str, Any]:
1549
- """List background tasks.
1550
-
1551
- Sends a 'tasks/list' MCP protocol request to the server. If the server
1552
- returns an empty list (indicating client-side tracking), falls back to
1553
- querying status for locally tracked task IDs.
1554
-
1555
- Args:
1556
- cursor: Optional pagination cursor
1557
- limit: Maximum number of tasks to return (default 50)
1558
-
1559
- Returns:
1560
- dict: Response with structure:
1561
- - tasks: List of task status dicts with taskId, status, etc.
1562
- - nextCursor: Optional cursor for next page
1563
-
1564
- Raises:
1565
- RuntimeError: If client not connected
1566
- """
1567
- # Send protocol request
1568
- params = PaginatedRequestParams(cursor=cursor, limit=limit)
1569
- request = ListTasksRequest(params=params)
1570
- server_response = await self.session.send_request(
1571
- request=request, # type: ignore[invalid-argument-type]
1572
- result_type=mcp.types.ListTasksResult,
1573
- )
1574
-
1575
- # If server returned tasks, use those
1576
- if server_response.tasks:
1577
- return server_response.model_dump(by_alias=True)
1578
-
1579
- # Server returned empty - fall back to client-side tracking
1580
- tasks = []
1581
- for task_id in list(self._submitted_task_ids)[:limit]:
1582
- try:
1583
- status = await self.get_task_status(task_id)
1584
- tasks.append(status.model_dump(by_alias=True))
1585
- except Exception:
1586
- # Task may have expired or been deleted, skip it
1587
- continue
1588
-
1589
- return {"tasks": tasks, "nextCursor": None}
1590
-
1591
- async def cancel_task(self, task_id: str) -> mcp.types.CancelTaskResult:
1592
- """Cancel a task, transitioning it to cancelled state.
1593
-
1594
- Sends a 'tasks/cancel' MCP protocol request. Task will halt execution
1595
- and transition to cancelled state.
1596
-
1597
- Args:
1598
- task_id: The task ID to cancel
1599
-
1600
- Returns:
1601
- CancelTaskResult: The task status showing cancelled state
1602
-
1603
- Raises:
1604
- RuntimeError: If task doesn't exist
1605
- """
1606
- request = CancelTaskRequest(params=CancelTaskRequestParams(taskId=task_id))
1607
- return await self.session.send_request(
1608
- request=request, # type: ignore[invalid-argument-type]
1609
- result_type=mcp.types.CancelTaskResult,
1610
- )
1611
-
1612
842
  @classmethod
1613
843
  def generate_name(cls, name: str | None = None) -> str:
1614
844
  class_name = cls.__name__