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