fastmcp 2.9.1__py3-none-any.whl → 2.10.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/cli/cli.py +16 -1
- fastmcp/cli/run.py +4 -0
- fastmcp/client/auth/oauth.py +5 -82
- fastmcp/client/client.py +114 -24
- fastmcp/client/elicitation.py +63 -0
- fastmcp/client/transports.py +50 -36
- fastmcp/contrib/component_manager/README.md +170 -0
- fastmcp/contrib/component_manager/__init__.py +4 -0
- fastmcp/contrib/component_manager/component_manager.py +186 -0
- fastmcp/contrib/component_manager/component_service.py +225 -0
- fastmcp/contrib/component_manager/example.py +59 -0
- fastmcp/prompts/prompt.py +12 -4
- fastmcp/resources/resource.py +8 -3
- fastmcp/resources/template.py +5 -0
- fastmcp/server/auth/auth.py +15 -0
- fastmcp/server/auth/providers/bearer.py +41 -3
- fastmcp/server/auth/providers/bearer_env.py +4 -0
- fastmcp/server/auth/providers/in_memory.py +15 -0
- fastmcp/server/context.py +144 -4
- fastmcp/server/elicitation.py +160 -0
- fastmcp/server/http.py +1 -9
- fastmcp/server/low_level.py +4 -2
- fastmcp/server/middleware/__init__.py +14 -1
- fastmcp/server/middleware/logging.py +11 -0
- fastmcp/server/middleware/middleware.py +10 -6
- fastmcp/server/openapi.py +19 -77
- fastmcp/server/proxy.py +13 -6
- fastmcp/server/server.py +76 -11
- fastmcp/settings.py +0 -17
- fastmcp/tools/tool.py +209 -57
- fastmcp/tools/tool_manager.py +2 -3
- fastmcp/tools/tool_transform.py +125 -26
- fastmcp/utilities/cli.py +106 -0
- fastmcp/utilities/components.py +5 -1
- fastmcp/utilities/json_schema_type.py +648 -0
- fastmcp/utilities/openapi.py +69 -0
- fastmcp/utilities/types.py +50 -19
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/METADATA +3 -2
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/RECORD +42 -33
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/tools/tool.py
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
import json
|
|
5
4
|
from collections.abc import Callable
|
|
6
5
|
from dataclasses import dataclass
|
|
7
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from typing import TYPE_CHECKING, Annotated, Any, Generic, Literal, TypeVar
|
|
8
7
|
|
|
8
|
+
import mcp.types
|
|
9
9
|
import pydantic_core
|
|
10
|
-
from mcp.types import TextContent, ToolAnnotations
|
|
10
|
+
from mcp.types import ContentBlock, TextContent, ToolAnnotations
|
|
11
11
|
from mcp.types import Tool as MCPTool
|
|
12
|
-
from pydantic import Field
|
|
12
|
+
from pydantic import Field, PydanticSchemaGenerationError
|
|
13
13
|
|
|
14
|
-
import fastmcp
|
|
15
14
|
from fastmcp.server.dependencies import get_context
|
|
16
15
|
from fastmcp.utilities.components import FastMCPComponent
|
|
17
16
|
from fastmcp.utilities.json_schema import compress_schema
|
|
@@ -20,9 +19,11 @@ from fastmcp.utilities.types import (
|
|
|
20
19
|
Audio,
|
|
21
20
|
File,
|
|
22
21
|
Image,
|
|
23
|
-
|
|
22
|
+
NotSet,
|
|
23
|
+
NotSetT,
|
|
24
24
|
find_kwarg_by_type,
|
|
25
25
|
get_cached_typeadapter,
|
|
26
|
+
replace_type,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
if TYPE_CHECKING:
|
|
@@ -30,21 +31,80 @@ if TYPE_CHECKING:
|
|
|
30
31
|
|
|
31
32
|
logger = get_logger(__name__)
|
|
32
33
|
|
|
34
|
+
T = TypeVar("T")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class _WrappedResult(Generic[T]):
|
|
39
|
+
"""Generic wrapper for non-object return types."""
|
|
40
|
+
|
|
41
|
+
result: T
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _UnserializableType:
|
|
45
|
+
pass
|
|
46
|
+
|
|
33
47
|
|
|
34
48
|
def default_serializer(data: Any) -> str:
|
|
35
49
|
return pydantic_core.to_json(data, fallback=str, indent=2).decode()
|
|
36
50
|
|
|
37
51
|
|
|
52
|
+
class ToolResult:
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
content: list[ContentBlock] | Any | None = None,
|
|
56
|
+
structured_content: dict[str, Any] | Any | None = None,
|
|
57
|
+
):
|
|
58
|
+
if content is None and structured_content is None:
|
|
59
|
+
raise ValueError("Either content or structured_content must be provided")
|
|
60
|
+
elif content is None:
|
|
61
|
+
content = structured_content
|
|
62
|
+
|
|
63
|
+
self.content = _convert_to_content(content)
|
|
64
|
+
|
|
65
|
+
if structured_content is not None:
|
|
66
|
+
try:
|
|
67
|
+
structured_content = pydantic_core.to_jsonable_python(
|
|
68
|
+
structured_content
|
|
69
|
+
)
|
|
70
|
+
except pydantic_core.PydanticSerializationError as e:
|
|
71
|
+
logger.error(
|
|
72
|
+
f"Could not serialize structured content. If this is unexpected, set your tool's output_schema to None to disable automatic serialization: {e}"
|
|
73
|
+
)
|
|
74
|
+
raise
|
|
75
|
+
if not isinstance(structured_content, dict):
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"structured_content must be a dict or None. "
|
|
78
|
+
f"Got {type(structured_content).__name__}: {structured_content!r}. "
|
|
79
|
+
"Tools should wrap non-dict values based on their output_schema."
|
|
80
|
+
)
|
|
81
|
+
self.structured_content: dict[str, Any] | None = structured_content
|
|
82
|
+
|
|
83
|
+
def to_mcp_result(
|
|
84
|
+
self,
|
|
85
|
+
) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]:
|
|
86
|
+
if self.structured_content is None:
|
|
87
|
+
return self.content
|
|
88
|
+
return self.content, self.structured_content
|
|
89
|
+
|
|
90
|
+
|
|
38
91
|
class Tool(FastMCPComponent):
|
|
39
92
|
"""Internal tool registration info."""
|
|
40
93
|
|
|
41
|
-
parameters:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
94
|
+
parameters: Annotated[
|
|
95
|
+
dict[str, Any], Field(description="JSON schema for tool parameters")
|
|
96
|
+
]
|
|
97
|
+
output_schema: Annotated[
|
|
98
|
+
dict[str, Any] | None, Field(description="JSON schema for tool output")
|
|
99
|
+
] = None
|
|
100
|
+
annotations: Annotated[
|
|
101
|
+
ToolAnnotations | None,
|
|
102
|
+
Field(description="Additional annotations about the tool"),
|
|
103
|
+
] = None
|
|
104
|
+
serializer: Annotated[
|
|
105
|
+
Callable[[Any], str] | None,
|
|
106
|
+
Field(description="Optional custom serializer for tool results"),
|
|
107
|
+
] = None
|
|
48
108
|
|
|
49
109
|
def enable(self) -> None:
|
|
50
110
|
super().enable()
|
|
@@ -63,11 +123,20 @@ class Tool(FastMCPComponent):
|
|
|
63
123
|
pass # No context available
|
|
64
124
|
|
|
65
125
|
def to_mcp_tool(self, **overrides: Any) -> MCPTool:
|
|
126
|
+
if self.title:
|
|
127
|
+
title = self.title
|
|
128
|
+
elif self.annotations and self.annotations.title:
|
|
129
|
+
title = self.annotations.title
|
|
130
|
+
else:
|
|
131
|
+
title = None
|
|
132
|
+
|
|
66
133
|
kwargs = {
|
|
67
134
|
"name": self.name,
|
|
68
135
|
"description": self.description,
|
|
69
136
|
"inputSchema": self.parameters,
|
|
137
|
+
"outputSchema": self.output_schema,
|
|
70
138
|
"annotations": self.annotations,
|
|
139
|
+
"title": title,
|
|
71
140
|
}
|
|
72
141
|
return MCPTool(**kwargs | overrides)
|
|
73
142
|
|
|
@@ -75,10 +144,12 @@ class Tool(FastMCPComponent):
|
|
|
75
144
|
def from_function(
|
|
76
145
|
fn: Callable[..., Any],
|
|
77
146
|
name: str | None = None,
|
|
147
|
+
title: str | None = None,
|
|
78
148
|
description: str | None = None,
|
|
79
149
|
tags: set[str] | None = None,
|
|
80
150
|
annotations: ToolAnnotations | None = None,
|
|
81
151
|
exclude_args: list[str] | None = None,
|
|
152
|
+
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
82
153
|
serializer: Callable[[Any], str] | None = None,
|
|
83
154
|
enabled: bool | None = None,
|
|
84
155
|
) -> FunctionTool:
|
|
@@ -86,16 +157,26 @@ class Tool(FastMCPComponent):
|
|
|
86
157
|
return FunctionTool.from_function(
|
|
87
158
|
fn=fn,
|
|
88
159
|
name=name,
|
|
160
|
+
title=title,
|
|
89
161
|
description=description,
|
|
90
162
|
tags=tags,
|
|
91
163
|
annotations=annotations,
|
|
92
164
|
exclude_args=exclude_args,
|
|
165
|
+
output_schema=output_schema,
|
|
93
166
|
serializer=serializer,
|
|
94
167
|
enabled=enabled,
|
|
95
168
|
)
|
|
96
169
|
|
|
97
|
-
async def run(self, arguments: dict[str, Any]) ->
|
|
98
|
-
"""
|
|
170
|
+
async def run(self, arguments: dict[str, Any]) -> ToolResult:
|
|
171
|
+
"""
|
|
172
|
+
Run the tool with arguments.
|
|
173
|
+
|
|
174
|
+
This method is not implemented in the base Tool class and must be
|
|
175
|
+
implemented by subclasses.
|
|
176
|
+
|
|
177
|
+
`run()` can EITHER return a list of ContentBlocks, or a tuple of
|
|
178
|
+
(list of ContentBlocks, dict of structured output).
|
|
179
|
+
"""
|
|
99
180
|
raise NotImplementedError("Subclasses must implement run()")
|
|
100
181
|
|
|
101
182
|
@classmethod
|
|
@@ -108,6 +189,7 @@ class Tool(FastMCPComponent):
|
|
|
108
189
|
description: str | None = None,
|
|
109
190
|
tags: set[str] | None = None,
|
|
110
191
|
annotations: ToolAnnotations | None = None,
|
|
192
|
+
output_schema: dict[str, Any] | None | Literal[False] = None,
|
|
111
193
|
serializer: Callable[[Any], str] | None = None,
|
|
112
194
|
enabled: bool | None = None,
|
|
113
195
|
) -> TransformedTool:
|
|
@@ -121,6 +203,7 @@ class Tool(FastMCPComponent):
|
|
|
121
203
|
description=description,
|
|
122
204
|
tags=tags,
|
|
123
205
|
annotations=annotations,
|
|
206
|
+
output_schema=output_schema,
|
|
124
207
|
serializer=serializer,
|
|
125
208
|
enabled=enabled,
|
|
126
209
|
)
|
|
@@ -134,10 +217,12 @@ class FunctionTool(Tool):
|
|
|
134
217
|
cls,
|
|
135
218
|
fn: Callable[..., Any],
|
|
136
219
|
name: str | None = None,
|
|
220
|
+
title: str | None = None,
|
|
137
221
|
description: str | None = None,
|
|
138
222
|
tags: set[str] | None = None,
|
|
139
223
|
annotations: ToolAnnotations | None = None,
|
|
140
224
|
exclude_args: list[str] | None = None,
|
|
225
|
+
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
141
226
|
serializer: Callable[[Any], str] | None = None,
|
|
142
227
|
enabled: bool | None = None,
|
|
143
228
|
) -> FunctionTool:
|
|
@@ -148,18 +233,33 @@ class FunctionTool(Tool):
|
|
|
148
233
|
if name is None and parsed_fn.name == "<lambda>":
|
|
149
234
|
raise ValueError("You must provide a name for lambda functions")
|
|
150
235
|
|
|
236
|
+
if isinstance(output_schema, NotSetT):
|
|
237
|
+
output_schema = parsed_fn.output_schema
|
|
238
|
+
elif output_schema is False:
|
|
239
|
+
output_schema = None
|
|
240
|
+
# Note: explicit schemas (dict) are used as-is without auto-wrapping
|
|
241
|
+
|
|
242
|
+
# Validate that explicit schemas are object type for structured content
|
|
243
|
+
if output_schema is not None and isinstance(output_schema, dict):
|
|
244
|
+
if output_schema.get("type") != "object":
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f'Output schemas must have "type" set to "object" due to MCP spec limitations. Received: {output_schema!r}'
|
|
247
|
+
)
|
|
248
|
+
|
|
151
249
|
return cls(
|
|
152
250
|
fn=parsed_fn.fn,
|
|
153
251
|
name=name or parsed_fn.name,
|
|
252
|
+
title=title,
|
|
154
253
|
description=description or parsed_fn.description,
|
|
155
|
-
parameters=parsed_fn.
|
|
156
|
-
|
|
254
|
+
parameters=parsed_fn.input_schema,
|
|
255
|
+
output_schema=output_schema,
|
|
157
256
|
annotations=annotations,
|
|
257
|
+
tags=tags or set(),
|
|
158
258
|
serializer=serializer,
|
|
159
259
|
enabled=enabled if enabled is not None else True,
|
|
160
260
|
)
|
|
161
261
|
|
|
162
|
-
async def run(self, arguments: dict[str, Any]) ->
|
|
262
|
+
async def run(self, arguments: dict[str, Any]) -> ToolResult:
|
|
163
263
|
"""Run the tool with arguments."""
|
|
164
264
|
from fastmcp.server.context import Context
|
|
165
265
|
|
|
@@ -169,41 +269,39 @@ class FunctionTool(Tool):
|
|
|
169
269
|
if context_kwarg and context_kwarg not in arguments:
|
|
170
270
|
arguments[context_kwarg] = get_context()
|
|
171
271
|
|
|
172
|
-
if fastmcp.settings.tool_attempt_parse_json_args:
|
|
173
|
-
# Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
|
|
174
|
-
# being passed in as JSON inside a string rather than an actual list.
|
|
175
|
-
#
|
|
176
|
-
# Claude desktop is prone to this - in fact it seems incapable of NOT doing
|
|
177
|
-
# this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings,
|
|
178
|
-
# which can be pre-parsed here.
|
|
179
|
-
signature = inspect.signature(self.fn)
|
|
180
|
-
for param_name in self.parameters["properties"]:
|
|
181
|
-
arg = arguments.get(param_name, None)
|
|
182
|
-
# if not in signature, we won't have annotations, so skip logic
|
|
183
|
-
if param_name not in signature.parameters:
|
|
184
|
-
continue
|
|
185
|
-
# if not a string, we won't have a JSON to parse, so skip logic
|
|
186
|
-
if not isinstance(arg, str):
|
|
187
|
-
continue
|
|
188
|
-
# skip if the type is a simple type (int, float, bool)
|
|
189
|
-
if signature.parameters[param_name].annotation in (
|
|
190
|
-
int,
|
|
191
|
-
float,
|
|
192
|
-
bool,
|
|
193
|
-
):
|
|
194
|
-
continue
|
|
195
|
-
try:
|
|
196
|
-
arguments[param_name] = json.loads(arg)
|
|
197
|
-
|
|
198
|
-
except json.JSONDecodeError:
|
|
199
|
-
pass
|
|
200
|
-
|
|
201
272
|
type_adapter = get_cached_typeadapter(self.fn)
|
|
202
273
|
result = type_adapter.validate_python(arguments)
|
|
274
|
+
|
|
203
275
|
if inspect.isawaitable(result):
|
|
204
276
|
result = await result
|
|
205
277
|
|
|
206
|
-
|
|
278
|
+
if isinstance(result, ToolResult):
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
unstructured_result = _convert_to_content(result, serializer=self.serializer)
|
|
282
|
+
|
|
283
|
+
structured_output = None
|
|
284
|
+
# First handle structured content based on output schema, if any
|
|
285
|
+
if self.output_schema is not None:
|
|
286
|
+
if self.output_schema.get("x-fastmcp-wrap-result"):
|
|
287
|
+
# Schema says wrap - always wrap in result key
|
|
288
|
+
structured_output = {"result": result}
|
|
289
|
+
else:
|
|
290
|
+
structured_output = result
|
|
291
|
+
# If no output schema, try to serialize the result. If it is a dict, use
|
|
292
|
+
# it as structured content. If it is not a dict, ignore it.
|
|
293
|
+
if structured_output is None:
|
|
294
|
+
try:
|
|
295
|
+
structured_output = pydantic_core.to_jsonable_python(result)
|
|
296
|
+
if not isinstance(structured_output, dict):
|
|
297
|
+
structured_output = None
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
return ToolResult(
|
|
302
|
+
content=unstructured_result,
|
|
303
|
+
structured_content=structured_output,
|
|
304
|
+
)
|
|
207
305
|
|
|
208
306
|
|
|
209
307
|
@dataclass
|
|
@@ -211,7 +309,8 @@ class ParsedFunction:
|
|
|
211
309
|
fn: Callable[..., Any]
|
|
212
310
|
name: str
|
|
213
311
|
description: str | None
|
|
214
|
-
|
|
312
|
+
input_schema: dict[str, Any]
|
|
313
|
+
output_schema: dict[str, Any] | None
|
|
215
314
|
|
|
216
315
|
@classmethod
|
|
217
316
|
def from_function(
|
|
@@ -219,6 +318,7 @@ class ParsedFunction:
|
|
|
219
318
|
fn: Callable[..., Any],
|
|
220
319
|
exclude_args: list[str] | None = None,
|
|
221
320
|
validate: bool = True,
|
|
321
|
+
wrap_non_object_output_schema: bool = True,
|
|
222
322
|
) -> ParsedFunction:
|
|
223
323
|
from fastmcp.server.context import Context
|
|
224
324
|
|
|
@@ -257,9 +357,6 @@ class ParsedFunction:
|
|
|
257
357
|
if isinstance(fn, staticmethod):
|
|
258
358
|
fn = fn.__func__
|
|
259
359
|
|
|
260
|
-
type_adapter = get_cached_typeadapter(fn)
|
|
261
|
-
schema = type_adapter.json_schema()
|
|
262
|
-
|
|
263
360
|
prune_params: list[str] = []
|
|
264
361
|
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
265
362
|
if context_kwarg:
|
|
@@ -267,12 +364,67 @@ class ParsedFunction:
|
|
|
267
364
|
if exclude_args:
|
|
268
365
|
prune_params.extend(exclude_args)
|
|
269
366
|
|
|
270
|
-
|
|
367
|
+
input_type_adapter = get_cached_typeadapter(fn)
|
|
368
|
+
input_schema = input_type_adapter.json_schema()
|
|
369
|
+
input_schema = compress_schema(input_schema, prune_params=prune_params)
|
|
370
|
+
|
|
371
|
+
output_schema = None
|
|
372
|
+
output_type = inspect.signature(fn).return_annotation
|
|
373
|
+
|
|
374
|
+
if output_type not in (inspect._empty, None, Any, ...):
|
|
375
|
+
# there are a variety of types that we don't want to attempt to
|
|
376
|
+
# serialize because they are either used by FastMCP internally,
|
|
377
|
+
# or are MCP content types that explicitly don't form structured
|
|
378
|
+
# content. By replacing them with an explicitly unserializable type,
|
|
379
|
+
# we ensure that no output schema is automatically generated.
|
|
380
|
+
clean_output_type = replace_type(
|
|
381
|
+
output_type,
|
|
382
|
+
{
|
|
383
|
+
t: _UnserializableType
|
|
384
|
+
for t in (
|
|
385
|
+
Image,
|
|
386
|
+
Audio,
|
|
387
|
+
File,
|
|
388
|
+
ToolResult,
|
|
389
|
+
mcp.types.TextContent,
|
|
390
|
+
mcp.types.ImageContent,
|
|
391
|
+
mcp.types.AudioContent,
|
|
392
|
+
mcp.types.ResourceLink,
|
|
393
|
+
mcp.types.EmbeddedResource,
|
|
394
|
+
)
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
type_adapter = get_cached_typeadapter(clean_output_type)
|
|
400
|
+
base_schema = type_adapter.json_schema()
|
|
401
|
+
|
|
402
|
+
# Generate schema for wrapped type if it's non-object
|
|
403
|
+
# because MCP requires that output schemas are objects
|
|
404
|
+
if (
|
|
405
|
+
wrap_non_object_output_schema
|
|
406
|
+
and base_schema.get("type") != "object"
|
|
407
|
+
):
|
|
408
|
+
# Use the wrapped result schema directly
|
|
409
|
+
wrapped_type = _WrappedResult[clean_output_type]
|
|
410
|
+
wrapped_adapter = get_cached_typeadapter(wrapped_type)
|
|
411
|
+
output_schema = wrapped_adapter.json_schema()
|
|
412
|
+
output_schema["x-fastmcp-wrap-result"] = True
|
|
413
|
+
else:
|
|
414
|
+
output_schema = base_schema
|
|
415
|
+
|
|
416
|
+
output_schema = compress_schema(output_schema)
|
|
417
|
+
|
|
418
|
+
except PydanticSchemaGenerationError as e:
|
|
419
|
+
if "_UnserializableType" not in str(e):
|
|
420
|
+
logger.debug(f"Unable to generate schema for type {output_type!r}")
|
|
421
|
+
|
|
271
422
|
return cls(
|
|
272
423
|
fn=fn,
|
|
273
424
|
name=fn_name,
|
|
274
425
|
description=fn_doc,
|
|
275
|
-
|
|
426
|
+
input_schema=input_schema,
|
|
427
|
+
output_schema=output_schema or None,
|
|
276
428
|
)
|
|
277
429
|
|
|
278
430
|
|
|
@@ -280,12 +432,12 @@ def _convert_to_content(
|
|
|
280
432
|
result: Any,
|
|
281
433
|
serializer: Callable[[Any], str] | None = None,
|
|
282
434
|
_process_as_single_item: bool = False,
|
|
283
|
-
) -> list[
|
|
435
|
+
) -> list[ContentBlock]:
|
|
284
436
|
"""Convert a result to a sequence of content objects."""
|
|
285
437
|
if result is None:
|
|
286
438
|
return []
|
|
287
439
|
|
|
288
|
-
if isinstance(result,
|
|
440
|
+
if isinstance(result, ContentBlock):
|
|
289
441
|
return [result]
|
|
290
442
|
|
|
291
443
|
if isinstance(result, Image):
|
|
@@ -308,7 +460,7 @@ def _convert_to_content(
|
|
|
308
460
|
other_content = []
|
|
309
461
|
|
|
310
462
|
for item in result:
|
|
311
|
-
if isinstance(item,
|
|
463
|
+
if isinstance(item, ContentBlock | Image | Audio | File):
|
|
312
464
|
mcp_types.append(_convert_to_content(item)[0])
|
|
313
465
|
else:
|
|
314
466
|
other_content.append(item)
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -9,9 +9,8 @@ from mcp.types import ToolAnnotations
|
|
|
9
9
|
from fastmcp import settings
|
|
10
10
|
from fastmcp.exceptions import NotFoundError, ToolError
|
|
11
11
|
from fastmcp.settings import DuplicateBehavior
|
|
12
|
-
from fastmcp.tools.tool import Tool
|
|
12
|
+
from fastmcp.tools.tool import Tool, ToolResult
|
|
13
13
|
from fastmcp.utilities.logging import get_logger
|
|
14
|
-
from fastmcp.utilities.types import MCPContent
|
|
15
14
|
|
|
16
15
|
if TYPE_CHECKING:
|
|
17
16
|
from fastmcp.server.server import MountedServer
|
|
@@ -170,7 +169,7 @@ class ToolManager:
|
|
|
170
169
|
else:
|
|
171
170
|
raise NotFoundError(f"Tool {key!r} not found")
|
|
172
171
|
|
|
173
|
-
async def call_tool(self, key: str, arguments: dict[str, Any]) ->
|
|
172
|
+
async def call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
|
|
174
173
|
"""
|
|
175
174
|
Internal API for servers: Finds and calls a tool, respecting the
|
|
176
175
|
filtered protocol path.
|