fastmcp 2.14.0__py3-none-any.whl → 2.14.2__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/client/client.py +79 -12
- fastmcp/client/sampling/__init__.py +69 -0
- fastmcp/client/sampling/handlers/__init__.py +0 -0
- fastmcp/client/sampling/handlers/anthropic.py +387 -0
- fastmcp/client/sampling/handlers/openai.py +399 -0
- fastmcp/client/tasks.py +0 -63
- fastmcp/client/transports.py +35 -16
- fastmcp/experimental/sampling/handlers/__init__.py +5 -0
- fastmcp/experimental/sampling/handlers/openai.py +4 -169
- fastmcp/prompts/prompt.py +5 -5
- fastmcp/prompts/prompt_manager.py +3 -4
- fastmcp/resources/resource.py +4 -4
- fastmcp/resources/resource_manager.py +9 -14
- fastmcp/resources/template.py +5 -5
- fastmcp/server/auth/auth.py +20 -5
- fastmcp/server/auth/oauth_proxy.py +73 -15
- fastmcp/server/auth/providers/supabase.py +11 -6
- fastmcp/server/context.py +448 -113
- fastmcp/server/dependencies.py +5 -0
- fastmcp/server/elicitation.py +7 -3
- fastmcp/server/middleware/error_handling.py +1 -1
- fastmcp/server/openapi/components.py +2 -4
- fastmcp/server/proxy.py +3 -3
- fastmcp/server/sampling/__init__.py +10 -0
- fastmcp/server/sampling/run.py +301 -0
- fastmcp/server/sampling/sampling_tool.py +108 -0
- fastmcp/server/server.py +84 -78
- fastmcp/server/tasks/converters.py +2 -1
- fastmcp/tools/tool.py +8 -6
- fastmcp/tools/tool_manager.py +5 -7
- fastmcp/utilities/cli.py +23 -43
- fastmcp/utilities/json_schema.py +40 -0
- fastmcp/utilities/openapi/schemas.py +4 -4
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/METADATA +8 -3
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/RECORD +38 -34
- fastmcp/client/sampling.py +0 -56
- fastmcp/experimental/sampling/handlers/base.py +0 -21
- fastmcp/server/sampling/handler.py +0 -19
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/dependencies.py
CHANGED
|
@@ -21,6 +21,7 @@ from mcp.server.auth.provider import (
|
|
|
21
21
|
from mcp.server.lowlevel.server import request_ctx
|
|
22
22
|
from starlette.requests import Request
|
|
23
23
|
|
|
24
|
+
from fastmcp.exceptions import FastMCPError
|
|
24
25
|
from fastmcp.server.auth import AccessToken
|
|
25
26
|
from fastmcp.server.http import _current_http_request
|
|
26
27
|
from fastmcp.utilities.types import is_class_member_of_type
|
|
@@ -188,6 +189,10 @@ async def _resolve_fastmcp_dependencies(
|
|
|
188
189
|
resolved[parameter] = await stack.enter_async_context(
|
|
189
190
|
dependency
|
|
190
191
|
)
|
|
192
|
+
except FastMCPError:
|
|
193
|
+
# Let FastMCPError subclasses (ToolError, ResourceError, etc.)
|
|
194
|
+
# propagate unchanged so they can be handled appropriately
|
|
195
|
+
raise
|
|
191
196
|
except Exception as error:
|
|
192
197
|
fn_name = getattr(fn, "__name__", repr(fn))
|
|
193
198
|
raise RuntimeError(
|
fastmcp/server/elicitation.py
CHANGED
|
@@ -304,15 +304,19 @@ def _dict_to_enum_schema(
|
|
|
304
304
|
multi_select: If True, use anyOf pattern; if False, use oneOf pattern
|
|
305
305
|
|
|
306
306
|
Returns:
|
|
307
|
-
{"
|
|
308
|
-
{"anyOf": [
|
|
307
|
+
{"type": "string", "oneOf": [...]} for single-select
|
|
308
|
+
{"anyOf": [...]} for multi-select (used as array items)
|
|
309
309
|
"""
|
|
310
310
|
pattern_key = "anyOf" if multi_select else "oneOf"
|
|
311
311
|
pattern = []
|
|
312
312
|
for value, metadata in enum_dict.items():
|
|
313
313
|
title = metadata.get("title", value)
|
|
314
314
|
pattern.append({"const": value, "title": title})
|
|
315
|
-
|
|
315
|
+
|
|
316
|
+
result: dict[str, Any] = {pattern_key: pattern}
|
|
317
|
+
if not multi_select:
|
|
318
|
+
result["type"] = "string"
|
|
319
|
+
return result
|
|
316
320
|
|
|
317
321
|
|
|
318
322
|
def get_elicitation_schema(response_type: type[T]) -> dict[str, Any]:
|
|
@@ -87,7 +87,7 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
87
87
|
return error
|
|
88
88
|
|
|
89
89
|
# Map common exceptions to appropriate MCP error codes
|
|
90
|
-
error_type = type(error)
|
|
90
|
+
error_type = type(error.__cause__) if error.__cause__ else type(error)
|
|
91
91
|
|
|
92
92
|
if error_type in (ValueError, TypeError):
|
|
93
93
|
return McpError(
|
|
@@ -64,10 +64,8 @@ class OpenAPITool(Tool):
|
|
|
64
64
|
try:
|
|
65
65
|
# Get base URL from client
|
|
66
66
|
base_url = (
|
|
67
|
-
str(self._client.base_url)
|
|
68
|
-
|
|
69
|
-
else "http://localhost"
|
|
70
|
-
)
|
|
67
|
+
str(self._client.base_url) if hasattr(self._client, "base_url") else ""
|
|
68
|
+
) or "http://localhost"
|
|
71
69
|
|
|
72
70
|
# Get Headers from client
|
|
73
71
|
cli_headers = (
|
fastmcp/server/proxy.py
CHANGED
|
@@ -589,15 +589,15 @@ class ProxyClient(Client[ClientTransportT]):
|
|
|
589
589
|
A handler that forwards the sampling request from the remote server to the proxy's connected clients and relays the response back to the remote server.
|
|
590
590
|
"""
|
|
591
591
|
ctx = get_context()
|
|
592
|
-
|
|
592
|
+
result = await ctx.sample(
|
|
593
593
|
list(messages),
|
|
594
594
|
system_prompt=params.systemPrompt,
|
|
595
595
|
temperature=params.temperature,
|
|
596
596
|
max_tokens=params.maxTokens,
|
|
597
597
|
model_preferences=params.modelPreferences,
|
|
598
598
|
)
|
|
599
|
-
|
|
600
|
-
|
|
599
|
+
# Create TextContent from the result text
|
|
600
|
+
content = mcp.types.TextContent(type="text", text=result.text or "")
|
|
601
601
|
return mcp.types.CreateMessageResult(
|
|
602
602
|
role="assistant",
|
|
603
603
|
model="fastmcp-client",
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Sampling types and helper functions for FastMCP servers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Generic
|
|
8
|
+
|
|
9
|
+
from mcp.types import (
|
|
10
|
+
ClientCapabilities,
|
|
11
|
+
CreateMessageResult,
|
|
12
|
+
CreateMessageResultWithTools,
|
|
13
|
+
ModelHint,
|
|
14
|
+
ModelPreferences,
|
|
15
|
+
SamplingCapability,
|
|
16
|
+
SamplingMessage,
|
|
17
|
+
SamplingToolsCapability,
|
|
18
|
+
TextContent,
|
|
19
|
+
ToolChoice,
|
|
20
|
+
ToolResultContent,
|
|
21
|
+
ToolUseContent,
|
|
22
|
+
)
|
|
23
|
+
from mcp.types import CreateMessageRequestParams as SamplingParams
|
|
24
|
+
from mcp.types import Tool as SDKTool
|
|
25
|
+
from typing_extensions import TypeVar
|
|
26
|
+
|
|
27
|
+
from fastmcp.exceptions import ToolError
|
|
28
|
+
from fastmcp.server.sampling.sampling_tool import SamplingTool
|
|
29
|
+
from fastmcp.utilities.logging import get_logger
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from fastmcp.server.context import Context
|
|
35
|
+
|
|
36
|
+
ResultT = TypeVar("ResultT", default=str)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class SamplingResult(Generic[ResultT]):
|
|
41
|
+
"""Result of a sampling operation.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
text: The text representation of the result (raw text or JSON for structured).
|
|
45
|
+
result: The typed result (str for text, parsed object for structured output).
|
|
46
|
+
history: All messages exchanged during sampling.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
text: str | None
|
|
50
|
+
result: ResultT
|
|
51
|
+
history: list[SamplingMessage]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class SampleStep:
|
|
56
|
+
"""Result of a single sampling call.
|
|
57
|
+
|
|
58
|
+
Represents what the LLM returned in this step plus the message history.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
response: CreateMessageResult | CreateMessageResultWithTools
|
|
62
|
+
history: list[SamplingMessage]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_tool_use(self) -> bool:
|
|
66
|
+
"""True if the LLM is requesting tool execution."""
|
|
67
|
+
if isinstance(self.response, CreateMessageResultWithTools):
|
|
68
|
+
return self.response.stopReason == "toolUse"
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def text(self) -> str | None:
|
|
73
|
+
"""Extract text from the response, if available."""
|
|
74
|
+
content = self.response.content
|
|
75
|
+
if isinstance(content, list):
|
|
76
|
+
for block in content:
|
|
77
|
+
if isinstance(block, TextContent):
|
|
78
|
+
return block.text
|
|
79
|
+
return None
|
|
80
|
+
elif isinstance(content, TextContent):
|
|
81
|
+
return content.text
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def tool_calls(self) -> list[ToolUseContent]:
|
|
86
|
+
"""Get the list of tool calls from the response."""
|
|
87
|
+
content = self.response.content
|
|
88
|
+
if isinstance(content, list):
|
|
89
|
+
return [c for c in content if isinstance(c, ToolUseContent)]
|
|
90
|
+
elif isinstance(content, ToolUseContent):
|
|
91
|
+
return [content]
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _parse_model_preferences(
|
|
96
|
+
model_preferences: ModelPreferences | str | list[str] | None,
|
|
97
|
+
) -> ModelPreferences | None:
|
|
98
|
+
"""Convert model preferences to ModelPreferences object."""
|
|
99
|
+
if model_preferences is None:
|
|
100
|
+
return None
|
|
101
|
+
elif isinstance(model_preferences, ModelPreferences):
|
|
102
|
+
return model_preferences
|
|
103
|
+
elif isinstance(model_preferences, str):
|
|
104
|
+
return ModelPreferences(hints=[ModelHint(name=model_preferences)])
|
|
105
|
+
elif isinstance(model_preferences, list):
|
|
106
|
+
if not all(isinstance(h, str) for h in model_preferences):
|
|
107
|
+
raise ValueError("All elements of model_preferences list must be strings.")
|
|
108
|
+
return ModelPreferences(hints=[ModelHint(name=h) for h in model_preferences])
|
|
109
|
+
else:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"model_preferences must be one of: ModelPreferences, str, list[str], or None."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# --- Standalone functions for sample_step() ---
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def determine_handler_mode(context: Context, needs_tools: bool) -> bool:
|
|
119
|
+
"""Determine whether to use fallback handler or client for sampling.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
context: The MCP context.
|
|
123
|
+
needs_tools: Whether the sampling request requires tool support.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if fallback handler should be used, False to use client.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: If client lacks required capability and no fallback configured.
|
|
130
|
+
"""
|
|
131
|
+
fastmcp = context.fastmcp
|
|
132
|
+
session = context.session
|
|
133
|
+
|
|
134
|
+
# Check what capabilities the client has
|
|
135
|
+
has_sampling = session.check_client_capability(
|
|
136
|
+
capability=ClientCapabilities(sampling=SamplingCapability())
|
|
137
|
+
)
|
|
138
|
+
has_tools_capability = session.check_client_capability(
|
|
139
|
+
capability=ClientCapabilities(
|
|
140
|
+
sampling=SamplingCapability(tools=SamplingToolsCapability())
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if fastmcp.sampling_handler_behavior == "always":
|
|
145
|
+
if fastmcp.sampling_handler is None:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
"sampling_handler_behavior is 'always' but no handler configured"
|
|
148
|
+
)
|
|
149
|
+
return True
|
|
150
|
+
elif fastmcp.sampling_handler_behavior == "fallback":
|
|
151
|
+
client_sufficient = has_sampling and (not needs_tools or has_tools_capability)
|
|
152
|
+
if not client_sufficient:
|
|
153
|
+
if fastmcp.sampling_handler is None:
|
|
154
|
+
if needs_tools and has_sampling and not has_tools_capability:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
"Client does not support sampling with tools. "
|
|
157
|
+
"The client must advertise the sampling.tools capability."
|
|
158
|
+
)
|
|
159
|
+
raise ValueError("Client does not support sampling")
|
|
160
|
+
return True
|
|
161
|
+
elif fastmcp.sampling_handler_behavior is not None:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"Invalid sampling_handler_behavior: {fastmcp.sampling_handler_behavior!r}. "
|
|
164
|
+
"Must be 'always', 'fallback', or None."
|
|
165
|
+
)
|
|
166
|
+
elif not has_sampling:
|
|
167
|
+
raise ValueError("Client does not support sampling")
|
|
168
|
+
elif needs_tools and not has_tools_capability:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
"Client does not support sampling with tools. "
|
|
171
|
+
"The client must advertise the sampling.tools capability."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def call_sampling_handler(
|
|
178
|
+
context: Context,
|
|
179
|
+
messages: list[SamplingMessage],
|
|
180
|
+
*,
|
|
181
|
+
system_prompt: str | None,
|
|
182
|
+
temperature: float | None,
|
|
183
|
+
max_tokens: int,
|
|
184
|
+
model_preferences: ModelPreferences | str | list[str] | None,
|
|
185
|
+
sdk_tools: list[SDKTool] | None,
|
|
186
|
+
tool_choice: ToolChoice | None,
|
|
187
|
+
) -> CreateMessageResult | CreateMessageResultWithTools:
|
|
188
|
+
"""Make LLM call using the fallback handler.
|
|
189
|
+
|
|
190
|
+
Note: This function expects the caller (sample_step) to have validated that
|
|
191
|
+
sampling_handler is set via determine_handler_mode(). The checks below are
|
|
192
|
+
safeguards against internal misuse.
|
|
193
|
+
"""
|
|
194
|
+
if context.fastmcp.sampling_handler is None:
|
|
195
|
+
raise RuntimeError("sampling_handler is None")
|
|
196
|
+
if context.request_context is None:
|
|
197
|
+
raise RuntimeError("request_context is None")
|
|
198
|
+
|
|
199
|
+
result = context.fastmcp.sampling_handler(
|
|
200
|
+
messages,
|
|
201
|
+
SamplingParams(
|
|
202
|
+
systemPrompt=system_prompt,
|
|
203
|
+
messages=messages,
|
|
204
|
+
temperature=temperature,
|
|
205
|
+
maxTokens=max_tokens,
|
|
206
|
+
modelPreferences=_parse_model_preferences(model_preferences),
|
|
207
|
+
tools=sdk_tools,
|
|
208
|
+
toolChoice=tool_choice,
|
|
209
|
+
),
|
|
210
|
+
context.request_context,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if inspect.isawaitable(result):
|
|
214
|
+
result = await result
|
|
215
|
+
|
|
216
|
+
# Convert string to CreateMessageResult
|
|
217
|
+
if isinstance(result, str):
|
|
218
|
+
return CreateMessageResult(
|
|
219
|
+
role="assistant",
|
|
220
|
+
content=TextContent(type="text", text=result),
|
|
221
|
+
model="unknown",
|
|
222
|
+
stopReason="endTurn",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def execute_tools(
|
|
229
|
+
tool_calls: list[ToolUseContent],
|
|
230
|
+
tool_map: dict[str, SamplingTool],
|
|
231
|
+
mask_error_details: bool = False,
|
|
232
|
+
) -> list[ToolResultContent]:
|
|
233
|
+
"""Execute tool calls and return results.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
tool_calls: List of tool use requests from the LLM.
|
|
237
|
+
tool_map: Mapping from tool name to SamplingTool.
|
|
238
|
+
mask_error_details: If True, mask detailed error messages from tool execution.
|
|
239
|
+
When masked, only generic error messages are returned to the LLM.
|
|
240
|
+
Tools can explicitly raise ToolError to bypass masking when they want
|
|
241
|
+
to provide specific error messages to the LLM.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
List of tool result content blocks.
|
|
245
|
+
"""
|
|
246
|
+
tool_results: list[ToolResultContent] = []
|
|
247
|
+
|
|
248
|
+
for tool_use in tool_calls:
|
|
249
|
+
tool = tool_map.get(tool_use.name)
|
|
250
|
+
if tool is None:
|
|
251
|
+
tool_results.append(
|
|
252
|
+
ToolResultContent(
|
|
253
|
+
type="tool_result",
|
|
254
|
+
toolUseId=tool_use.id,
|
|
255
|
+
content=[
|
|
256
|
+
TextContent(
|
|
257
|
+
type="text",
|
|
258
|
+
text=f"Error: Unknown tool '{tool_use.name}'",
|
|
259
|
+
)
|
|
260
|
+
],
|
|
261
|
+
isError=True,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
try:
|
|
266
|
+
result_value = await tool.run(tool_use.input)
|
|
267
|
+
tool_results.append(
|
|
268
|
+
ToolResultContent(
|
|
269
|
+
type="tool_result",
|
|
270
|
+
toolUseId=tool_use.id,
|
|
271
|
+
content=[TextContent(type="text", text=str(result_value))],
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
except ToolError as e:
|
|
275
|
+
# ToolError is the escape hatch - always pass message through
|
|
276
|
+
logger.exception(f"Error calling sampling tool '{tool_use.name}'")
|
|
277
|
+
tool_results.append(
|
|
278
|
+
ToolResultContent(
|
|
279
|
+
type="tool_result",
|
|
280
|
+
toolUseId=tool_use.id,
|
|
281
|
+
content=[TextContent(type="text", text=str(e))],
|
|
282
|
+
isError=True,
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
# Generic exceptions - mask based on setting
|
|
287
|
+
logger.exception(f"Error calling sampling tool '{tool_use.name}'")
|
|
288
|
+
if mask_error_details:
|
|
289
|
+
error_text = f"Error executing tool '{tool_use.name}'"
|
|
290
|
+
else:
|
|
291
|
+
error_text = f"Error executing tool '{tool_use.name}': {e}"
|
|
292
|
+
tool_results.append(
|
|
293
|
+
ToolResultContent(
|
|
294
|
+
type="tool_result",
|
|
295
|
+
toolUseId=tool_use.id,
|
|
296
|
+
content=[TextContent(type="text", text=error_text)],
|
|
297
|
+
isError=True,
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return tool_results
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""SamplingTool for use during LLM sampling requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from mcp.types import Tool as SDKTool
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
|
|
12
|
+
from fastmcp.tools.tool import ParsedFunction
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SamplingTool(BaseModel):
|
|
16
|
+
"""A tool that can be used during LLM sampling.
|
|
17
|
+
|
|
18
|
+
SamplingTools bundle a tool's schema (name, description, parameters) with
|
|
19
|
+
an executor function, enabling servers to execute agentic workflows where
|
|
20
|
+
the LLM can request tool calls during sampling.
|
|
21
|
+
|
|
22
|
+
In most cases, pass functions directly to ctx.sample():
|
|
23
|
+
|
|
24
|
+
def search(query: str) -> str:
|
|
25
|
+
'''Search the web.'''
|
|
26
|
+
return web_search(query)
|
|
27
|
+
|
|
28
|
+
result = await context.sample(
|
|
29
|
+
messages="Find info about Python",
|
|
30
|
+
tools=[search], # Plain functions work directly
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
Create a SamplingTool explicitly when you need custom name/description:
|
|
34
|
+
|
|
35
|
+
tool = SamplingTool.from_function(search, name="web_search")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
description: str | None = None
|
|
40
|
+
parameters: dict[str, Any]
|
|
41
|
+
fn: Callable[..., Any]
|
|
42
|
+
|
|
43
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
44
|
+
|
|
45
|
+
async def run(self, arguments: dict[str, Any] | None = None) -> Any:
|
|
46
|
+
"""Execute the tool with the given arguments.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
arguments: Dictionary of arguments to pass to the tool function.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The result of executing the tool function.
|
|
53
|
+
"""
|
|
54
|
+
if arguments is None:
|
|
55
|
+
arguments = {}
|
|
56
|
+
|
|
57
|
+
result = self.fn(**arguments)
|
|
58
|
+
if inspect.isawaitable(result):
|
|
59
|
+
result = await result
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
def _to_sdk_tool(self) -> SDKTool:
|
|
63
|
+
"""Convert to an mcp.types.Tool for SDK compatibility.
|
|
64
|
+
|
|
65
|
+
This is used internally when passing tools to the MCP SDK's
|
|
66
|
+
create_message() method.
|
|
67
|
+
"""
|
|
68
|
+
return SDKTool(
|
|
69
|
+
name=self.name,
|
|
70
|
+
description=self.description,
|
|
71
|
+
inputSchema=self.parameters,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_function(
|
|
76
|
+
cls,
|
|
77
|
+
fn: Callable[..., Any],
|
|
78
|
+
*,
|
|
79
|
+
name: str | None = None,
|
|
80
|
+
description: str | None = None,
|
|
81
|
+
) -> SamplingTool:
|
|
82
|
+
"""Create a SamplingTool from a function.
|
|
83
|
+
|
|
84
|
+
The function's signature is analyzed to generate a JSON schema for
|
|
85
|
+
the tool's parameters. Type hints are used to determine parameter types.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
fn: The function to create a tool from.
|
|
89
|
+
name: Optional name override. Defaults to the function's name.
|
|
90
|
+
description: Optional description override. Defaults to the function's docstring.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A SamplingTool wrapping the function.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: If the function is a lambda without a name override.
|
|
97
|
+
"""
|
|
98
|
+
parsed = ParsedFunction.from_function(fn, validate=True)
|
|
99
|
+
|
|
100
|
+
if name is None and parsed.name == "<lambda>":
|
|
101
|
+
raise ValueError("You must provide a name for lambda functions")
|
|
102
|
+
|
|
103
|
+
return cls(
|
|
104
|
+
name=name or parsed.name,
|
|
105
|
+
description=description or parsed.description,
|
|
106
|
+
parameters=parsed.input_schema,
|
|
107
|
+
fn=parsed.fn,
|
|
108
|
+
)
|