fastmcp 2.9.2__py3-none-any.whl → 2.10.1__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/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 +27 -7
- 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/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.2.dist-info → fastmcp-2.10.1.dist-info}/METADATA +3 -2
- {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/RECORD +39 -31
- {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -26,6 +26,7 @@ from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
|
|
|
26
26
|
from mcp.server.stdio import stdio_server
|
|
27
27
|
from mcp.types import (
|
|
28
28
|
AnyFunction,
|
|
29
|
+
ContentBlock,
|
|
29
30
|
GetPromptResult,
|
|
30
31
|
ToolAnnotations,
|
|
31
32
|
)
|
|
@@ -57,12 +58,12 @@ from fastmcp.server.low_level import LowLevelServer
|
|
|
57
58
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
58
59
|
from fastmcp.settings import Settings
|
|
59
60
|
from fastmcp.tools import ToolManager
|
|
60
|
-
from fastmcp.tools.tool import FunctionTool, Tool
|
|
61
|
+
from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
|
|
61
62
|
from fastmcp.utilities.cache import TimedCache
|
|
62
63
|
from fastmcp.utilities.components import FastMCPComponent
|
|
63
64
|
from fastmcp.utilities.logging import get_logger
|
|
64
65
|
from fastmcp.utilities.mcp_config import MCPConfig
|
|
65
|
-
from fastmcp.utilities.types import
|
|
66
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
66
67
|
|
|
67
68
|
if TYPE_CHECKING:
|
|
68
69
|
from fastmcp.client import Client
|
|
@@ -441,7 +442,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
441
442
|
"""
|
|
442
443
|
List all available tools, in the format expected by the low-level MCP
|
|
443
444
|
server.
|
|
444
|
-
|
|
445
445
|
"""
|
|
446
446
|
|
|
447
447
|
async def _handler(
|
|
@@ -593,7 +593,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
593
593
|
|
|
594
594
|
async def _mcp_call_tool(
|
|
595
595
|
self, key: str, arguments: dict[str, Any]
|
|
596
|
-
) -> list[
|
|
596
|
+
) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]:
|
|
597
597
|
"""
|
|
598
598
|
Handle MCP 'callTool' requests.
|
|
599
599
|
|
|
@@ -610,20 +610,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
610
610
|
|
|
611
611
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
612
612
|
try:
|
|
613
|
-
|
|
613
|
+
result = await self._call_tool(key, arguments)
|
|
614
|
+
return result.to_mcp_result()
|
|
614
615
|
except DisabledError:
|
|
615
616
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
616
617
|
except NotFoundError:
|
|
617
618
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
618
619
|
|
|
619
|
-
async def _call_tool(self, key: str, arguments: dict[str, Any]) ->
|
|
620
|
+
async def _call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
|
|
620
621
|
"""
|
|
621
622
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
622
623
|
"""
|
|
623
624
|
|
|
624
625
|
async def _handler(
|
|
625
626
|
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
626
|
-
) ->
|
|
627
|
+
) -> ToolResult:
|
|
627
628
|
tool = await self._tool_manager.get_tool(context.message.name)
|
|
628
629
|
if not self._should_enable_component(tool):
|
|
629
630
|
raise NotFoundError(f"Unknown tool: {context.message.name!r}")
|
|
@@ -789,8 +790,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
789
790
|
name_or_fn: AnyFunction,
|
|
790
791
|
*,
|
|
791
792
|
name: str | None = None,
|
|
793
|
+
title: str | None = None,
|
|
792
794
|
description: str | None = None,
|
|
793
795
|
tags: set[str] | None = None,
|
|
796
|
+
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
794
797
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
795
798
|
exclude_args: list[str] | None = None,
|
|
796
799
|
enabled: bool | None = None,
|
|
@@ -802,8 +805,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
802
805
|
name_or_fn: str | None = None,
|
|
803
806
|
*,
|
|
804
807
|
name: str | None = None,
|
|
808
|
+
title: str | None = None,
|
|
805
809
|
description: str | None = None,
|
|
806
810
|
tags: set[str] | None = None,
|
|
811
|
+
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
807
812
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
808
813
|
exclude_args: list[str] | None = None,
|
|
809
814
|
enabled: bool | None = None,
|
|
@@ -814,8 +819,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
814
819
|
name_or_fn: str | AnyFunction | None = None,
|
|
815
820
|
*,
|
|
816
821
|
name: str | None = None,
|
|
822
|
+
title: str | None = None,
|
|
817
823
|
description: str | None = None,
|
|
818
824
|
tags: set[str] | None = None,
|
|
825
|
+
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
819
826
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
820
827
|
exclude_args: list[str] | None = None,
|
|
821
828
|
enabled: bool | None = None,
|
|
@@ -838,6 +845,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
838
845
|
name: Optional name for the tool (keyword-only, alternative to name_or_fn)
|
|
839
846
|
description: Optional description of what the tool does
|
|
840
847
|
tags: Optional set of tags for categorizing the tool
|
|
848
|
+
output_schema: Optional JSON schema for the tool's output
|
|
841
849
|
annotations: Optional annotations about the tool's behavior
|
|
842
850
|
exclude_args: Optional list of argument names to exclude from the tool schema
|
|
843
851
|
enabled: Optional boolean to enable or disable the tool
|
|
@@ -892,8 +900,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
892
900
|
tool = Tool.from_function(
|
|
893
901
|
fn,
|
|
894
902
|
name=tool_name,
|
|
903
|
+
title=title,
|
|
895
904
|
description=description,
|
|
896
905
|
tags=tags,
|
|
906
|
+
output_schema=output_schema,
|
|
897
907
|
annotations=annotations,
|
|
898
908
|
exclude_args=exclude_args,
|
|
899
909
|
serializer=self._tool_serializer,
|
|
@@ -922,8 +932,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
922
932
|
return partial(
|
|
923
933
|
self.tool,
|
|
924
934
|
name=tool_name,
|
|
935
|
+
title=title,
|
|
925
936
|
description=description,
|
|
926
937
|
tags=tags,
|
|
938
|
+
output_schema=output_schema,
|
|
927
939
|
annotations=annotations,
|
|
928
940
|
exclude_args=exclude_args,
|
|
929
941
|
enabled=enabled,
|
|
@@ -1009,6 +1021,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1009
1021
|
uri: str,
|
|
1010
1022
|
*,
|
|
1011
1023
|
name: str | None = None,
|
|
1024
|
+
title: str | None = None,
|
|
1012
1025
|
description: str | None = None,
|
|
1013
1026
|
mime_type: str | None = None,
|
|
1014
1027
|
tags: set[str] | None = None,
|
|
@@ -1100,6 +1113,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1100
1113
|
fn=fn,
|
|
1101
1114
|
uri_template=uri,
|
|
1102
1115
|
name=name,
|
|
1116
|
+
title=title,
|
|
1103
1117
|
description=description,
|
|
1104
1118
|
mime_type=mime_type,
|
|
1105
1119
|
tags=tags,
|
|
@@ -1112,6 +1126,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1112
1126
|
fn=fn,
|
|
1113
1127
|
uri=uri,
|
|
1114
1128
|
name=name,
|
|
1129
|
+
title=title,
|
|
1115
1130
|
description=description,
|
|
1116
1131
|
mime_type=mime_type,
|
|
1117
1132
|
tags=tags,
|
|
@@ -1151,6 +1166,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1151
1166
|
name_or_fn: AnyFunction,
|
|
1152
1167
|
*,
|
|
1153
1168
|
name: str | None = None,
|
|
1169
|
+
title: str | None = None,
|
|
1154
1170
|
description: str | None = None,
|
|
1155
1171
|
tags: set[str] | None = None,
|
|
1156
1172
|
enabled: bool | None = None,
|
|
@@ -1162,6 +1178,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1162
1178
|
name_or_fn: str | None = None,
|
|
1163
1179
|
*,
|
|
1164
1180
|
name: str | None = None,
|
|
1181
|
+
title: str | None = None,
|
|
1165
1182
|
description: str | None = None,
|
|
1166
1183
|
tags: set[str] | None = None,
|
|
1167
1184
|
enabled: bool | None = None,
|
|
@@ -1172,6 +1189,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1172
1189
|
name_or_fn: str | AnyFunction | None = None,
|
|
1173
1190
|
*,
|
|
1174
1191
|
name: str | None = None,
|
|
1192
|
+
title: str | None = None,
|
|
1175
1193
|
description: str | None = None,
|
|
1176
1194
|
tags: set[str] | None = None,
|
|
1177
1195
|
enabled: bool | None = None,
|
|
@@ -1268,6 +1286,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1268
1286
|
prompt = Prompt.from_function(
|
|
1269
1287
|
fn=fn,
|
|
1270
1288
|
name=prompt_name,
|
|
1289
|
+
title=title,
|
|
1271
1290
|
description=description,
|
|
1272
1291
|
tags=tags,
|
|
1273
1292
|
enabled=enabled,
|
|
@@ -1296,6 +1315,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1296
1315
|
return partial(
|
|
1297
1316
|
self.prompt,
|
|
1298
1317
|
name=prompt_name,
|
|
1318
|
+
title=title,
|
|
1299
1319
|
description=description,
|
|
1300
1320
|
tags=tags,
|
|
1301
1321
|
enabled=enabled,
|
fastmcp/settings.py
CHANGED
|
@@ -154,23 +154,6 @@ class Settings(BaseSettings):
|
|
|
154
154
|
),
|
|
155
155
|
] = "path"
|
|
156
156
|
|
|
157
|
-
tool_attempt_parse_json_args: Annotated[
|
|
158
|
-
bool,
|
|
159
|
-
Field(
|
|
160
|
-
default=False,
|
|
161
|
-
description=inspect.cleandoc(
|
|
162
|
-
"""
|
|
163
|
-
Note: this enables a legacy behavior. If True, will attempt to parse
|
|
164
|
-
stringified JSON lists and objects strings in tool arguments before
|
|
165
|
-
passing them to the tool. This is an old behavior that can create
|
|
166
|
-
unexpected type coercion issues, but may be helpful for less powerful
|
|
167
|
-
LLMs that stringify JSON instead of passing actual lists and objects.
|
|
168
|
-
Defaults to False.
|
|
169
|
-
"""
|
|
170
|
-
),
|
|
171
|
-
),
|
|
172
|
-
] = False
|
|
173
|
-
|
|
174
157
|
client_init_timeout: Annotated[
|
|
175
158
|
float | None,
|
|
176
159
|
Field(
|
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.
|