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/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 = SettingsConfigDict(
23
- env_prefix="FASTMCP_",
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
- dependencies: Annotated[
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
@@ -1,4 +1,5 @@
1
1
  from .tool import Tool, FunctionTool
2
2
  from .tool_manager import ToolManager
3
+ from .tool_transform import forward, forward_raw
3
4
 
4
- __all__ = ["Tool", "ToolManager", "FunctionTool"]
5
+ __all__ = ["Tool", "ToolManager", "FunctionTool", "forward", "forward_raw"]
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 typing import TYPE_CHECKING, Annotated, Any
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 BeforeValidator, Field
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
- pass
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(FastMCPBaseModel, ABC):
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
- func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
129
+ parsed_fn = ParsedFunction.from_function(fn, exclude_args=exclude_args)
140
130
 
141
- if func_name == "<lambda>":
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=func_name,
168
- description=func_doc,
169
- parameters=schema,
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.settings.tool_attempt_parse_json_args:
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,
@@ -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 = False,
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: