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.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.5.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
1176
|
-
|
|
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__
|