fastmcp 2.7.1__py3-none-any.whl → 2.8.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/__init__.py +32 -3
- fastmcp/cli/cli.py +3 -2
- fastmcp/client/auth/oauth.py +1 -1
- fastmcp/client/client.py +6 -5
- fastmcp/client/sampling.py +5 -9
- fastmcp/client/transports.py +42 -33
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/prompt.py +11 -21
- fastmcp/prompts/prompt_manager.py +13 -9
- fastmcp/resources/resource.py +21 -26
- fastmcp/resources/resource_manager.py +15 -12
- fastmcp/resources/template.py +8 -16
- fastmcp/server/auth/providers/bearer_env.py +8 -11
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/context.py +12 -10
- fastmcp/server/openapi.py +87 -57
- fastmcp/server/proxy.py +29 -20
- fastmcp/server/server.py +422 -206
- fastmcp/settings.py +113 -37
- fastmcp/tools/__init__.py +2 -1
- fastmcp/tools/tool.py +125 -85
- fastmcp/tools/tool_manager.py +12 -11
- fastmcp/tools/tool_transform.py +669 -0
- fastmcp/utilities/components.py +55 -0
- fastmcp/utilities/exceptions.py +1 -1
- fastmcp/utilities/mcp_config.py +1 -1
- fastmcp/utilities/tests.py +3 -3
- fastmcp/utilities/types.py +82 -14
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/METADATA +48 -26
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/RECORD +33 -31
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/settings.py
CHANGED
|
@@ -2,31 +2,99 @@ from __future__ import annotations as _annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Annotated, Literal
|
|
5
|
+
from typing import Annotated, Any, Literal
|
|
6
6
|
|
|
7
7
|
from pydantic import Field, model_validator
|
|
8
|
+
from pydantic.fields import FieldInfo
|
|
8
9
|
from pydantic_settings import (
|
|
9
10
|
BaseSettings,
|
|
11
|
+
EnvSettingsSource,
|
|
12
|
+
PydanticBaseSettingsSource,
|
|
10
13
|
SettingsConfigDict,
|
|
11
14
|
)
|
|
12
15
|
from typing_extensions import Self
|
|
13
16
|
|
|
17
|
+
from fastmcp.utilities.logging import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
14
21
|
LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
15
22
|
|
|
16
23
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
17
24
|
|
|
18
25
|
|
|
26
|
+
class ExtendedEnvSettingsSource(EnvSettingsSource):
|
|
27
|
+
"""
|
|
28
|
+
A special EnvSettingsSource that allows for multiple env var prefixes to be used.
|
|
29
|
+
|
|
30
|
+
Raises a deprecation warning if the old `FASTMCP_SERVER_` prefix is used.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def get_field_value(
|
|
34
|
+
self, field: FieldInfo, field_name: str
|
|
35
|
+
) -> tuple[Any, str, bool]:
|
|
36
|
+
if prefixes := self.config.get("env_prefixes"):
|
|
37
|
+
for prefix in prefixes:
|
|
38
|
+
self.env_prefix = prefix
|
|
39
|
+
env_val, field_key, value_is_complex = super().get_field_value(
|
|
40
|
+
field, field_name
|
|
41
|
+
)
|
|
42
|
+
if env_val is not None:
|
|
43
|
+
if prefix == "FASTMCP_SERVER_":
|
|
44
|
+
# Deprecated in 2.8.0
|
|
45
|
+
logger.warning(
|
|
46
|
+
"Using `FASTMCP_SERVER_` environment variables is deprecated. Use `FASTMCP_` instead.",
|
|
47
|
+
)
|
|
48
|
+
return env_val, field_key, value_is_complex
|
|
49
|
+
|
|
50
|
+
return super().get_field_value(field, field_name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ExtendedSettingsConfigDict(SettingsConfigDict, total=False):
|
|
54
|
+
env_prefixes: list[str] | None
|
|
55
|
+
|
|
56
|
+
|
|
19
57
|
class Settings(BaseSettings):
|
|
20
58
|
"""FastMCP settings."""
|
|
21
59
|
|
|
22
|
-
model_config =
|
|
23
|
-
|
|
60
|
+
model_config = ExtendedSettingsConfigDict(
|
|
61
|
+
env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"],
|
|
24
62
|
env_file=".env",
|
|
25
63
|
extra="ignore",
|
|
26
64
|
env_nested_delimiter="__",
|
|
27
65
|
nested_model_default_partial_update=True,
|
|
28
66
|
)
|
|
29
67
|
|
|
68
|
+
@classmethod
|
|
69
|
+
def settings_customise_sources(
|
|
70
|
+
cls,
|
|
71
|
+
settings_cls: type[BaseSettings],
|
|
72
|
+
init_settings: PydanticBaseSettingsSource,
|
|
73
|
+
env_settings: PydanticBaseSettingsSource,
|
|
74
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
75
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
76
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
77
|
+
# can remove this classmethod after deprecated FASTMCP_SERVER_ prefix is
|
|
78
|
+
# removed
|
|
79
|
+
return (
|
|
80
|
+
init_settings,
|
|
81
|
+
ExtendedEnvSettingsSource(settings_cls),
|
|
82
|
+
dotenv_settings,
|
|
83
|
+
file_secret_settings,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def settings(self) -> Self:
|
|
88
|
+
"""
|
|
89
|
+
This property is for backwards compatibility with FastMCP < 2.8.0,
|
|
90
|
+
which accessed fastmcp.settings.settings
|
|
91
|
+
"""
|
|
92
|
+
# Deprecated in 2.8.0
|
|
93
|
+
logger.warning(
|
|
94
|
+
"Using fastmcp.settings.settings is deprecated. Use fastmcp.settings instead.",
|
|
95
|
+
)
|
|
96
|
+
return self
|
|
97
|
+
|
|
30
98
|
home: Path = Path.home() / ".fastmcp"
|
|
31
99
|
|
|
32
100
|
test_mode: bool = False
|
|
@@ -42,6 +110,20 @@ class Settings(BaseSettings):
|
|
|
42
110
|
),
|
|
43
111
|
] = True
|
|
44
112
|
|
|
113
|
+
deprecation_warnings: Annotated[
|
|
114
|
+
bool,
|
|
115
|
+
Field(
|
|
116
|
+
description=inspect.cleandoc(
|
|
117
|
+
"""
|
|
118
|
+
Whether to show deprecation warnings. You can completely reset
|
|
119
|
+
Python's warning behavior by running `warnings.resetwarnings()`.
|
|
120
|
+
Note this will NOT apply to deprecation warnings from the
|
|
121
|
+
settings class itself.
|
|
122
|
+
""",
|
|
123
|
+
)
|
|
124
|
+
),
|
|
125
|
+
] = True
|
|
126
|
+
|
|
45
127
|
client_raise_first_exceptiongroup_error: Annotated[
|
|
46
128
|
bool,
|
|
47
129
|
Field(
|
|
@@ -107,27 +189,6 @@ class Settings(BaseSettings):
|
|
|
107
189
|
|
|
108
190
|
return self
|
|
109
191
|
|
|
110
|
-
|
|
111
|
-
class ServerSettings(BaseSettings):
|
|
112
|
-
"""FastMCP server settings.
|
|
113
|
-
|
|
114
|
-
All settings can be configured via environment variables with the prefix FASTMCP_.
|
|
115
|
-
For example, FASTMCP_DEBUG=true will set debug=True.
|
|
116
|
-
"""
|
|
117
|
-
|
|
118
|
-
model_config = SettingsConfigDict(
|
|
119
|
-
env_prefix="FASTMCP_SERVER_",
|
|
120
|
-
env_file=".env",
|
|
121
|
-
extra="ignore",
|
|
122
|
-
env_nested_delimiter="__",
|
|
123
|
-
nested_model_default_partial_update=True,
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
log_level: Annotated[
|
|
127
|
-
LOG_LEVEL,
|
|
128
|
-
Field(default_factory=lambda: Settings().log_level),
|
|
129
|
-
]
|
|
130
|
-
|
|
131
192
|
# HTTP settings
|
|
132
193
|
host: str = "127.0.0.1"
|
|
133
194
|
port: int = 8000
|
|
@@ -136,15 +197,6 @@ class ServerSettings(BaseSettings):
|
|
|
136
197
|
streamable_http_path: str = "/mcp"
|
|
137
198
|
debug: bool = False
|
|
138
199
|
|
|
139
|
-
# resource settings
|
|
140
|
-
on_duplicate_resources: DuplicateBehavior = "warn"
|
|
141
|
-
|
|
142
|
-
# tool settings
|
|
143
|
-
on_duplicate_tools: DuplicateBehavior = "warn"
|
|
144
|
-
|
|
145
|
-
# prompt settings
|
|
146
|
-
on_duplicate_prompts: DuplicateBehavior = "warn"
|
|
147
|
-
|
|
148
200
|
# error handling
|
|
149
201
|
mask_error_details: Annotated[
|
|
150
202
|
bool,
|
|
@@ -162,7 +214,7 @@ class ServerSettings(BaseSettings):
|
|
|
162
214
|
),
|
|
163
215
|
] = False
|
|
164
216
|
|
|
165
|
-
|
|
217
|
+
server_dependencies: Annotated[
|
|
166
218
|
list[str],
|
|
167
219
|
Field(
|
|
168
220
|
default_factory=list,
|
|
@@ -170,9 +222,6 @@ class ServerSettings(BaseSettings):
|
|
|
170
222
|
),
|
|
171
223
|
] = []
|
|
172
224
|
|
|
173
|
-
# cache settings (for getting attributes from servers, used to avoid repeated calls)
|
|
174
|
-
cache_expiration_seconds: float = 0
|
|
175
|
-
|
|
176
225
|
# StreamableHTTP settings
|
|
177
226
|
json_response: bool = False
|
|
178
227
|
stateless_http: bool = (
|
|
@@ -198,5 +247,32 @@ class ServerSettings(BaseSettings):
|
|
|
198
247
|
),
|
|
199
248
|
] = None
|
|
200
249
|
|
|
250
|
+
include_tags: Annotated[
|
|
251
|
+
set[str] | None,
|
|
252
|
+
Field(
|
|
253
|
+
default=None,
|
|
254
|
+
description=inspect.cleandoc(
|
|
255
|
+
"""
|
|
256
|
+
If provided, only components that match these tags will be
|
|
257
|
+
exposed to clients. A component is considered to match if ANY of
|
|
258
|
+
its tags match ANY of the tags in the set.
|
|
259
|
+
"""
|
|
260
|
+
),
|
|
261
|
+
),
|
|
262
|
+
] = None
|
|
263
|
+
exclude_tags: Annotated[
|
|
264
|
+
set[str] | None,
|
|
265
|
+
Field(
|
|
266
|
+
default=None,
|
|
267
|
+
description=inspect.cleandoc(
|
|
268
|
+
"""
|
|
269
|
+
If provided, components that match these tags will be excluded
|
|
270
|
+
from the server. A component is considered to match if ANY of
|
|
271
|
+
its tags match ANY of the tags in the set.
|
|
272
|
+
"""
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
] = None
|
|
276
|
+
|
|
201
277
|
|
|
202
278
|
settings = Settings()
|
fastmcp/tools/__init__.py
CHANGED
fastmcp/tools/tool.py
CHANGED
|
@@ -2,29 +2,30 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
|
-
from abc import ABC, abstractmethod
|
|
6
5
|
from collections.abc import Callable
|
|
7
|
-
from
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
import pydantic_core
|
|
10
|
-
from mcp.types import
|
|
10
|
+
from mcp.types import TextContent, ToolAnnotations
|
|
11
11
|
from mcp.types import Tool as MCPTool
|
|
12
|
-
from pydantic import
|
|
12
|
+
from pydantic import Field
|
|
13
13
|
|
|
14
14
|
import fastmcp
|
|
15
15
|
from fastmcp.server.dependencies import get_context
|
|
16
|
+
from fastmcp.utilities.components import FastMCPComponent
|
|
16
17
|
from fastmcp.utilities.json_schema import compress_schema
|
|
17
18
|
from fastmcp.utilities.logging import get_logger
|
|
18
19
|
from fastmcp.utilities.types import (
|
|
19
|
-
|
|
20
|
+
Audio,
|
|
20
21
|
Image,
|
|
21
|
-
|
|
22
|
+
MCPContent,
|
|
22
23
|
find_kwarg_by_type,
|
|
23
24
|
get_cached_typeadapter,
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
27
|
-
|
|
28
|
+
from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
|
|
28
29
|
|
|
29
30
|
logger = get_logger(__name__)
|
|
30
31
|
|
|
@@ -33,24 +34,13 @@ def default_serializer(data: Any) -> str:
|
|
|
33
34
|
return pydantic_core.to_json(data, fallback=str, indent=2).decode()
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
class Tool(
|
|
37
|
+
class Tool(FastMCPComponent):
|
|
37
38
|
"""Internal tool registration info."""
|
|
38
39
|
|
|
39
|
-
name: str = Field(description="Name of the tool")
|
|
40
|
-
description: str | None = Field(
|
|
41
|
-
default=None, description="Description of what the tool does"
|
|
42
|
-
)
|
|
43
40
|
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
|
|
44
|
-
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
45
|
-
default_factory=set, description="Tags for the tool"
|
|
46
|
-
)
|
|
47
41
|
annotations: ToolAnnotations | None = Field(
|
|
48
42
|
default=None, description="Additional annotations about the tool"
|
|
49
43
|
)
|
|
50
|
-
exclude_args: list[str] | None = Field(
|
|
51
|
-
default=None,
|
|
52
|
-
description="Arguments to exclude from the tool schema, such as State, Memory, or Credential",
|
|
53
|
-
)
|
|
54
44
|
serializer: Callable[[Any], str] | None = Field(
|
|
55
45
|
default=None, description="Optional custom serializer for tool results"
|
|
56
46
|
)
|
|
@@ -73,6 +63,7 @@ class Tool(FastMCPBaseModel, ABC):
|
|
|
73
63
|
annotations: ToolAnnotations | None = None,
|
|
74
64
|
exclude_args: list[str] | None = None,
|
|
75
65
|
serializer: Callable[[Any], str] | None = None,
|
|
66
|
+
enabled: bool | None = None,
|
|
76
67
|
) -> FunctionTool:
|
|
77
68
|
"""Create a Tool from a function."""
|
|
78
69
|
return FunctionTool.from_function(
|
|
@@ -83,21 +74,40 @@ class Tool(FastMCPBaseModel, ABC):
|
|
|
83
74
|
annotations=annotations,
|
|
84
75
|
exclude_args=exclude_args,
|
|
85
76
|
serializer=serializer,
|
|
77
|
+
enabled=enabled,
|
|
86
78
|
)
|
|
87
79
|
|
|
88
|
-
def
|
|
89
|
-
if type(self) is not type(other):
|
|
90
|
-
return False
|
|
91
|
-
assert isinstance(other, type(self))
|
|
92
|
-
return self.model_dump() == other.model_dump()
|
|
93
|
-
|
|
94
|
-
@abstractmethod
|
|
95
|
-
async def run(
|
|
96
|
-
self, arguments: dict[str, Any]
|
|
97
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
80
|
+
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
98
81
|
"""Run the tool with arguments."""
|
|
99
82
|
raise NotImplementedError("Subclasses must implement run()")
|
|
100
83
|
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_tool(
|
|
86
|
+
cls,
|
|
87
|
+
tool: Tool,
|
|
88
|
+
transform_fn: Callable[..., Any] | None = None,
|
|
89
|
+
name: str | None = None,
|
|
90
|
+
transform_args: dict[str, ArgTransform] | None = None,
|
|
91
|
+
description: str | None = None,
|
|
92
|
+
tags: set[str] | None = None,
|
|
93
|
+
annotations: ToolAnnotations | None = None,
|
|
94
|
+
serializer: Callable[[Any], str] | None = None,
|
|
95
|
+
enabled: bool | None = None,
|
|
96
|
+
) -> TransformedTool:
|
|
97
|
+
from fastmcp.tools.tool_transform import TransformedTool
|
|
98
|
+
|
|
99
|
+
return TransformedTool.from_tool(
|
|
100
|
+
tool=tool,
|
|
101
|
+
transform_fn=transform_fn,
|
|
102
|
+
name=name,
|
|
103
|
+
transform_args=transform_args,
|
|
104
|
+
description=description,
|
|
105
|
+
tags=tags,
|
|
106
|
+
annotations=annotations,
|
|
107
|
+
serializer=serializer,
|
|
108
|
+
enabled=enabled,
|
|
109
|
+
)
|
|
110
|
+
|
|
101
111
|
|
|
102
112
|
class FunctionTool(Tool):
|
|
103
113
|
fn: Callable[..., Any]
|
|
@@ -112,70 +122,27 @@ class FunctionTool(Tool):
|
|
|
112
122
|
annotations: ToolAnnotations | None = None,
|
|
113
123
|
exclude_args: list[str] | None = None,
|
|
114
124
|
serializer: Callable[[Any], str] | None = None,
|
|
125
|
+
enabled: bool | None = None,
|
|
115
126
|
) -> FunctionTool:
|
|
116
127
|
"""Create a Tool from a function."""
|
|
117
|
-
from fastmcp.server.context import Context
|
|
118
|
-
|
|
119
|
-
# Reject functions with *args or **kwargs
|
|
120
|
-
sig = inspect.signature(fn)
|
|
121
|
-
for param in sig.parameters.values():
|
|
122
|
-
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
123
|
-
raise ValueError("Functions with *args are not supported as tools")
|
|
124
|
-
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
125
|
-
raise ValueError("Functions with **kwargs are not supported as tools")
|
|
126
|
-
|
|
127
|
-
if exclude_args:
|
|
128
|
-
for arg_name in exclude_args:
|
|
129
|
-
if arg_name not in sig.parameters:
|
|
130
|
-
raise ValueError(
|
|
131
|
-
f"Parameter '{arg_name}' in exclude_args does not exist in function."
|
|
132
|
-
)
|
|
133
|
-
param = sig.parameters[arg_name]
|
|
134
|
-
if param.default == inspect.Parameter.empty:
|
|
135
|
-
raise ValueError(
|
|
136
|
-
f"Parameter '{arg_name}' in exclude_args must have a default value."
|
|
137
|
-
)
|
|
138
128
|
|
|
139
|
-
|
|
129
|
+
parsed_fn = ParsedFunction.from_function(fn, exclude_args=exclude_args)
|
|
140
130
|
|
|
141
|
-
if
|
|
131
|
+
if name is None and parsed_fn.name == "<lambda>":
|
|
142
132
|
raise ValueError("You must provide a name for lambda functions")
|
|
143
133
|
|
|
144
|
-
func_doc = description or fn.__doc__
|
|
145
|
-
|
|
146
|
-
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
147
|
-
if not inspect.isroutine(fn):
|
|
148
|
-
fn = fn.__call__
|
|
149
|
-
# if the fn is a staticmethod, we need to work with the underlying function
|
|
150
|
-
if isinstance(fn, staticmethod):
|
|
151
|
-
fn = fn.__func__
|
|
152
|
-
|
|
153
|
-
type_adapter = get_cached_typeadapter(fn)
|
|
154
|
-
schema = type_adapter.json_schema()
|
|
155
|
-
|
|
156
|
-
prune_params: list[str] = []
|
|
157
|
-
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
158
|
-
if context_kwarg:
|
|
159
|
-
prune_params.append(context_kwarg)
|
|
160
|
-
if exclude_args:
|
|
161
|
-
prune_params.extend(exclude_args)
|
|
162
|
-
|
|
163
|
-
schema = compress_schema(schema, prune_params=prune_params)
|
|
164
|
-
|
|
165
134
|
return cls(
|
|
166
|
-
fn=fn,
|
|
167
|
-
name=
|
|
168
|
-
description=
|
|
169
|
-
parameters=
|
|
135
|
+
fn=parsed_fn.fn,
|
|
136
|
+
name=name or parsed_fn.name,
|
|
137
|
+
description=description or parsed_fn.description,
|
|
138
|
+
parameters=parsed_fn.parameters,
|
|
170
139
|
tags=tags or set(),
|
|
171
140
|
annotations=annotations,
|
|
172
|
-
exclude_args=exclude_args,
|
|
173
141
|
serializer=serializer,
|
|
142
|
+
enabled=enabled if enabled is not None else True,
|
|
174
143
|
)
|
|
175
144
|
|
|
176
|
-
async def run(
|
|
177
|
-
self, arguments: dict[str, Any]
|
|
178
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
145
|
+
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
179
146
|
"""Run the tool with arguments."""
|
|
180
147
|
from fastmcp.server.context import Context
|
|
181
148
|
|
|
@@ -185,7 +152,7 @@ class FunctionTool(Tool):
|
|
|
185
152
|
if context_kwarg and context_kwarg not in arguments:
|
|
186
153
|
arguments[context_kwarg] = get_context()
|
|
187
154
|
|
|
188
|
-
if fastmcp.settings.
|
|
155
|
+
if fastmcp.settings.tool_attempt_parse_json_args:
|
|
189
156
|
# Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
|
|
190
157
|
# being passed in as JSON inside a string rather than an actual list.
|
|
191
158
|
#
|
|
@@ -222,21 +189,94 @@ class FunctionTool(Tool):
|
|
|
222
189
|
return _convert_to_content(result, serializer=self.serializer)
|
|
223
190
|
|
|
224
191
|
|
|
192
|
+
@dataclass
|
|
193
|
+
class ParsedFunction:
|
|
194
|
+
fn: Callable[..., Any]
|
|
195
|
+
name: str
|
|
196
|
+
description: str | None
|
|
197
|
+
parameters: dict[str, Any]
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def from_function(
|
|
201
|
+
cls,
|
|
202
|
+
fn: Callable[..., Any],
|
|
203
|
+
exclude_args: list[str] | None = None,
|
|
204
|
+
validate: bool = True,
|
|
205
|
+
) -> ParsedFunction:
|
|
206
|
+
from fastmcp.server.context import Context
|
|
207
|
+
|
|
208
|
+
if validate:
|
|
209
|
+
sig = inspect.signature(fn)
|
|
210
|
+
# Reject functions with *args or **kwargs
|
|
211
|
+
for param in sig.parameters.values():
|
|
212
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
213
|
+
raise ValueError("Functions with *args are not supported as tools")
|
|
214
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
215
|
+
raise ValueError(
|
|
216
|
+
"Functions with **kwargs are not supported as tools"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Reject exclude_args that don't exist in the function or don't have a default value
|
|
220
|
+
if exclude_args:
|
|
221
|
+
for arg_name in exclude_args:
|
|
222
|
+
if arg_name not in sig.parameters:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"Parameter '{arg_name}' in exclude_args does not exist in function."
|
|
225
|
+
)
|
|
226
|
+
param = sig.parameters[arg_name]
|
|
227
|
+
if param.default == inspect.Parameter.empty:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Parameter '{arg_name}' in exclude_args must have a default value."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# collect name and doc before we potentially modify the function
|
|
233
|
+
fn_name = getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
234
|
+
fn_doc = fn.__doc__
|
|
235
|
+
|
|
236
|
+
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
237
|
+
if not inspect.isroutine(fn):
|
|
238
|
+
fn = fn.__call__
|
|
239
|
+
# if the fn is a staticmethod, we need to work with the underlying function
|
|
240
|
+
if isinstance(fn, staticmethod):
|
|
241
|
+
fn = fn.__func__
|
|
242
|
+
|
|
243
|
+
type_adapter = get_cached_typeadapter(fn)
|
|
244
|
+
schema = type_adapter.json_schema()
|
|
245
|
+
|
|
246
|
+
prune_params: list[str] = []
|
|
247
|
+
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
248
|
+
if context_kwarg:
|
|
249
|
+
prune_params.append(context_kwarg)
|
|
250
|
+
if exclude_args:
|
|
251
|
+
prune_params.extend(exclude_args)
|
|
252
|
+
|
|
253
|
+
schema = compress_schema(schema, prune_params=prune_params)
|
|
254
|
+
return cls(
|
|
255
|
+
fn=fn,
|
|
256
|
+
name=fn_name,
|
|
257
|
+
description=fn_doc,
|
|
258
|
+
parameters=schema,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
225
262
|
def _convert_to_content(
|
|
226
263
|
result: Any,
|
|
227
264
|
serializer: Callable[[Any], str] | None = None,
|
|
228
265
|
_process_as_single_item: bool = False,
|
|
229
|
-
) -> list[
|
|
266
|
+
) -> list[MCPContent]:
|
|
230
267
|
"""Convert a result to a sequence of content objects."""
|
|
231
268
|
if result is None:
|
|
232
269
|
return []
|
|
233
270
|
|
|
234
|
-
if isinstance(result,
|
|
271
|
+
if isinstance(result, MCPContent):
|
|
235
272
|
return [result]
|
|
236
273
|
|
|
237
274
|
if isinstance(result, Image):
|
|
238
275
|
return [result.to_image_content()]
|
|
239
276
|
|
|
277
|
+
elif isinstance(result, Audio):
|
|
278
|
+
return [result.to_audio_content()]
|
|
279
|
+
|
|
240
280
|
if isinstance(result, list | tuple) and not _process_as_single_item:
|
|
241
281
|
# if the result is a list, then it could either be a list of MCP types,
|
|
242
282
|
# or a "regular" list that the tool is returning, or a mix of both.
|
|
@@ -248,7 +288,7 @@ def _convert_to_content(
|
|
|
248
288
|
other_content = []
|
|
249
289
|
|
|
250
290
|
for item in result:
|
|
251
|
-
if isinstance(item,
|
|
291
|
+
if isinstance(item, MCPContent | Image | Audio):
|
|
252
292
|
mcp_types.append(_convert_to_content(item)[0])
|
|
253
293
|
else:
|
|
254
294
|
other_content.append(item)
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -4,12 +4,14 @@ import warnings
|
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
-
from mcp.types import
|
|
7
|
+
from mcp.types import ToolAnnotations
|
|
8
8
|
|
|
9
|
+
from fastmcp import settings
|
|
9
10
|
from fastmcp.exceptions import NotFoundError, ToolError
|
|
10
11
|
from fastmcp.settings import DuplicateBehavior
|
|
11
12
|
from fastmcp.tools.tool import Tool
|
|
12
13
|
from fastmcp.utilities.logging import get_logger
|
|
14
|
+
from fastmcp.utilities.types import MCPContent
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
17
|
pass
|
|
@@ -23,10 +25,10 @@ class ToolManager:
|
|
|
23
25
|
def __init__(
|
|
24
26
|
self,
|
|
25
27
|
duplicate_behavior: DuplicateBehavior | None = None,
|
|
26
|
-
mask_error_details: bool =
|
|
28
|
+
mask_error_details: bool | None = None,
|
|
27
29
|
):
|
|
28
30
|
self._tools: dict[str, Tool] = {}
|
|
29
|
-
self.mask_error_details = mask_error_details
|
|
31
|
+
self.mask_error_details = mask_error_details or settings.mask_error_details
|
|
30
32
|
|
|
31
33
|
# Default to "warn" if None is provided
|
|
32
34
|
if duplicate_behavior is None:
|
|
@@ -70,11 +72,12 @@ class ToolManager:
|
|
|
70
72
|
) -> Tool:
|
|
71
73
|
"""Add a tool to the server."""
|
|
72
74
|
# deprecated in 2.7.0
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
if settings.deprecation_warnings:
|
|
76
|
+
warnings.warn(
|
|
77
|
+
"ToolManager.add_tool_from_fn() is deprecated. Use Tool.from_function() and call add_tool() instead.",
|
|
78
|
+
DeprecationWarning,
|
|
79
|
+
stacklevel=2,
|
|
80
|
+
)
|
|
78
81
|
tool = Tool.from_function(
|
|
79
82
|
fn,
|
|
80
83
|
name=name,
|
|
@@ -118,9 +121,7 @@ class ToolManager:
|
|
|
118
121
|
else:
|
|
119
122
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
120
123
|
|
|
121
|
-
async def call_tool(
|
|
122
|
-
self, key: str, arguments: dict[str, Any]
|
|
123
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
124
|
+
async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
124
125
|
"""Call a tool by name with arguments."""
|
|
125
126
|
tool = self.get_tool(key)
|
|
126
127
|
if not tool:
|