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/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 = SettingsConfigDict(
23
- env_prefix="FASTMCP_",
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
- dependencies: Annotated[
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
@@ -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,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 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
- from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
10
+ from mcp.types import 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
+ Audio,
20
21
  Image,
21
- _convert_set_defaults,
22
+ MCPContent,
22
23
  find_kwarg_by_type,
23
24
  get_cached_typeadapter,
24
25
  )
25
26
 
26
27
  if TYPE_CHECKING:
27
- pass
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(FastMCPBaseModel, ABC):
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 __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
- 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
- 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
- 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.settings.tool_attempt_parse_json_args:
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[TextContent | ImageContent | EmbeddedResource]:
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, TextContent | ImageContent | EmbeddedResource):
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, TextContent | ImageContent | EmbeddedResource | Image):
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)
@@ -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 EmbeddedResource, ImageContent, TextContent, ToolAnnotations
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 = False,
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
- warnings.warn(
74
- "ToolManager.add_tool_from_fn() is deprecated. Use Tool.from_function() and call add_tool() instead.",
75
- DeprecationWarning,
76
- stacklevel=2,
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: