fastmcp 2.7.1__py3-none-any.whl → 2.8.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/__init__.py +4 -1
- fastmcp/cli/cli.py +3 -2
- fastmcp/client/auth/oauth.py +1 -1
- fastmcp/client/client.py +3 -1
- fastmcp/client/transports.py +35 -28
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/prompt.py +8 -18
- fastmcp/prompts/prompt_manager.py +7 -4
- fastmcp/resources/resource.py +21 -26
- fastmcp/resources/resource_manager.py +3 -2
- fastmcp/resources/template.py +8 -16
- fastmcp/server/auth/providers/bearer_env.py +8 -11
- fastmcp/server/openapi.py +65 -38
- fastmcp/server/proxy.py +27 -14
- fastmcp/server/server.py +320 -131
- fastmcp/settings.py +100 -37
- fastmcp/tools/__init__.py +2 -1
- fastmcp/tools/tool.py +114 -75
- fastmcp/tools/tool_manager.py +3 -2
- fastmcp/tools/tool_transform.py +665 -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 +0 -9
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/METADATA +3 -1
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/RECORD +30 -28
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/settings.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import warnings
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Annotated, Literal
|
|
6
|
+
from typing import Annotated, Any, Literal
|
|
6
7
|
|
|
7
8
|
from pydantic import Field, model_validator
|
|
9
|
+
from pydantic.fields import FieldInfo
|
|
8
10
|
from pydantic_settings import (
|
|
9
11
|
BaseSettings,
|
|
12
|
+
EnvSettingsSource,
|
|
13
|
+
PydanticBaseSettingsSource,
|
|
10
14
|
SettingsConfigDict,
|
|
11
15
|
)
|
|
12
16
|
from typing_extensions import Self
|
|
@@ -16,17 +20,82 @@ LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
|
16
20
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
17
21
|
|
|
18
22
|
|
|
23
|
+
class ExtendedEnvSettingsSource(EnvSettingsSource):
|
|
24
|
+
"""
|
|
25
|
+
A special EnvSettingsSource that allows for multiple env var prefixes to be used.
|
|
26
|
+
|
|
27
|
+
Raises a deprecation warning if the old `FASTMCP_SERVER_` prefix is used.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def get_field_value(
|
|
31
|
+
self, field: FieldInfo, field_name: str
|
|
32
|
+
) -> tuple[Any, str, bool]:
|
|
33
|
+
if prefixes := self.config.get("env_prefixes"):
|
|
34
|
+
for prefix in prefixes:
|
|
35
|
+
self.env_prefix = prefix
|
|
36
|
+
env_val, field_key, value_is_complex = super().get_field_value(
|
|
37
|
+
field, field_name
|
|
38
|
+
)
|
|
39
|
+
if env_val is not None:
|
|
40
|
+
if prefix == "FASTMCP_SERVER_":
|
|
41
|
+
# Deprecated in 2.8.0
|
|
42
|
+
warnings.warn(
|
|
43
|
+
"Using `FASTMCP_SERVER_` environment variables is deprecated. Use `FASTMCP_` instead.",
|
|
44
|
+
DeprecationWarning,
|
|
45
|
+
stacklevel=2,
|
|
46
|
+
)
|
|
47
|
+
return env_val, field_key, value_is_complex
|
|
48
|
+
|
|
49
|
+
return super().get_field_value(field, field_name)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ExtendedSettingsConfigDict(SettingsConfigDict, total=False):
|
|
53
|
+
env_prefixes: list[str] | None
|
|
54
|
+
|
|
55
|
+
|
|
19
56
|
class Settings(BaseSettings):
|
|
20
57
|
"""FastMCP settings."""
|
|
21
58
|
|
|
22
|
-
model_config =
|
|
23
|
-
|
|
59
|
+
model_config = ExtendedSettingsConfigDict(
|
|
60
|
+
env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"],
|
|
24
61
|
env_file=".env",
|
|
25
62
|
extra="ignore",
|
|
26
63
|
env_nested_delimiter="__",
|
|
27
64
|
nested_model_default_partial_update=True,
|
|
28
65
|
)
|
|
29
66
|
|
|
67
|
+
@classmethod
|
|
68
|
+
def settings_customise_sources(
|
|
69
|
+
cls,
|
|
70
|
+
settings_cls: type[BaseSettings],
|
|
71
|
+
init_settings: PydanticBaseSettingsSource,
|
|
72
|
+
env_settings: PydanticBaseSettingsSource,
|
|
73
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
74
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
75
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
76
|
+
# can remove this classmethod after deprecated FASTMCP_SERVER_ prefix is
|
|
77
|
+
# removed
|
|
78
|
+
return (
|
|
79
|
+
init_settings,
|
|
80
|
+
ExtendedEnvSettingsSource(settings_cls),
|
|
81
|
+
dotenv_settings,
|
|
82
|
+
file_secret_settings,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def settings(self) -> Self:
|
|
87
|
+
"""
|
|
88
|
+
This property is for backwards compatibility with FastMCP < 2.8.0,
|
|
89
|
+
which accessed fastmcp.settings.settings
|
|
90
|
+
"""
|
|
91
|
+
# Deprecated in 2.8.0
|
|
92
|
+
warnings.warn(
|
|
93
|
+
"Using fastmcp.settings.settings is deprecated. Use fastmcp.settings instead.",
|
|
94
|
+
DeprecationWarning,
|
|
95
|
+
stacklevel=2,
|
|
96
|
+
)
|
|
97
|
+
return self
|
|
98
|
+
|
|
30
99
|
home: Path = Path.home() / ".fastmcp"
|
|
31
100
|
|
|
32
101
|
test_mode: bool = False
|
|
@@ -107,27 +176,6 @@ class Settings(BaseSettings):
|
|
|
107
176
|
|
|
108
177
|
return self
|
|
109
178
|
|
|
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
179
|
# HTTP settings
|
|
132
180
|
host: str = "127.0.0.1"
|
|
133
181
|
port: int = 8000
|
|
@@ -136,15 +184,6 @@ class ServerSettings(BaseSettings):
|
|
|
136
184
|
streamable_http_path: str = "/mcp"
|
|
137
185
|
debug: bool = False
|
|
138
186
|
|
|
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
187
|
# error handling
|
|
149
188
|
mask_error_details: Annotated[
|
|
150
189
|
bool,
|
|
@@ -162,7 +201,7 @@ class ServerSettings(BaseSettings):
|
|
|
162
201
|
),
|
|
163
202
|
] = False
|
|
164
203
|
|
|
165
|
-
|
|
204
|
+
server_dependencies: Annotated[
|
|
166
205
|
list[str],
|
|
167
206
|
Field(
|
|
168
207
|
default_factory=list,
|
|
@@ -170,9 +209,6 @@ class ServerSettings(BaseSettings):
|
|
|
170
209
|
),
|
|
171
210
|
] = []
|
|
172
211
|
|
|
173
|
-
# cache settings (for getting attributes from servers, used to avoid repeated calls)
|
|
174
|
-
cache_expiration_seconds: float = 0
|
|
175
|
-
|
|
176
212
|
# StreamableHTTP settings
|
|
177
213
|
json_response: bool = False
|
|
178
214
|
stateless_http: bool = (
|
|
@@ -198,5 +234,32 @@ class ServerSettings(BaseSettings):
|
|
|
198
234
|
),
|
|
199
235
|
] = None
|
|
200
236
|
|
|
237
|
+
include_tags: Annotated[
|
|
238
|
+
set[str] | None,
|
|
239
|
+
Field(
|
|
240
|
+
default=None,
|
|
241
|
+
description=inspect.cleandoc(
|
|
242
|
+
"""
|
|
243
|
+
If provided, only components that match these tags will be
|
|
244
|
+
exposed to clients. A component is considered to match if ANY of
|
|
245
|
+
its tags match ANY of the tags in the set.
|
|
246
|
+
"""
|
|
247
|
+
),
|
|
248
|
+
),
|
|
249
|
+
] = None
|
|
250
|
+
exclude_tags: Annotated[
|
|
251
|
+
set[str] | None,
|
|
252
|
+
Field(
|
|
253
|
+
default=None,
|
|
254
|
+
description=inspect.cleandoc(
|
|
255
|
+
"""
|
|
256
|
+
If provided, components that match these tags will be excluded
|
|
257
|
+
from the server. 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
|
+
|
|
201
264
|
|
|
202
265
|
settings = Settings()
|
fastmcp/tools/__init__.py
CHANGED
fastmcp/tools/tool.py
CHANGED
|
@@ -2,29 +2,28 @@ 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
10
|
from mcp.types import EmbeddedResource, ImageContent, 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
|
-
FastMCPBaseModel,
|
|
20
20
|
Image,
|
|
21
|
-
_convert_set_defaults,
|
|
22
21
|
find_kwarg_by_type,
|
|
23
22
|
get_cached_typeadapter,
|
|
24
23
|
)
|
|
25
24
|
|
|
26
25
|
if TYPE_CHECKING:
|
|
27
|
-
|
|
26
|
+
from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
|
|
28
27
|
|
|
29
28
|
logger = get_logger(__name__)
|
|
30
29
|
|
|
@@ -33,24 +32,13 @@ def default_serializer(data: Any) -> str:
|
|
|
33
32
|
return pydantic_core.to_json(data, fallback=str, indent=2).decode()
|
|
34
33
|
|
|
35
34
|
|
|
36
|
-
class Tool(
|
|
35
|
+
class Tool(FastMCPComponent):
|
|
37
36
|
"""Internal tool registration info."""
|
|
38
37
|
|
|
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
38
|
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
39
|
annotations: ToolAnnotations | None = Field(
|
|
48
40
|
default=None, description="Additional annotations about the tool"
|
|
49
41
|
)
|
|
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
42
|
serializer: Callable[[Any], str] | None = Field(
|
|
55
43
|
default=None, description="Optional custom serializer for tool results"
|
|
56
44
|
)
|
|
@@ -73,6 +61,7 @@ class Tool(FastMCPBaseModel, ABC):
|
|
|
73
61
|
annotations: ToolAnnotations | None = None,
|
|
74
62
|
exclude_args: list[str] | None = None,
|
|
75
63
|
serializer: Callable[[Any], str] | None = None,
|
|
64
|
+
enabled: bool | None = None,
|
|
76
65
|
) -> FunctionTool:
|
|
77
66
|
"""Create a Tool from a function."""
|
|
78
67
|
return FunctionTool.from_function(
|
|
@@ -83,21 +72,42 @@ class Tool(FastMCPBaseModel, ABC):
|
|
|
83
72
|
annotations=annotations,
|
|
84
73
|
exclude_args=exclude_args,
|
|
85
74
|
serializer=serializer,
|
|
75
|
+
enabled=enabled,
|
|
86
76
|
)
|
|
87
77
|
|
|
88
|
-
def __eq__(self, other: object) -> bool:
|
|
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
78
|
async def run(
|
|
96
79
|
self, arguments: dict[str, Any]
|
|
97
80
|
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
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,65 +122,24 @@ 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
145
|
async def run(
|
|
@@ -185,7 +154,7 @@ class FunctionTool(Tool):
|
|
|
185
154
|
if context_kwarg and context_kwarg not in arguments:
|
|
186
155
|
arguments[context_kwarg] = get_context()
|
|
187
156
|
|
|
188
|
-
if fastmcp.settings.
|
|
157
|
+
if fastmcp.settings.tool_attempt_parse_json_args:
|
|
189
158
|
# Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
|
|
190
159
|
# being passed in as JSON inside a string rather than an actual list.
|
|
191
160
|
#
|
|
@@ -222,6 +191,76 @@ class FunctionTool(Tool):
|
|
|
222
191
|
return _convert_to_content(result, serializer=self.serializer)
|
|
223
192
|
|
|
224
193
|
|
|
194
|
+
@dataclass
|
|
195
|
+
class ParsedFunction:
|
|
196
|
+
fn: Callable[..., Any]
|
|
197
|
+
name: str
|
|
198
|
+
description: str | None
|
|
199
|
+
parameters: dict[str, Any]
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def from_function(
|
|
203
|
+
cls,
|
|
204
|
+
fn: Callable[..., Any],
|
|
205
|
+
exclude_args: list[str] | None = None,
|
|
206
|
+
validate: bool = True,
|
|
207
|
+
) -> ParsedFunction:
|
|
208
|
+
from fastmcp.server.context import Context
|
|
209
|
+
|
|
210
|
+
if validate:
|
|
211
|
+
sig = inspect.signature(fn)
|
|
212
|
+
# Reject functions with *args or **kwargs
|
|
213
|
+
for param in sig.parameters.values():
|
|
214
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
215
|
+
raise ValueError("Functions with *args are not supported as tools")
|
|
216
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
"Functions with **kwargs are not supported as tools"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Reject exclude_args that don't exist in the function or don't have a default value
|
|
222
|
+
if exclude_args:
|
|
223
|
+
for arg_name in exclude_args:
|
|
224
|
+
if arg_name not in sig.parameters:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"Parameter '{arg_name}' in exclude_args does not exist in function."
|
|
227
|
+
)
|
|
228
|
+
param = sig.parameters[arg_name]
|
|
229
|
+
if param.default == inspect.Parameter.empty:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"Parameter '{arg_name}' in exclude_args must have a default value."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# collect name and doc before we potentially modify the function
|
|
235
|
+
fn_name = getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
236
|
+
fn_doc = fn.__doc__
|
|
237
|
+
|
|
238
|
+
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
239
|
+
if not inspect.isroutine(fn):
|
|
240
|
+
fn = fn.__call__
|
|
241
|
+
# if the fn is a staticmethod, we need to work with the underlying function
|
|
242
|
+
if isinstance(fn, staticmethod):
|
|
243
|
+
fn = fn.__func__
|
|
244
|
+
|
|
245
|
+
type_adapter = get_cached_typeadapter(fn)
|
|
246
|
+
schema = type_adapter.json_schema()
|
|
247
|
+
|
|
248
|
+
prune_params: list[str] = []
|
|
249
|
+
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
250
|
+
if context_kwarg:
|
|
251
|
+
prune_params.append(context_kwarg)
|
|
252
|
+
if exclude_args:
|
|
253
|
+
prune_params.extend(exclude_args)
|
|
254
|
+
|
|
255
|
+
schema = compress_schema(schema, prune_params=prune_params)
|
|
256
|
+
return cls(
|
|
257
|
+
fn=fn,
|
|
258
|
+
name=fn_name,
|
|
259
|
+
description=fn_doc,
|
|
260
|
+
parameters=schema,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
225
264
|
def _convert_to_content(
|
|
226
265
|
result: Any,
|
|
227
266
|
serializer: Callable[[Any], str] | None = None,
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
|
|
7
7
|
from mcp.types import EmbeddedResource, ImageContent, TextContent, 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
|
|
@@ -23,10 +24,10 @@ class ToolManager:
|
|
|
23
24
|
def __init__(
|
|
24
25
|
self,
|
|
25
26
|
duplicate_behavior: DuplicateBehavior | None = None,
|
|
26
|
-
mask_error_details: bool =
|
|
27
|
+
mask_error_details: bool | None = None,
|
|
27
28
|
):
|
|
28
29
|
self._tools: dict[str, Tool] = {}
|
|
29
|
-
self.mask_error_details = mask_error_details
|
|
30
|
+
self.mask_error_details = mask_error_details or settings.mask_error_details
|
|
30
31
|
|
|
31
32
|
# Default to "warn" if None is provided
|
|
32
33
|
if duplicate_behavior is None:
|