fastmcp 2.14.4__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.4.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.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Task management methods for FastMCP Client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import mcp.types
|
|
8
|
+
from mcp import McpError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from fastmcp.client.client import Client
|
|
12
|
+
from mcp.types import (
|
|
13
|
+
CancelTaskRequest,
|
|
14
|
+
CancelTaskRequestParams,
|
|
15
|
+
GetTaskPayloadRequest,
|
|
16
|
+
GetTaskPayloadRequestParams,
|
|
17
|
+
GetTaskPayloadResult,
|
|
18
|
+
GetTaskRequest,
|
|
19
|
+
GetTaskRequestParams,
|
|
20
|
+
GetTaskResult,
|
|
21
|
+
ListTasksRequest,
|
|
22
|
+
PaginatedRequestParams,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from fastmcp.utilities.logging import get_logger
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ClientTaskManagementMixin:
|
|
31
|
+
"""Mixin providing task management methods for Client."""
|
|
32
|
+
|
|
33
|
+
async def get_task_status(self: Client, task_id: str) -> GetTaskResult:
|
|
34
|
+
"""Query the status of a background task.
|
|
35
|
+
|
|
36
|
+
Sends a 'tasks/get' MCP protocol request over the existing transport.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
task_id: The task ID returned from call_tool_as_task
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
GetTaskResult: Status information including taskId, status, pollInterval, etc.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
RuntimeError: If client not connected
|
|
46
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
47
|
+
"""
|
|
48
|
+
request = GetTaskRequest(params=GetTaskRequestParams(taskId=task_id))
|
|
49
|
+
return await self._await_with_session_monitoring(
|
|
50
|
+
self.session.send_request(
|
|
51
|
+
request=request, # type: ignore[arg-type]
|
|
52
|
+
result_type=GetTaskResult,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def get_task_result(self: Client, task_id: str) -> Any:
|
|
57
|
+
"""Retrieve the raw result of a completed background task.
|
|
58
|
+
|
|
59
|
+
Sends a 'tasks/result' MCP protocol request over the existing transport.
|
|
60
|
+
Returns the raw result - callers should parse it appropriately.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
task_id: The task ID returned from call_tool_as_task
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Any: The raw result (could be tool, prompt, or resource result)
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
RuntimeError: If client not connected, task not found, or task failed
|
|
70
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
71
|
+
"""
|
|
72
|
+
request = GetTaskPayloadRequest(
|
|
73
|
+
params=GetTaskPayloadRequestParams(taskId=task_id)
|
|
74
|
+
)
|
|
75
|
+
# Return raw result - Task classes handle type-specific parsing
|
|
76
|
+
result = await self._await_with_session_monitoring(
|
|
77
|
+
self.session.send_request(
|
|
78
|
+
request=request, # type: ignore[arg-type]
|
|
79
|
+
result_type=GetTaskPayloadResult,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
# Return as dict for compatibility with Task class parsing
|
|
83
|
+
return result.model_dump(exclude_none=True, by_alias=True)
|
|
84
|
+
|
|
85
|
+
async def list_tasks(
|
|
86
|
+
self: Client,
|
|
87
|
+
cursor: str | None = None,
|
|
88
|
+
limit: int = 50,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""List background tasks.
|
|
91
|
+
|
|
92
|
+
Sends a 'tasks/list' MCP protocol request to the server. If the server
|
|
93
|
+
returns an empty list (indicating client-side tracking), falls back to
|
|
94
|
+
querying status for locally tracked task IDs.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
cursor: Optional pagination cursor
|
|
98
|
+
limit: Maximum number of tasks to return (default 50)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
dict: Response with structure:
|
|
102
|
+
- tasks: List of task status dicts with taskId, status, etc.
|
|
103
|
+
- nextCursor: Optional cursor for next page
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
RuntimeError: If client not connected
|
|
107
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
108
|
+
"""
|
|
109
|
+
# Send protocol request
|
|
110
|
+
params = PaginatedRequestParams(cursor=cursor, limit=limit) # type: ignore[call-arg] # Optional field in MCP SDK
|
|
111
|
+
request = ListTasksRequest(params=params)
|
|
112
|
+
server_response = await self._await_with_session_monitoring(
|
|
113
|
+
self.session.send_request(
|
|
114
|
+
request=request, # type: ignore[invalid-argument-type]
|
|
115
|
+
result_type=mcp.types.ListTasksResult,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# If server returned tasks, use those
|
|
120
|
+
if server_response.tasks:
|
|
121
|
+
return server_response.model_dump(by_alias=True)
|
|
122
|
+
|
|
123
|
+
# Server returned empty - fall back to client-side tracking
|
|
124
|
+
tasks = []
|
|
125
|
+
for task_id in list(self._submitted_task_ids)[:limit]:
|
|
126
|
+
try:
|
|
127
|
+
status = await self.get_task_status(task_id)
|
|
128
|
+
tasks.append(status.model_dump(by_alias=True))
|
|
129
|
+
except McpError:
|
|
130
|
+
# Task may have expired or been deleted, skip it
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
return {"tasks": tasks, "nextCursor": None}
|
|
134
|
+
|
|
135
|
+
async def cancel_task(self: Client, task_id: str) -> mcp.types.CancelTaskResult:
|
|
136
|
+
"""Cancel a task, transitioning it to cancelled state.
|
|
137
|
+
|
|
138
|
+
Sends a 'tasks/cancel' MCP protocol request. Task will halt execution
|
|
139
|
+
and transition to cancelled state.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
task_id: The task ID to cancel
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
CancelTaskResult: The task status showing cancelled state
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
RuntimeError: If task doesn't exist
|
|
149
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
150
|
+
"""
|
|
151
|
+
request = CancelTaskRequest(params=CancelTaskRequestParams(taskId=task_id))
|
|
152
|
+
return await self._await_with_session_monitoring(
|
|
153
|
+
self.session.send_request(
|
|
154
|
+
request=request, # type: ignore[invalid-argument-type]
|
|
155
|
+
result_type=mcp.types.CancelTaskResult,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Tool-related methods for FastMCP Client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
import weakref
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
8
|
+
|
|
9
|
+
import mcp.types
|
|
10
|
+
from pydantic import RootModel
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import datetime
|
|
14
|
+
|
|
15
|
+
from fastmcp.client.client import CallToolResult, Client
|
|
16
|
+
from fastmcp.client.progress import ProgressHandler
|
|
17
|
+
from fastmcp.client.tasks import ToolTask
|
|
18
|
+
from fastmcp.client.telemetry import client_span
|
|
19
|
+
from fastmcp.exceptions import ToolError
|
|
20
|
+
from fastmcp.telemetry import inject_trace_context
|
|
21
|
+
from fastmcp.utilities.json_schema_type import json_schema_to_type
|
|
22
|
+
from fastmcp.utilities.logging import get_logger
|
|
23
|
+
from fastmcp.utilities.timeout import normalize_timeout_to_timedelta
|
|
24
|
+
from fastmcp.utilities.types import get_cached_typeadapter
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
# Type alias for task response union (SEP-1686 graceful degradation)
|
|
29
|
+
ToolTaskResponseUnion = RootModel[mcp.types.CreateTaskResult | mcp.types.CallToolResult]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ClientToolsMixin:
|
|
33
|
+
"""Mixin providing tool-related methods for Client."""
|
|
34
|
+
|
|
35
|
+
# --- Tools ---
|
|
36
|
+
|
|
37
|
+
async def list_tools_mcp(
|
|
38
|
+
self: Client, *, cursor: str | None = None
|
|
39
|
+
) -> mcp.types.ListToolsResult:
|
|
40
|
+
"""Send a tools/list request and return the complete MCP protocol result.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
cursor: Optional pagination cursor from a previous request's nextCursor.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
mcp.types.ListToolsResult: The complete response object from the protocol,
|
|
47
|
+
containing the list of tools and any additional metadata.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
RuntimeError: If called while the client is not connected.
|
|
51
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
52
|
+
"""
|
|
53
|
+
logger.debug(f"[{self.name}] called list_tools")
|
|
54
|
+
|
|
55
|
+
result = await self._await_with_session_monitoring(
|
|
56
|
+
self.session.list_tools(cursor=cursor)
|
|
57
|
+
)
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
async def list_tools(self: Client) -> list[mcp.types.Tool]:
|
|
61
|
+
"""Retrieve all tools available on the server.
|
|
62
|
+
|
|
63
|
+
This method automatically fetches all pages if the server paginates results,
|
|
64
|
+
returning the complete list. For manual pagination control (e.g., to handle
|
|
65
|
+
large result sets incrementally), use list_tools_mcp() with the cursor parameter.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
list[mcp.types.Tool]: A list of all Tool objects.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
RuntimeError: If called while the client is not connected.
|
|
72
|
+
McpError: If the request results in a TimeoutError | JSONRPCError
|
|
73
|
+
"""
|
|
74
|
+
all_tools: list[mcp.types.Tool] = []
|
|
75
|
+
cursor: str | None = None
|
|
76
|
+
|
|
77
|
+
while True:
|
|
78
|
+
result = await self.list_tools_mcp(cursor=cursor)
|
|
79
|
+
all_tools.extend(result.tools)
|
|
80
|
+
if result.nextCursor is None:
|
|
81
|
+
break
|
|
82
|
+
cursor = result.nextCursor
|
|
83
|
+
|
|
84
|
+
return all_tools
|
|
85
|
+
|
|
86
|
+
# --- Call Tool ---
|
|
87
|
+
|
|
88
|
+
async def call_tool_mcp(
|
|
89
|
+
self: Client,
|
|
90
|
+
name: str,
|
|
91
|
+
arguments: dict[str, Any],
|
|
92
|
+
progress_handler: ProgressHandler | None = None,
|
|
93
|
+
timeout: datetime.timedelta | float | int | None = None,
|
|
94
|
+
meta: dict[str, Any] | None = None,
|
|
95
|
+
) -> mcp.types.CallToolResult:
|
|
96
|
+
"""Send a tools/call request and return the complete MCP protocol result.
|
|
97
|
+
|
|
98
|
+
This method returns the raw CallToolResult object, which includes an isError flag
|
|
99
|
+
and other metadata. It does not raise an exception if the tool call results in an error.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
name (str): The name of the tool to call.
|
|
103
|
+
arguments (dict[str, Any]): Arguments to pass to the tool.
|
|
104
|
+
timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
|
|
105
|
+
progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
|
|
106
|
+
meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
|
|
107
|
+
This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
|
|
108
|
+
that shouldn't be tool arguments but may influence server-side processing. The server
|
|
109
|
+
can access this via `context.request_context.meta`. Defaults to None.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
mcp.types.CallToolResult: The complete response object from the protocol,
|
|
113
|
+
containing the tool result and any additional metadata.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
RuntimeError: If called while the client is not connected.
|
|
117
|
+
McpError: If the tool call requests results in a TimeoutError | JSONRPCError
|
|
118
|
+
"""
|
|
119
|
+
with client_span(
|
|
120
|
+
f"tools/call {name}",
|
|
121
|
+
"tools/call",
|
|
122
|
+
name,
|
|
123
|
+
session_id=self.transport.get_session_id(),
|
|
124
|
+
):
|
|
125
|
+
logger.debug(f"[{self.name}] called call_tool: {name}")
|
|
126
|
+
|
|
127
|
+
# Inject trace context into meta for propagation to server
|
|
128
|
+
propagated_meta = inject_trace_context(meta)
|
|
129
|
+
|
|
130
|
+
result = await self._await_with_session_monitoring(
|
|
131
|
+
self.session.call_tool(
|
|
132
|
+
name=name,
|
|
133
|
+
arguments=arguments,
|
|
134
|
+
read_timeout_seconds=normalize_timeout_to_timedelta(timeout),
|
|
135
|
+
progress_callback=progress_handler or self._progress_handler,
|
|
136
|
+
meta=propagated_meta if propagated_meta else None,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
async def _parse_call_tool_result(
|
|
142
|
+
self: Client,
|
|
143
|
+
name: str,
|
|
144
|
+
result: mcp.types.CallToolResult,
|
|
145
|
+
raise_on_error: bool = False,
|
|
146
|
+
) -> CallToolResult:
|
|
147
|
+
"""Parse an mcp.types.CallToolResult into our CallToolResult dataclass.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: Tool name (for schema lookup)
|
|
151
|
+
result: Raw MCP protocol result
|
|
152
|
+
raise_on_error: Whether to raise ToolError on errors
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
CallToolResult: Parsed result with structured data
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
return await _parse_call_tool_result(
|
|
159
|
+
name=name,
|
|
160
|
+
result=result,
|
|
161
|
+
tool_output_schemas=self.session._tool_output_schemas,
|
|
162
|
+
list_tools_fn=self.session.list_tools,
|
|
163
|
+
client_name=self.name,
|
|
164
|
+
raise_on_error=raise_on_error,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@overload
|
|
168
|
+
async def call_tool(
|
|
169
|
+
self: Client,
|
|
170
|
+
name: str,
|
|
171
|
+
arguments: dict[str, Any] | None = None,
|
|
172
|
+
*,
|
|
173
|
+
version: str | None = None,
|
|
174
|
+
timeout: datetime.timedelta | float | int | None = None,
|
|
175
|
+
progress_handler: ProgressHandler | None = None,
|
|
176
|
+
raise_on_error: bool = True,
|
|
177
|
+
meta: dict[str, Any] | None = None,
|
|
178
|
+
task: Literal[False] = False,
|
|
179
|
+
) -> CallToolResult: ...
|
|
180
|
+
|
|
181
|
+
@overload
|
|
182
|
+
async def call_tool(
|
|
183
|
+
self: Client,
|
|
184
|
+
name: str,
|
|
185
|
+
arguments: dict[str, Any] | None = None,
|
|
186
|
+
*,
|
|
187
|
+
version: str | None = None,
|
|
188
|
+
timeout: datetime.timedelta | float | int | None = None,
|
|
189
|
+
progress_handler: ProgressHandler | None = None,
|
|
190
|
+
raise_on_error: bool = True,
|
|
191
|
+
meta: dict[str, Any] | None = None,
|
|
192
|
+
task: Literal[True],
|
|
193
|
+
task_id: str | None = None,
|
|
194
|
+
ttl: int = 60000,
|
|
195
|
+
) -> ToolTask: ...
|
|
196
|
+
|
|
197
|
+
async def call_tool(
|
|
198
|
+
self: Client,
|
|
199
|
+
name: str,
|
|
200
|
+
arguments: dict[str, Any] | None = None,
|
|
201
|
+
*,
|
|
202
|
+
version: str | None = None,
|
|
203
|
+
timeout: datetime.timedelta | float | int | None = None,
|
|
204
|
+
progress_handler: ProgressHandler | None = None,
|
|
205
|
+
raise_on_error: bool = True,
|
|
206
|
+
meta: dict[str, Any] | None = None,
|
|
207
|
+
task: bool = False,
|
|
208
|
+
task_id: str | None = None,
|
|
209
|
+
ttl: int = 60000,
|
|
210
|
+
) -> CallToolResult | ToolTask:
|
|
211
|
+
"""Call a tool on the server.
|
|
212
|
+
|
|
213
|
+
Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
name (str): The name of the tool to call.
|
|
217
|
+
arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
|
|
218
|
+
version (str | None, optional): Specific tool version to call. If None, calls highest version.
|
|
219
|
+
timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
|
|
220
|
+
progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
|
|
221
|
+
raise_on_error (bool, optional): Whether to raise an exception if the tool call results in an error. Defaults to True.
|
|
222
|
+
meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
|
|
223
|
+
This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
|
|
224
|
+
that shouldn't be tool arguments but may influence server-side processing. The server
|
|
225
|
+
can access this via `context.request_context.meta`. Defaults to None.
|
|
226
|
+
task (bool): If True, execute as background task (SEP-1686). Defaults to False.
|
|
227
|
+
task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
|
|
228
|
+
ttl (int): Time to keep results available in milliseconds (default 60s).
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
CallToolResult | ToolTask: The content returned by the tool if task=False,
|
|
232
|
+
or a ToolTask object if task=True. If the tool returns structured
|
|
233
|
+
outputs, they are returned as a dataclass (if an output schema
|
|
234
|
+
is available) or a dictionary; otherwise, a list of content
|
|
235
|
+
blocks is returned. Note: to receive both structured and
|
|
236
|
+
unstructured outputs, use call_tool_mcp instead and access the
|
|
237
|
+
raw result object.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
ToolError: If the tool call results in an error.
|
|
241
|
+
McpError: If the tool call request results in a TimeoutError | JSONRPCError
|
|
242
|
+
RuntimeError: If called while the client is not connected.
|
|
243
|
+
"""
|
|
244
|
+
# Merge version into request-level meta (not arguments)
|
|
245
|
+
request_meta = dict(meta) if meta else {}
|
|
246
|
+
if version is not None:
|
|
247
|
+
request_meta["fastmcp"] = {
|
|
248
|
+
**request_meta.get("fastmcp", {}),
|
|
249
|
+
"version": version,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if task:
|
|
253
|
+
return await self._call_tool_as_task(
|
|
254
|
+
name, arguments, task_id, ttl, meta=request_meta or None
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
result = await self.call_tool_mcp(
|
|
258
|
+
name=name,
|
|
259
|
+
arguments=arguments or {},
|
|
260
|
+
timeout=timeout,
|
|
261
|
+
progress_handler=progress_handler,
|
|
262
|
+
meta=request_meta or None,
|
|
263
|
+
)
|
|
264
|
+
return await self._parse_call_tool_result(
|
|
265
|
+
name, result, raise_on_error=raise_on_error
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
async def _call_tool_as_task(
|
|
269
|
+
self: Client,
|
|
270
|
+
name: str,
|
|
271
|
+
arguments: dict[str, Any] | None = None,
|
|
272
|
+
task_id: str | None = None,
|
|
273
|
+
ttl: int = 60000,
|
|
274
|
+
meta: dict[str, Any] | None = None,
|
|
275
|
+
) -> ToolTask:
|
|
276
|
+
"""Call a tool for background execution (SEP-1686).
|
|
277
|
+
|
|
278
|
+
Returns a ToolTask object that handles both background and immediate execution.
|
|
279
|
+
If the server accepts background execution, ToolTask will poll for results.
|
|
280
|
+
If the server declines (graceful degradation), ToolTask wraps the immediate result.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
name: Tool name to call
|
|
284
|
+
arguments: Tool arguments
|
|
285
|
+
task_id: Optional client-provided task ID (ignored, for backward compatibility)
|
|
286
|
+
ttl: Time to keep results available in milliseconds (default 60s)
|
|
287
|
+
meta: Optional request metadata (e.g., version info)
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
ToolTask: Future-like object for accessing task status and results
|
|
291
|
+
"""
|
|
292
|
+
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
|
|
293
|
+
# Inject trace context into meta for propagation to server
|
|
294
|
+
propagated_meta = inject_trace_context(meta)
|
|
295
|
+
|
|
296
|
+
# Build request with task metadata
|
|
297
|
+
request = mcp.types.CallToolRequest(
|
|
298
|
+
params=mcp.types.CallToolRequestParams(
|
|
299
|
+
name=name,
|
|
300
|
+
arguments=arguments or {},
|
|
301
|
+
task=mcp.types.TaskMetadata(ttl=ttl),
|
|
302
|
+
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Server returns CreateTaskResult (task accepted) or CallToolResult (graceful degradation)
|
|
307
|
+
# Use RootModel with Union to handle both response types (SDK calls model_validate)
|
|
308
|
+
wrapped_result = await self._await_with_session_monitoring(
|
|
309
|
+
self.session.send_request(
|
|
310
|
+
request=request, # type: ignore[arg-type]
|
|
311
|
+
result_type=ToolTaskResponseUnion,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
raw_result = wrapped_result.root
|
|
315
|
+
|
|
316
|
+
if isinstance(raw_result, mcp.types.CreateTaskResult):
|
|
317
|
+
# Task was accepted - extract task info from CreateTaskResult
|
|
318
|
+
server_task_id = raw_result.task.taskId
|
|
319
|
+
self._submitted_task_ids.add(server_task_id)
|
|
320
|
+
|
|
321
|
+
task_obj = ToolTask(
|
|
322
|
+
self, server_task_id, tool_name=name, immediate_result=None
|
|
323
|
+
)
|
|
324
|
+
self._task_registry[server_task_id] = weakref.ref(task_obj)
|
|
325
|
+
return task_obj
|
|
326
|
+
else:
|
|
327
|
+
# Graceful degradation - server returned CallToolResult
|
|
328
|
+
parsed_result = await self._parse_call_tool_result(name, raw_result)
|
|
329
|
+
synthetic_task_id = task_id or str(uuid.uuid4())
|
|
330
|
+
return ToolTask(
|
|
331
|
+
self,
|
|
332
|
+
synthetic_task_id,
|
|
333
|
+
tool_name=name,
|
|
334
|
+
immediate_result=parsed_result,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def _parse_call_tool_result(
|
|
339
|
+
name: str,
|
|
340
|
+
result: mcp.types.CallToolResult,
|
|
341
|
+
tool_output_schemas: dict[str, dict[str, Any] | None],
|
|
342
|
+
list_tools_fn: Any, # Callable[[], Awaitable[None]]
|
|
343
|
+
client_name: str | None = None,
|
|
344
|
+
raise_on_error: bool = False,
|
|
345
|
+
) -> CallToolResult:
|
|
346
|
+
"""Parse an mcp.types.CallToolResult into our CallToolResult dataclass.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
name: Tool name (for schema lookup)
|
|
350
|
+
result: Raw MCP protocol result
|
|
351
|
+
tool_output_schemas: Dictionary mapping tool names to their output schemas
|
|
352
|
+
list_tools_fn: Async function to refresh tool schemas if needed
|
|
353
|
+
client_name: Optional client name for logging
|
|
354
|
+
raise_on_error: Whether to raise ToolError on errors
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
CallToolResult: Parsed result with structured data
|
|
358
|
+
"""
|
|
359
|
+
from typing import cast
|
|
360
|
+
|
|
361
|
+
from fastmcp.client.client import CallToolResult
|
|
362
|
+
|
|
363
|
+
data = None
|
|
364
|
+
if result.isError and raise_on_error:
|
|
365
|
+
msg = cast(mcp.types.TextContent, result.content[0]).text
|
|
366
|
+
raise ToolError(msg)
|
|
367
|
+
elif result.structuredContent:
|
|
368
|
+
try:
|
|
369
|
+
if name not in tool_output_schemas:
|
|
370
|
+
await list_tools_fn()
|
|
371
|
+
if name in tool_output_schemas:
|
|
372
|
+
output_schema = tool_output_schemas.get(name)
|
|
373
|
+
if output_schema:
|
|
374
|
+
if output_schema.get("x-fastmcp-wrap-result"):
|
|
375
|
+
output_schema = output_schema.get("properties", {}).get(
|
|
376
|
+
"result"
|
|
377
|
+
)
|
|
378
|
+
structured_content = result.structuredContent.get("result")
|
|
379
|
+
else:
|
|
380
|
+
structured_content = result.structuredContent
|
|
381
|
+
output_type = json_schema_to_type(output_schema)
|
|
382
|
+
type_adapter = get_cached_typeadapter(output_type)
|
|
383
|
+
data = type_adapter.validate_python(structured_content)
|
|
384
|
+
else:
|
|
385
|
+
data = result.structuredContent
|
|
386
|
+
except Exception as e:
|
|
387
|
+
logger.error(
|
|
388
|
+
f"[{client_name or 'client'}] Error parsing structured content: {e}"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return CallToolResult(
|
|
392
|
+
content=result.content,
|
|
393
|
+
structured_content=result.structuredContent,
|
|
394
|
+
meta=result.meta,
|
|
395
|
+
data=data,
|
|
396
|
+
is_error=result.isError,
|
|
397
|
+
)
|
|
@@ -198,7 +198,7 @@ class AnthropicSamplingHandler:
|
|
|
198
198
|
anthropic_messages.append(
|
|
199
199
|
MessageParam(
|
|
200
200
|
role=message.role,
|
|
201
|
-
content=content_blocks,
|
|
201
|
+
content=content_blocks,
|
|
202
202
|
)
|
|
203
203
|
)
|
|
204
204
|
continue
|
|
@@ -310,7 +310,7 @@ class AnthropicSamplingHandler:
|
|
|
310
310
|
ToolParam(
|
|
311
311
|
name=tool.name,
|
|
312
312
|
description=tool.description or "",
|
|
313
|
-
input_schema=input_schema,
|
|
313
|
+
input_schema=input_schema,
|
|
314
314
|
)
|
|
315
315
|
)
|
|
316
316
|
return anthropic_tools
|
|
@@ -368,7 +368,7 @@ class OpenAISamplingHandler:
|
|
|
368
368
|
# Skip non-function tool calls
|
|
369
369
|
if not hasattr(tool_call, "function"):
|
|
370
370
|
continue
|
|
371
|
-
func = tool_call.function
|
|
371
|
+
func = tool_call.function
|
|
372
372
|
# Parse the arguments JSON string
|
|
373
373
|
try:
|
|
374
374
|
arguments = json.loads(func.arguments) # type: ignore[union-attr]
|
fastmcp/client/tasks.py
CHANGED
|
@@ -378,15 +378,15 @@ class ToolTask(Task["CallToolResult"]):
|
|
|
378
378
|
):
|
|
379
379
|
mcp_result = mcp.types.CallToolResult(
|
|
380
380
|
content=raw_result.content,
|
|
381
|
-
structuredContent=raw_result.structured_content,
|
|
382
|
-
_meta=raw_result.meta,
|
|
381
|
+
structuredContent=raw_result.structured_content,
|
|
382
|
+
_meta=raw_result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
|
383
383
|
)
|
|
384
384
|
result = await self._client._parse_call_tool_result(
|
|
385
385
|
self._tool_name, mcp_result, raise_on_error=True
|
|
386
386
|
)
|
|
387
387
|
else:
|
|
388
388
|
# Unknown type - just return it
|
|
389
|
-
result = raw_result
|
|
389
|
+
result = raw_result
|
|
390
390
|
|
|
391
391
|
# Cache before returning
|
|
392
392
|
self._cached_result = result
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Client-side telemetry helpers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
|
|
6
|
+
from opentelemetry.trace import Span, SpanKind, Status, StatusCode
|
|
7
|
+
|
|
8
|
+
from fastmcp.telemetry import get_tracer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@contextmanager
|
|
12
|
+
def client_span(
|
|
13
|
+
name: str,
|
|
14
|
+
method: str,
|
|
15
|
+
component_key: str,
|
|
16
|
+
session_id: str | None = None,
|
|
17
|
+
resource_uri: str | None = None,
|
|
18
|
+
) -> Generator[Span, None, None]:
|
|
19
|
+
"""Create a CLIENT span with standard MCP attributes.
|
|
20
|
+
|
|
21
|
+
Automatically records any exception on the span and sets error status.
|
|
22
|
+
"""
|
|
23
|
+
tracer = get_tracer()
|
|
24
|
+
with tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span:
|
|
25
|
+
attrs: dict[str, str] = {
|
|
26
|
+
# RPC semantic conventions
|
|
27
|
+
"rpc.system": "mcp",
|
|
28
|
+
"rpc.method": method,
|
|
29
|
+
# MCP semantic conventions
|
|
30
|
+
"mcp.method.name": method,
|
|
31
|
+
# FastMCP-specific attributes
|
|
32
|
+
"fastmcp.component.key": component_key,
|
|
33
|
+
}
|
|
34
|
+
if session_id:
|
|
35
|
+
attrs["mcp.session.id"] = session_id
|
|
36
|
+
if resource_uri:
|
|
37
|
+
attrs["mcp.resource.uri"] = resource_uri
|
|
38
|
+
span.set_attributes(attrs)
|
|
39
|
+
try:
|
|
40
|
+
yield span
|
|
41
|
+
except Exception as e:
|
|
42
|
+
span.record_exception(e)
|
|
43
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["client_span"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Re-export all public APIs for backward compatibility
|
|
2
|
+
from mcp.server.fastmcp import FastMCP as FastMCP1Server
|
|
3
|
+
|
|
4
|
+
from fastmcp.client.transports.base import (
|
|
5
|
+
ClientTransport,
|
|
6
|
+
ClientTransportT,
|
|
7
|
+
SessionKwargs,
|
|
8
|
+
)
|
|
9
|
+
from fastmcp.client.transports.config import MCPConfigTransport
|
|
10
|
+
from fastmcp.client.transports.http import StreamableHttpTransport
|
|
11
|
+
from fastmcp.client.transports.inference import infer_transport
|
|
12
|
+
from fastmcp.client.transports.sse import SSETransport
|
|
13
|
+
from fastmcp.client.transports.memory import FastMCPTransport
|
|
14
|
+
from fastmcp.client.transports.stdio import (
|
|
15
|
+
FastMCPStdioTransport,
|
|
16
|
+
NodeStdioTransport,
|
|
17
|
+
NpxStdioTransport,
|
|
18
|
+
PythonStdioTransport,
|
|
19
|
+
StdioTransport,
|
|
20
|
+
UvStdioTransport,
|
|
21
|
+
UvxStdioTransport,
|
|
22
|
+
)
|
|
23
|
+
from fastmcp.server.server import FastMCP
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ClientTransport",
|
|
27
|
+
"FastMCPStdioTransport",
|
|
28
|
+
"FastMCPTransport",
|
|
29
|
+
"NodeStdioTransport",
|
|
30
|
+
"NpxStdioTransport",
|
|
31
|
+
"PythonStdioTransport",
|
|
32
|
+
"SSETransport",
|
|
33
|
+
"StdioTransport",
|
|
34
|
+
"StreamableHttpTransport",
|
|
35
|
+
"UvStdioTransport",
|
|
36
|
+
"UvxStdioTransport",
|
|
37
|
+
"infer_transport",
|
|
38
|
+
]
|