Jeremiah Lowin commited on
Commit
927e044
·
1 Parent(s): 6b83cfc

Remove open-ended and server-specific settings

Browse files
docs/servers/composition.mdx CHANGED
@@ -215,7 +215,7 @@ You can configure the prefix format globally in code:
215
 
216
  ```python
217
  import fastmcp
218
- fastmcp.settings.settings.resource_prefix_format = "protocol"
219
  ```
220
 
221
  Or via environment variable:
 
215
 
216
  ```python
217
  import fastmcp
218
+ fastmcp.settings.resource_prefix_format = "protocol"
219
  ```
220
 
221
  Or via environment variable:
src/fastmcp/__init__.py CHANGED
@@ -1,6 +1,9 @@
1
  """FastMCP - An ergonomic MCP interface."""
2
 
3
  from importlib.metadata import version
 
 
 
4
 
5
  from fastmcp.server.server import FastMCP
6
  from fastmcp.server.context import Context
@@ -8,7 +11,7 @@ import fastmcp.server
8
 
9
  from fastmcp.client import Client
10
  from fastmcp.utilities.types import Image
11
- from . import client, settings
12
 
13
  __version__ = version("fastmcp")
14
  __all__ = [
 
1
  """FastMCP - An ergonomic MCP interface."""
2
 
3
  from importlib.metadata import version
4
+ from fastmcp.settings import Settings
5
+
6
+ settings = Settings()
7
 
8
  from fastmcp.server.server import FastMCP
9
  from fastmcp.server.context import Context
 
11
 
12
  from fastmcp.client import Client
13
  from fastmcp.utilities.types import Image
14
+ from . import client
15
 
16
  __version__ = version("fastmcp")
17
  __all__ = [
src/fastmcp/cli/cli.py CHANGED
@@ -18,6 +18,7 @@ from typer import Context, Exit
18
  import fastmcp
19
  from fastmcp.cli import claude
20
  from fastmcp.cli import run as run_module
 
21
  from fastmcp.utilities.logging import get_logger
22
 
23
  logger = get_logger("cli")
@@ -165,8 +166,8 @@ def dev(
165
 
166
  try:
167
  # Import server to get dependencies
168
- server = run_module.import_server(file, server_object)
169
- if hasattr(server, "dependencies") and server.dependencies is not None:
170
  with_packages = list(set(with_packages + server.dependencies))
171
 
172
  env_vars = {}
 
18
  import fastmcp
19
  from fastmcp.cli import claude
20
  from fastmcp.cli import run as run_module
21
+ from fastmcp.server.server import FastMCP
22
  from fastmcp.utilities.logging import get_logger
23
 
24
  logger = get_logger("cli")
 
166
 
167
  try:
168
  # Import server to get dependencies
169
+ server: FastMCP = run_module.import_server(file, server_object)
170
+ if server.dependencies is not None:
171
  with_packages = list(set(with_packages + server.dependencies))
172
 
173
  env_vars = {}
src/fastmcp/client/auth/oauth.py CHANGED
@@ -23,10 +23,10 @@ from mcp.shared.auth import (
23
  )
24
  from pydantic import AnyHttpUrl, ValidationError
25
 
 
26
  from fastmcp.client.oauth_callback import (
27
  create_oauth_callback_server,
28
  )
29
- from fastmcp.settings import settings as fastmcp_global_settings
30
  from fastmcp.utilities.http import find_available_port
31
  from fastmcp.utilities.logging import get_logger
32
 
 
23
  )
24
  from pydantic import AnyHttpUrl, ValidationError
25
 
26
+ from fastmcp import settings as fastmcp_global_settings
27
  from fastmcp.client.oauth_callback import (
28
  create_oauth_callback_server,
29
  )
 
30
  from fastmcp.utilities.http import find_available_port
31
  from fastmcp.utilities.logging import get_logger
32
 
src/fastmcp/client/client.py CHANGED
@@ -165,7 +165,7 @@ class Client(Generic[ClientTransportT]):
165
 
166
  # handle init handshake timeout
167
  if init_timeout is None:
168
- init_timeout = fastmcp.settings.settings.client_init_timeout
169
  if isinstance(init_timeout, datetime.timedelta):
170
  init_timeout = init_timeout.total_seconds()
171
  elif not init_timeout:
 
165
 
166
  # handle init handshake timeout
167
  if init_timeout is None:
168
+ init_timeout = fastmcp.settings.client_init_timeout
169
  if isinstance(init_timeout, datetime.timedelta):
170
  init_timeout = init_timeout.total_seconds()
171
  elif not init_timeout:
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
6
 
7
  from mcp import GetPromptResult
8
 
 
9
  from fastmcp.exceptions import NotFoundError, PromptError
10
  from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
11
  from fastmcp.settings import DuplicateBehavior
@@ -23,10 +24,10 @@ class PromptManager:
23
  def __init__(
24
  self,
25
  duplicate_behavior: DuplicateBehavior | None = None,
26
- mask_error_details: bool = False,
27
  ):
28
  self._prompts: dict[str, Prompt] = {}
29
- self.mask_error_details = mask_error_details
30
 
31
  # Default to "warn" if None is provided
32
  if duplicate_behavior is None:
 
6
 
7
  from mcp import GetPromptResult
8
 
9
+ from fastmcp import settings
10
  from fastmcp.exceptions import NotFoundError, PromptError
11
  from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
12
  from fastmcp.settings import DuplicateBehavior
 
24
  def __init__(
25
  self,
26
  duplicate_behavior: DuplicateBehavior | None = None,
27
+ mask_error_details: bool | None = None,
28
  ):
29
  self._prompts: dict[str, Prompt] = {}
30
+ self.mask_error_details = mask_error_details or settings.mask_error_details
31
 
32
  # Default to "warn" if None is provided
33
  if duplicate_behavior is None:
src/fastmcp/resources/resource_manager.py CHANGED
@@ -7,6 +7,7 @@ from typing import Any
7
 
8
  from pydantic import AnyUrl
9
 
 
10
  from fastmcp.exceptions import NotFoundError, ResourceError
11
  from fastmcp.resources.resource import Resource
12
  from fastmcp.resources.template import (
@@ -25,7 +26,7 @@ class ResourceManager:
25
  def __init__(
26
  self,
27
  duplicate_behavior: DuplicateBehavior | None = None,
28
- mask_error_details: bool = False,
29
  ):
30
  """Initialize the ResourceManager.
31
 
@@ -37,7 +38,7 @@ class ResourceManager:
37
  """
38
  self._resources: dict[str, Resource] = {}
39
  self._templates: dict[str, ResourceTemplate] = {}
40
- self.mask_error_details = mask_error_details
41
 
42
  # Default to "warn" if None is provided
43
  if duplicate_behavior is None:
 
7
 
8
  from pydantic import AnyUrl
9
 
10
+ from fastmcp import settings
11
  from fastmcp.exceptions import NotFoundError, ResourceError
12
  from fastmcp.resources.resource import Resource
13
  from fastmcp.resources.template import (
 
26
  def __init__(
27
  self,
28
  duplicate_behavior: DuplicateBehavior | None = None,
29
+ mask_error_details: bool | None = None,
30
  ):
31
  """Initialize the ResourceManager.
32
 
 
38
  """
39
  self._resources: dict[str, Resource] = {}
40
  self._templates: dict[str, ResourceTemplate] = {}
41
+ self.mask_error_details = mask_error_details or settings.mask_error_details
42
 
43
  # Default to "warn" if None is provided
44
  if duplicate_behavior is None:
src/fastmcp/server/server.py CHANGED
@@ -43,7 +43,6 @@ from starlette.routing import BaseRoute, Route
43
 
44
  import fastmcp
45
  import fastmcp.server
46
- import fastmcp.settings
47
  from fastmcp.exceptions import NotFoundError
48
  from fastmcp.prompts import Prompt, PromptManager
49
  from fastmcp.prompts.prompt import FunctionPrompt
@@ -56,6 +55,7 @@ from fastmcp.server.http import (
56
  create_sse_app,
57
  create_streamable_http_app,
58
  )
 
59
  from fastmcp.tools import ToolManager
60
  from fastmcp.tools.tool import FunctionTool, Tool
61
  from fastmcp.utilities.cache import TimedCache
@@ -121,7 +121,6 @@ class FastMCP(Generic[LifespanResultT]):
121
  | None
122
  ) = None,
123
  tags: set[str] | None = None,
124
- dependencies: list[str] | None = None,
125
  tool_serializer: Callable[[Any], str] | None = None,
126
  cache_expiration_seconds: float | None = None,
127
  on_duplicate_tools: DuplicateBehavior | None = None,
@@ -130,44 +129,44 @@ class FastMCP(Generic[LifespanResultT]):
130
  resource_prefix_format: Literal["protocol", "path"] | None = None,
131
  mask_error_details: bool | None = None,
132
  tools: list[Tool | Callable[..., Any]] | None = None,
133
- **settings: Any,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  ):
135
- if cache_expiration_seconds is not None:
136
- settings["cache_expiration_seconds"] = cache_expiration_seconds
137
- self.settings = fastmcp.settings.ServerSettings(**settings)
138
-
139
- # If mask_error_details is provided, override the settings value
140
- if mask_error_details is not None:
141
- self.settings.mask_error_details = mask_error_details
142
-
143
- self.resource_prefix_format: Literal["protocol", "path"]
144
- if resource_prefix_format is None:
145
- self.resource_prefix_format = (
146
- fastmcp.settings.settings.resource_prefix_format
147
- )
148
- else:
149
- self.resource_prefix_format = resource_prefix_format
150
 
151
  self.tags: set[str] = tags or set()
152
- self.dependencies = dependencies
153
  self._cache = TimedCache(
154
- expiration=datetime.timedelta(
155
- seconds=self.settings.cache_expiration_seconds
156
- )
157
  )
158
  self._mounted_servers: dict[str, MountedServer] = {}
159
  self._additional_http_routes: list[BaseRoute] = []
160
  self._tool_manager = ToolManager(
161
  duplicate_behavior=on_duplicate_tools,
162
- mask_error_details=self.settings.mask_error_details,
163
  )
164
  self._resource_manager = ResourceManager(
165
  duplicate_behavior=on_duplicate_resources,
166
- mask_error_details=self.settings.mask_error_details,
167
  )
168
  self._prompt_manager = PromptManager(
169
  duplicate_behavior=on_duplicate_prompts,
170
- mask_error_details=self.settings.mask_error_details,
171
  )
172
  self._tool_serializer = tool_serializer
173
 
@@ -182,7 +181,7 @@ class FastMCP(Generic[LifespanResultT]):
182
  lifespan=_lifespan_wrapper(self, lifespan),
183
  )
184
 
185
- if auth is None and self.settings.default_auth_provider == "bearer_env":
186
  auth = EnvBearerAuthProvider()
187
  self.auth = auth
188
 
@@ -194,10 +193,62 @@ class FastMCP(Generic[LifespanResultT]):
194
 
195
  # Set up MCP protocol handlers
196
  self._setup_handlers()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
  def __repr__(self) -> str:
199
  return f"{type(self).__name__}({self.name!r})"
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  @property
202
  def name(self) -> str:
203
  return self._mcp_server.name
@@ -1008,9 +1059,11 @@ class FastMCP(Generic[LifespanResultT]):
1008
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1009
  uvicorn_config: Additional configuration for the Uvicorn server
1010
  """
1011
- host = host or self.settings.host
1012
- port = port or self.settings.port
1013
- default_log_level_to_use = (log_level or self.settings.log_level).lower()
 
 
1014
 
1015
  app = self.http_app(path=path, transport=transport, middleware=middleware)
1016
 
@@ -1084,10 +1137,10 @@ class FastMCP(Generic[LifespanResultT]):
1084
  )
1085
  return create_sse_app(
1086
  server=self,
1087
- message_path=message_path or self.settings.message_path,
1088
- sse_path=path or self.settings.sse_path,
1089
  auth=self.auth,
1090
- debug=self.settings.debug,
1091
  middleware=middleware,
1092
  )
1093
 
@@ -1115,6 +1168,8 @@ class FastMCP(Generic[LifespanResultT]):
1115
  self,
1116
  path: str | None = None,
1117
  middleware: list[Middleware] | None = None,
 
 
1118
  transport: Literal["streamable-http", "sse"] = "streamable-http",
1119
  ) -> StarletteWithLifespan:
1120
  """Create a Starlette app using the specified HTTP transport.
@@ -1131,21 +1186,22 @@ class FastMCP(Generic[LifespanResultT]):
1131
  if transport == "streamable-http":
1132
  return create_streamable_http_app(
1133
  server=self,
1134
- streamable_http_path=path or self.settings.streamable_http_path,
 
1135
  event_store=None,
1136
  auth=self.auth,
1137
- json_response=self.settings.json_response,
1138
- stateless_http=self.settings.stateless_http,
1139
- debug=self.settings.debug,
1140
  middleware=middleware,
1141
  )
1142
  elif transport == "sse":
1143
  return create_sse_app(
1144
  server=self,
1145
- message_path=self.settings.message_path,
1146
- sse_path=path or self.settings.sse_path,
1147
  auth=self.auth,
1148
- debug=self.settings.debug,
1149
  middleware=middleware,
1150
  )
1151
 
@@ -1597,7 +1653,7 @@ def add_resource_prefix(
1597
  # Get the server settings to check for legacy format preference
1598
 
1599
  if prefix_format is None:
1600
- prefix_format = fastmcp.settings.settings.resource_prefix_format
1601
 
1602
  if prefix_format == "protocol":
1603
  # Legacy style: prefix+protocol://path
@@ -1646,7 +1702,7 @@ def remove_resource_prefix(
1646
  return uri
1647
 
1648
  if prefix_format is None:
1649
- prefix_format = fastmcp.settings.settings.resource_prefix_format
1650
 
1651
  if prefix_format == "protocol":
1652
  # Legacy style: prefix+protocol://path
@@ -1706,7 +1762,7 @@ def has_resource_prefix(
1706
  # Get the server settings to check for legacy format preference
1707
 
1708
  if prefix_format is None:
1709
- prefix_format = fastmcp.settings.settings.resource_prefix_format
1710
 
1711
  if prefix_format == "protocol":
1712
  # Legacy style: prefix+protocol://path
 
43
 
44
  import fastmcp
45
  import fastmcp.server
 
46
  from fastmcp.exceptions import NotFoundError
47
  from fastmcp.prompts import Prompt, PromptManager
48
  from fastmcp.prompts.prompt import FunctionPrompt
 
55
  create_sse_app,
56
  create_streamable_http_app,
57
  )
58
+ from fastmcp.settings import Settings
59
  from fastmcp.tools import ToolManager
60
  from fastmcp.tools.tool import FunctionTool, Tool
61
  from fastmcp.utilities.cache import TimedCache
 
121
  | None
122
  ) = None,
123
  tags: set[str] | None = None,
 
124
  tool_serializer: Callable[[Any], str] | None = None,
125
  cache_expiration_seconds: float | None = None,
126
  on_duplicate_tools: DuplicateBehavior | None = None,
 
129
  resource_prefix_format: Literal["protocol", "path"] | None = None,
130
  mask_error_details: bool | None = None,
131
  tools: list[Tool | Callable[..., Any]] | None = None,
132
+ dependencies: list[str] | None = None,
133
+ # ---
134
+ # ---
135
+ # --- The following arguments are DEPRECATED ---
136
+ # ---
137
+ # ---
138
+ log_level: str | None = None,
139
+ debug: bool | None = None,
140
+ host: str | None = None,
141
+ port: int | None = None,
142
+ sse_path: str | None = None,
143
+ message_path: str | None = None,
144
+ streamable_http_path: str | None = None,
145
+ json_response: bool | None = None,
146
+ stateless_http: bool | None = None,
147
  ):
148
+ self.resource_prefix_format: Literal["protocol", "path"] = (
149
+ resource_prefix_format or fastmcp.settings.resource_prefix_format
150
+ )
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  self.tags: set[str] = tags or set()
153
+
154
  self._cache = TimedCache(
155
+ expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
 
 
156
  )
157
  self._mounted_servers: dict[str, MountedServer] = {}
158
  self._additional_http_routes: list[BaseRoute] = []
159
  self._tool_manager = ToolManager(
160
  duplicate_behavior=on_duplicate_tools,
161
+ mask_error_details=mask_error_details,
162
  )
163
  self._resource_manager = ResourceManager(
164
  duplicate_behavior=on_duplicate_resources,
165
+ mask_error_details=mask_error_details,
166
  )
167
  self._prompt_manager = PromptManager(
168
  duplicate_behavior=on_duplicate_prompts,
169
+ mask_error_details=mask_error_details,
170
  )
171
  self._tool_serializer = tool_serializer
172
 
 
181
  lifespan=_lifespan_wrapper(self, lifespan),
182
  )
183
 
184
+ if auth is None and fastmcp.settings.default_auth_provider == "bearer_env":
185
  auth = EnvBearerAuthProvider()
186
  self.auth = auth
187
 
 
193
 
194
  # Set up MCP protocol handlers
195
  self._setup_handlers()
196
+ self.dependencies = dependencies or fastmcp.settings.server_dependencies
197
+
198
+ # handle deprecated settings
199
+ self._handle_deprecated_settings(
200
+ log_level=log_level,
201
+ debug=debug,
202
+ host=host,
203
+ port=port,
204
+ sse_path=sse_path,
205
+ message_path=message_path,
206
+ streamable_http_path=streamable_http_path,
207
+ json_response=json_response,
208
+ stateless_http=stateless_http,
209
+ )
210
 
211
  def __repr__(self) -> str:
212
  return f"{type(self).__name__}({self.name!r})"
213
 
214
+ def _handle_deprecated_settings(
215
+ self,
216
+ log_level: str | None,
217
+ debug: bool | None,
218
+ host: str | None,
219
+ port: int | None,
220
+ sse_path: str | None,
221
+ message_path: str | None,
222
+ streamable_http_path: str | None,
223
+ json_response: bool | None,
224
+ stateless_http: bool | None,
225
+ ) -> None:
226
+ """Handle deprecated settings. Deprecated in 2.8.0."""
227
+ deprecated_settings: dict[str, Any] = {}
228
+
229
+ for name, arg in [
230
+ ("log_level", log_level),
231
+ ("debug", debug),
232
+ ("host", host),
233
+ ("port", port),
234
+ ("sse_path", sse_path),
235
+ ("message_path", message_path),
236
+ ("streamable_http_path", streamable_http_path),
237
+ ("json_response", json_response),
238
+ ("stateless_http", stateless_http),
239
+ ]:
240
+ if arg is not None:
241
+ # Deprecated in 2.8.0
242
+ warnings.warn(
243
+ f"Providing `{name}` when creating a server is deprecated. Provide it when calling `run` or as a global setting instead.",
244
+ DeprecationWarning,
245
+ stacklevel=2,
246
+ )
247
+ deprecated_settings[name] = arg
248
+
249
+ combined_settings = fastmcp.settings.model_dump() | deprecated_settings
250
+ self._deprecated_settings = Settings(**combined_settings)
251
+
252
  @property
253
  def name(self) -> str:
254
  return self._mcp_server.name
 
1059
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1060
  uvicorn_config: Additional configuration for the Uvicorn server
1061
  """
1062
+ host = host or self._deprecated_settings.host
1063
+ port = port or self._deprecated_settings.port
1064
+ default_log_level_to_use = (
1065
+ log_level or self._deprecated_settings.log_level
1066
+ ).lower()
1067
 
1068
  app = self.http_app(path=path, transport=transport, middleware=middleware)
1069
 
 
1137
  )
1138
  return create_sse_app(
1139
  server=self,
1140
+ message_path=message_path or self._deprecated_settings.message_path,
1141
+ sse_path=path or self._deprecated_settings.sse_path,
1142
  auth=self.auth,
1143
+ debug=self._deprecated_settings.debug,
1144
  middleware=middleware,
1145
  )
1146
 
 
1168
  self,
1169
  path: str | None = None,
1170
  middleware: list[Middleware] | None = None,
1171
+ json_response: bool | None = None,
1172
+ stateless_http: bool | None = None,
1173
  transport: Literal["streamable-http", "sse"] = "streamable-http",
1174
  ) -> StarletteWithLifespan:
1175
  """Create a Starlette app using the specified HTTP transport.
 
1186
  if transport == "streamable-http":
1187
  return create_streamable_http_app(
1188
  server=self,
1189
+ streamable_http_path=path
1190
+ or self._deprecated_settings.streamable_http_path,
1191
  event_store=None,
1192
  auth=self.auth,
1193
+ json_response=self._deprecated_settings.json_response,
1194
+ stateless_http=self._deprecated_settings.stateless_http,
1195
+ debug=self._deprecated_settings.debug,
1196
  middleware=middleware,
1197
  )
1198
  elif transport == "sse":
1199
  return create_sse_app(
1200
  server=self,
1201
+ message_path=self._deprecated_settings.message_path,
1202
+ sse_path=path or self._deprecated_settings.sse_path,
1203
  auth=self.auth,
1204
+ debug=self._deprecated_settings.debug,
1205
  middleware=middleware,
1206
  )
1207
 
 
1653
  # Get the server settings to check for legacy format preference
1654
 
1655
  if prefix_format is None:
1656
+ prefix_format = fastmcp.settings.resource_prefix_format
1657
 
1658
  if prefix_format == "protocol":
1659
  # Legacy style: prefix+protocol://path
 
1702
  return uri
1703
 
1704
  if prefix_format is None:
1705
+ prefix_format = fastmcp.settings.resource_prefix_format
1706
 
1707
  if prefix_format == "protocol":
1708
  # Legacy style: prefix+protocol://path
 
1762
  # Get the server settings to check for legacy format preference
1763
 
1764
  if prefix_format is None:
1765
+ prefix_format = fastmcp.settings.resource_prefix_format
1766
 
1767
  if prefix_format == "protocol":
1768
  # Legacy style: prefix+protocol://path
src/fastmcp/settings.py CHANGED
@@ -1,16 +1,47 @@
1
  from __future__ import annotations as _annotations
2
 
3
  import inspect
 
4
  from pathlib import Path
5
- from typing import Annotated, Literal
6
 
7
  from pydantic import Field, model_validator
 
8
  from pydantic_settings import (
9
  BaseSettings,
 
 
10
  SettingsConfigDict,
11
  )
12
  from typing_extensions import Self
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
15
 
16
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
@@ -19,14 +50,30 @@ DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
19
  class Settings(BaseSettings):
20
  """FastMCP settings."""
21
 
22
- model_config = SettingsConfigDict(
23
- env_prefix="FASTMCP_",
24
  env_file=".env",
25
  extra="ignore",
26
  env_nested_delimiter="__",
27
  nested_model_default_partial_update=True,
28
  )
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  home: Path = Path.home() / ".fastmcp"
31
 
32
  test_mode: bool = False
@@ -107,27 +154,6 @@ class Settings(BaseSettings):
107
 
108
  return self
109
 
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
  # HTTP settings
132
  host: str = "127.0.0.1"
133
  port: int = 8000
@@ -136,15 +162,6 @@ class ServerSettings(BaseSettings):
136
  streamable_http_path: str = "/mcp"
137
  debug: bool = False
138
 
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
  # error handling
149
  mask_error_details: Annotated[
150
  bool,
@@ -162,7 +179,7 @@ class ServerSettings(BaseSettings):
162
  ),
163
  ] = False
164
 
165
- dependencies: Annotated[
166
  list[str],
167
  Field(
168
  default_factory=list,
@@ -170,9 +187,6 @@ class ServerSettings(BaseSettings):
170
  ),
171
  ] = []
172
 
173
- # cache settings (for getting attributes from servers, used to avoid repeated calls)
174
- cache_expiration_seconds: float = 0
175
-
176
  # StreamableHTTP settings
177
  json_response: bool = False
178
  stateless_http: bool = (
@@ -197,6 +211,3 @@ class ServerSettings(BaseSettings):
197
  ),
198
  ),
199
  ] = None
200
-
201
-
202
- settings = Settings()
 
1
  from __future__ import annotations as _annotations
2
 
3
  import inspect
4
+ import warnings
5
  from pathlib import Path
6
+ from typing import Annotated, Any, Literal
7
 
8
  from pydantic import Field, model_validator
9
+ from pydantic.fields import FieldInfo
10
  from pydantic_settings import (
11
  BaseSettings,
12
+ EnvSettingsSource,
13
+ PydanticBaseSettingsSource,
14
  SettingsConfigDict,
15
  )
16
  from typing_extensions import Self
17
 
18
+
19
+ class ExtendedEnvSettingsSource(EnvSettingsSource):
20
+ def get_field_value(
21
+ self, field: FieldInfo, field_name: str
22
+ ) -> tuple[Any, str, bool]:
23
+ if prefixes := self.config.get("env_prefixes"):
24
+ for prefix in prefixes:
25
+ self.env_prefix = prefix
26
+ env_val, field_key, value_is_complex = super().get_field_value(
27
+ field, field_name
28
+ )
29
+ if env_val is not None:
30
+ if prefix == "FASTMCP_SERVER_":
31
+ warnings.warn(
32
+ "Using `FASTMCP_SERVER_` environment variables is deprecated. Use `FASTMCP_` instead.",
33
+ DeprecationWarning,
34
+ stacklevel=2,
35
+ )
36
+ return env_val, field_key, value_is_complex
37
+
38
+ return super().get_field_value(field, field_name)
39
+
40
+
41
+ class ExtendedSettingsConfigDict(SettingsConfigDict, total=False):
42
+ env_prefixes: list[str] | None
43
+
44
+
45
  LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
46
 
47
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
 
50
  class Settings(BaseSettings):
51
  """FastMCP settings."""
52
 
53
+ model_config = ExtendedSettingsConfigDict(
54
+ env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"],
55
  env_file=".env",
56
  extra="ignore",
57
  env_nested_delimiter="__",
58
  nested_model_default_partial_update=True,
59
  )
60
 
61
+ @classmethod
62
+ def settings_customise_sources(
63
+ cls,
64
+ settings_cls: type[BaseSettings],
65
+ init_settings: PydanticBaseSettingsSource,
66
+ env_settings: PydanticBaseSettingsSource,
67
+ dotenv_settings: PydanticBaseSettingsSource,
68
+ file_secret_settings: PydanticBaseSettingsSource,
69
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
70
+ return (
71
+ init_settings,
72
+ ExtendedEnvSettingsSource(settings_cls),
73
+ dotenv_settings,
74
+ file_secret_settings,
75
+ )
76
+
77
  home: Path = Path.home() / ".fastmcp"
78
 
79
  test_mode: bool = False
 
154
 
155
  return self
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  # HTTP settings
158
  host: str = "127.0.0.1"
159
  port: int = 8000
 
162
  streamable_http_path: str = "/mcp"
163
  debug: bool = False
164
 
 
 
 
 
 
 
 
 
 
165
  # error handling
166
  mask_error_details: Annotated[
167
  bool,
 
179
  ),
180
  ] = False
181
 
182
+ server_dependencies: Annotated[
183
  list[str],
184
  Field(
185
  default_factory=list,
 
187
  ),
188
  ] = []
189
 
 
 
 
190
  # StreamableHTTP settings
191
  json_response: bool = False
192
  stateless_http: bool = (
 
211
  ),
212
  ),
213
  ] = None
 
 
 
src/fastmcp/tools/tool.py CHANGED
@@ -185,7 +185,7 @@ class FunctionTool(Tool):
185
  if context_kwarg and context_kwarg not in arguments:
186
  arguments[context_kwarg] = get_context()
187
 
188
- if fastmcp.settings.settings.tool_attempt_parse_json_args:
189
  # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
190
  # being passed in as JSON inside a string rather than an actual list.
191
  #
 
185
  if context_kwarg and context_kwarg not in arguments:
186
  arguments[context_kwarg] = get_context()
187
 
188
+ if fastmcp.settings.tool_attempt_parse_json_args:
189
  # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
190
  # being passed in as JSON inside a string rather than an actual list.
191
  #
src/fastmcp/tools/tool_manager.py CHANGED
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
6
 
7
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
8
 
 
9
  from fastmcp.exceptions import NotFoundError, ToolError
10
  from fastmcp.settings import DuplicateBehavior
11
  from fastmcp.tools.tool import Tool
@@ -23,10 +24,10 @@ class ToolManager:
23
  def __init__(
24
  self,
25
  duplicate_behavior: DuplicateBehavior | None = None,
26
- mask_error_details: bool = False,
27
  ):
28
  self._tools: dict[str, Tool] = {}
29
- self.mask_error_details = mask_error_details
30
 
31
  # Default to "warn" if None is provided
32
  if duplicate_behavior is None:
 
6
 
7
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
8
 
9
+ from fastmcp import settings
10
  from fastmcp.exceptions import NotFoundError, ToolError
11
  from fastmcp.settings import DuplicateBehavior
12
  from fastmcp.tools.tool import Tool
 
24
  def __init__(
25
  self,
26
  duplicate_behavior: DuplicateBehavior | None = None,
27
+ mask_error_details: bool | None = None,
28
  ):
29
  self._tools: dict[str, Tool] = {}
30
+ self.mask_error_details = mask_error_details or settings.mask_error_details
31
 
32
  # Default to "warn" if None is provided
33
  if duplicate_behavior is None:
src/fastmcp/utilities/exceptions.py CHANGED
@@ -43,7 +43,7 @@ def get_catch_handlers() -> Mapping[
43
  type[BaseException] | Iterable[type[BaseException]],
44
  Callable[[BaseExceptionGroup[Any]], Any],
45
  ]:
46
- if fastmcp.settings.settings.client_raise_first_exceptiongroup_error:
47
  return _catch_handlers
48
  else:
49
  return {}
 
43
  type[BaseException] | Iterable[type[BaseException]],
44
  Callable[[BaseExceptionGroup[Any]], Any],
45
  ]:
46
+ if fastmcp.settings.client_raise_first_exceptiongroup_error:
47
  return _catch_handlers
48
  else:
49
  return {}
src/fastmcp/utilities/tests.py CHANGED
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Literal
10
 
11
  import uvicorn
12
 
13
- from fastmcp.settings import settings
14
  from fastmcp.utilities.http import find_available_port
15
 
16
  if TYPE_CHECKING:
@@ -32,8 +32,8 @@ def temporary_settings(**kwargs: Any):
32
  from fastmcp.utilities.tests import temporary_settings
33
 
34
  with temporary_settings(log_level='DEBUG'):
35
- assert fastmcp.settings.settings.log_level == 'DEBUG'
36
- assert fastmcp.settings.settings.log_level == 'INFO'
37
  ```
38
  """
39
  old_settings = copy.deepcopy(settings.model_dump())
 
10
 
11
  import uvicorn
12
 
13
+ from fastmcp import settings
14
  from fastmcp.utilities.http import find_available_port
15
 
16
  if TYPE_CHECKING:
 
32
  from fastmcp.utilities.tests import temporary_settings
33
 
34
  with temporary_settings(log_level='DEBUG'):
35
+ assert fastmcp.settings.log_level == 'DEBUG'
36
+ assert fastmcp.settings.log_level == 'INFO'
37
  ```
38
  """
39
  old_settings = copy.deepcopy(settings.model_dump())
tests/auth/providers/test_bearer_env.py CHANGED
@@ -4,6 +4,8 @@ from pydantic import AnyHttpUrl, ValidationError
4
  from fastmcp import FastMCP
5
  from fastmcp.server.auth.providers.bearer import BearerAuthProvider
6
  from fastmcp.server.auth.providers.bearer_env import EnvBearerAuthProvider
 
 
7
 
8
 
9
  def test_load_bearer_env_from_env_var(monkeypatch):
@@ -13,7 +15,8 @@ def test_load_bearer_env_from_env_var(monkeypatch):
13
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
14
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
15
 
16
- mcp_with_auth = FastMCP()
 
17
  assert isinstance(mcp_with_auth.auth, EnvBearerAuthProvider)
18
 
19
 
@@ -23,10 +26,11 @@ def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatc
23
 
24
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
25
 
26
- with pytest.raises(
27
- ValueError, match="Either public_key or jwks_uri must be provided"
28
- ):
29
- FastMCP()
 
30
 
31
 
32
  def test_configure_bearer_env_from_env_var(monkeypatch):
@@ -38,7 +42,8 @@ def test_configure_bearer_env_from_env_var(monkeypatch):
38
  "FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", '["test-scope1", "test-scope2"]'
39
  )
40
 
41
- mcp = FastMCP()
 
42
  assert isinstance(mcp.auth, EnvBearerAuthProvider)
43
  assert mcp.auth.public_key == "test-public-key"
44
  assert mcp.auth.issuer_url == AnyHttpUrl("http://test-issuer")
@@ -50,17 +55,19 @@ def test_list_of_scopes_must_be_a_list(monkeypatch):
50
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
51
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1")
52
 
53
- with pytest.raises(ValidationError, match="Input should be a valid list"):
54
- FastMCP()
 
55
 
56
 
57
  def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
58
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
59
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
60
 
61
- mcp = FastMCP()
62
- assert isinstance(mcp.auth, EnvBearerAuthProvider)
63
- assert mcp.auth.jwks_uri == "test-jwks-uri"
 
64
 
65
 
66
  def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
@@ -68,15 +75,17 @@ def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
68
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
69
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
70
 
71
- with pytest.raises(ValueError, match="Provide either public_key or jwks_uri"):
72
- FastMCP()
 
73
 
74
 
75
  def test_provided_auth_takes_precedence_over_env_vars(monkeypatch):
76
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
77
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
78
 
79
- mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2"))
80
- assert isinstance(mcp.auth, BearerAuthProvider)
81
- assert not isinstance(mcp.auth, EnvBearerAuthProvider)
82
- assert mcp.auth.public_key == "test-public-key-2"
 
 
4
  from fastmcp import FastMCP
5
  from fastmcp.server.auth.providers.bearer import BearerAuthProvider
6
  from fastmcp.server.auth.providers.bearer_env import EnvBearerAuthProvider
7
+ from fastmcp.settings import Settings
8
+ from fastmcp.utilities.tests import temporary_settings
9
 
10
 
11
  def test_load_bearer_env_from_env_var(monkeypatch):
 
15
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
16
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
17
 
18
+ with temporary_settings(**Settings().model_dump()):
19
+ mcp_with_auth = FastMCP()
20
  assert isinstance(mcp_with_auth.auth, EnvBearerAuthProvider)
21
 
22
 
 
26
 
27
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
28
 
29
+ with temporary_settings(**Settings().model_dump()):
30
+ with pytest.raises(
31
+ ValueError, match="Either public_key or jwks_uri must be provided"
32
+ ):
33
+ FastMCP()
34
 
35
 
36
  def test_configure_bearer_env_from_env_var(monkeypatch):
 
42
  "FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", '["test-scope1", "test-scope2"]'
43
  )
44
 
45
+ with temporary_settings(**Settings().model_dump()):
46
+ mcp = FastMCP()
47
  assert isinstance(mcp.auth, EnvBearerAuthProvider)
48
  assert mcp.auth.public_key == "test-public-key"
49
  assert mcp.auth.issuer_url == AnyHttpUrl("http://test-issuer")
 
55
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
56
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1")
57
 
58
+ with temporary_settings(**Settings().model_dump()):
59
+ with pytest.raises(ValidationError, match="Input should be a valid list"):
60
+ FastMCP()
61
 
62
 
63
  def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
64
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
65
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
66
 
67
+ with temporary_settings(**Settings().model_dump()):
68
+ mcp = FastMCP()
69
+ assert isinstance(mcp.auth, EnvBearerAuthProvider)
70
+ assert mcp.auth.jwks_uri == "test-jwks-uri"
71
 
72
 
73
  def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
 
75
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
76
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
77
 
78
+ with temporary_settings(**Settings().model_dump()):
79
+ with pytest.raises(ValueError, match="Provide either public_key or jwks_uri"):
80
+ FastMCP()
81
 
82
 
83
  def test_provided_auth_takes_precedence_over_env_vars(monkeypatch):
84
  monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
85
  monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
86
 
87
+ with temporary_settings(**Settings().model_dump()):
88
+ mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2"))
89
+ assert isinstance(mcp.auth, BearerAuthProvider)
90
+ assert not isinstance(mcp.auth, EnvBearerAuthProvider)
91
+ assert mcp.auth.public_key == "test-public-key-2"
tests/deprecated/test_server_init_kwargs.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import warnings
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+
6
+ from fastmcp import FastMCP
7
+
8
+ # reset deprecation warnings for this module
9
+ pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
10
+
11
+
12
+ class TestDeprecatedServerInitKwargs:
13
+ """Test deprecated server initialization keyword arguments."""
14
+
15
+ def test_log_level_deprecation_warning(self):
16
+ """Test that log_level raises a deprecation warning."""
17
+ with pytest.warns(
18
+ DeprecationWarning,
19
+ match=r"Providing `log_level` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
20
+ ):
21
+ server = FastMCP("TestServer", log_level="DEBUG")
22
+
23
+ # Verify the setting is still applied
24
+ assert server._deprecated_settings.log_level == "DEBUG"
25
+
26
+ def test_debug_deprecation_warning(self):
27
+ """Test that debug raises a deprecation warning."""
28
+ with pytest.warns(
29
+ DeprecationWarning,
30
+ match=r"Providing `debug` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
31
+ ):
32
+ server = FastMCP("TestServer", debug=True)
33
+
34
+ # Verify the setting is still applied
35
+ assert server._deprecated_settings.debug is True
36
+
37
+ def test_host_deprecation_warning(self):
38
+ """Test that host raises a deprecation warning."""
39
+ with pytest.warns(
40
+ DeprecationWarning,
41
+ match=r"Providing `host` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
42
+ ):
43
+ server = FastMCP("TestServer", host="0.0.0.0")
44
+
45
+ # Verify the setting is still applied
46
+ assert server._deprecated_settings.host == "0.0.0.0"
47
+
48
+ def test_port_deprecation_warning(self):
49
+ """Test that port raises a deprecation warning."""
50
+ with pytest.warns(
51
+ DeprecationWarning,
52
+ match=r"Providing `port` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
53
+ ):
54
+ server = FastMCP("TestServer", port=8080)
55
+
56
+ # Verify the setting is still applied
57
+ assert server._deprecated_settings.port == 8080
58
+
59
+ def test_sse_path_deprecation_warning(self):
60
+ """Test that sse_path raises a deprecation warning."""
61
+ with pytest.warns(
62
+ DeprecationWarning,
63
+ match=r"Providing `sse_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
64
+ ):
65
+ server = FastMCP("TestServer", sse_path="/custom-sse")
66
+
67
+ # Verify the setting is still applied
68
+ assert server._deprecated_settings.sse_path == "/custom-sse"
69
+
70
+ def test_message_path_deprecation_warning(self):
71
+ """Test that message_path raises a deprecation warning."""
72
+ with pytest.warns(
73
+ DeprecationWarning,
74
+ match=r"Providing `message_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
75
+ ):
76
+ server = FastMCP("TestServer", message_path="/custom-message")
77
+
78
+ # Verify the setting is still applied
79
+ assert server._deprecated_settings.message_path == "/custom-message"
80
+
81
+ def test_streamable_http_path_deprecation_warning(self):
82
+ """Test that streamable_http_path raises a deprecation warning."""
83
+ with pytest.warns(
84
+ DeprecationWarning,
85
+ match=r"Providing `streamable_http_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
86
+ ):
87
+ server = FastMCP("TestServer", streamable_http_path="/custom-http")
88
+
89
+ # Verify the setting is still applied
90
+ assert server._deprecated_settings.streamable_http_path == "/custom-http"
91
+
92
+ def test_json_response_deprecation_warning(self):
93
+ """Test that json_response raises a deprecation warning."""
94
+ with pytest.warns(
95
+ DeprecationWarning,
96
+ match=r"Providing `json_response` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
97
+ ):
98
+ server = FastMCP("TestServer", json_response=True)
99
+
100
+ # Verify the setting is still applied
101
+ assert server._deprecated_settings.json_response is True
102
+
103
+ def test_stateless_http_deprecation_warning(self):
104
+ """Test that stateless_http raises a deprecation warning."""
105
+ with pytest.warns(
106
+ DeprecationWarning,
107
+ match=r"Providing `stateless_http` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
108
+ ):
109
+ server = FastMCP("TestServer", stateless_http=True)
110
+
111
+ # Verify the setting is still applied
112
+ assert server._deprecated_settings.stateless_http is True
113
+
114
+ def test_multiple_deprecated_kwargs_warnings(self):
115
+ """Test that multiple deprecated kwargs each raise their own warning."""
116
+ with warnings.catch_warnings(record=True) as recorded_warnings:
117
+ warnings.simplefilter("always")
118
+ server = FastMCP(
119
+ "TestServer",
120
+ log_level="INFO",
121
+ debug=False,
122
+ host="127.0.0.1",
123
+ port=9999,
124
+ sse_path="/sse",
125
+ message_path="/msg",
126
+ streamable_http_path="/http",
127
+ json_response=False,
128
+ stateless_http=False,
129
+ )
130
+
131
+ # Should have 9 deprecation warnings (one for each deprecated parameter)
132
+ deprecation_warnings = [
133
+ w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
134
+ ]
135
+ assert len(deprecation_warnings) == 9
136
+
137
+ # Verify all expected parameters are mentioned in warnings
138
+ expected_params = {
139
+ "log_level",
140
+ "debug",
141
+ "host",
142
+ "port",
143
+ "sse_path",
144
+ "message_path",
145
+ "streamable_http_path",
146
+ "json_response",
147
+ "stateless_http",
148
+ }
149
+ mentioned_params = set()
150
+ for warning in deprecation_warnings:
151
+ message = str(warning.message)
152
+ for param in expected_params:
153
+ if f"Providing `{param}`" in message:
154
+ mentioned_params.add(param)
155
+
156
+ assert mentioned_params == expected_params
157
+
158
+ # Verify all settings are still applied
159
+ assert server._deprecated_settings.log_level == "INFO"
160
+ assert server._deprecated_settings.debug is False
161
+ assert server._deprecated_settings.host == "127.0.0.1"
162
+ assert server._deprecated_settings.port == 9999
163
+ assert server._deprecated_settings.sse_path == "/sse"
164
+ assert server._deprecated_settings.message_path == "/msg"
165
+ assert server._deprecated_settings.streamable_http_path == "/http"
166
+ assert server._deprecated_settings.json_response is False
167
+ assert server._deprecated_settings.stateless_http is False
168
+
169
+ def test_non_deprecated_kwargs_no_warnings(self):
170
+ """Test that non-deprecated kwargs don't raise warnings."""
171
+ with warnings.catch_warnings(record=True) as recorded_warnings:
172
+ warnings.simplefilter("always")
173
+ server = FastMCP(
174
+ name="TestServer",
175
+ instructions="Test instructions",
176
+ tags={"test", "server"},
177
+ cache_expiration_seconds=60.0,
178
+ on_duplicate_tools="warn",
179
+ on_duplicate_resources="error",
180
+ on_duplicate_prompts="replace",
181
+ resource_prefix_format="path",
182
+ mask_error_details=True,
183
+ )
184
+
185
+ # Should have no deprecation warnings
186
+ deprecation_warnings = [
187
+ w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
188
+ ]
189
+ assert len(deprecation_warnings) == 0
190
+
191
+ # Verify server was created successfully
192
+ assert server.name == "TestServer"
193
+ assert server.instructions == "Test instructions"
194
+ assert server.tags == {"test", "server"}
195
+
196
+ def test_none_values_no_warnings(self):
197
+ """Test that None values for deprecated kwargs don't raise warnings."""
198
+ with warnings.catch_warnings(record=True) as recorded_warnings:
199
+ warnings.simplefilter("always")
200
+ server = FastMCP(
201
+ "TestServer",
202
+ log_level=None,
203
+ debug=None,
204
+ host=None,
205
+ port=None,
206
+ sse_path=None,
207
+ message_path=None,
208
+ streamable_http_path=None,
209
+ json_response=None,
210
+ stateless_http=None,
211
+ )
212
+
213
+ # Should have no deprecation warnings for None values
214
+ deprecation_warnings = [
215
+ w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
216
+ ]
217
+ assert len(deprecation_warnings) == 0
218
+
219
+ def test_deprecated_settings_inheritance_from_global(self):
220
+ """Test that deprecated settings inherit from global settings when not provided."""
221
+ # Mock fastmcp.settings to test inheritance
222
+ with patch("fastmcp.settings") as mock_settings:
223
+ mock_settings.model_dump.return_value = {
224
+ "log_level": "WARNING",
225
+ "debug": True,
226
+ "host": "0.0.0.0",
227
+ "port": 3000,
228
+ "sse_path": "/events",
229
+ "message_path": "/messages",
230
+ "streamable_http_path": "/stream",
231
+ "json_response": True,
232
+ "stateless_http": True,
233
+ }
234
+
235
+ server = FastMCP("TestServer")
236
+
237
+ # Verify settings are inherited from global settings
238
+ assert server._deprecated_settings.log_level == "WARNING"
239
+ assert server._deprecated_settings.debug is True
240
+ assert server._deprecated_settings.host == "0.0.0.0"
241
+ assert server._deprecated_settings.port == 3000
242
+ assert server._deprecated_settings.sse_path == "/events"
243
+ assert server._deprecated_settings.message_path == "/messages"
244
+ assert server._deprecated_settings.streamable_http_path == "/stream"
245
+ assert server._deprecated_settings.json_response is True
246
+ assert server._deprecated_settings.stateless_http is True
247
+
248
+ def test_deprecated_settings_override_global(self):
249
+ """Test that deprecated settings override global settings when provided."""
250
+ # Mock fastmcp.settings to test override behavior
251
+ with patch("fastmcp.settings") as mock_settings:
252
+ mock_settings.model_dump.return_value = {
253
+ "log_level": "WARNING",
254
+ "debug": True,
255
+ "host": "0.0.0.0",
256
+ "port": 3000,
257
+ "sse_path": "/events",
258
+ "message_path": "/messages",
259
+ "streamable_http_path": "/stream",
260
+ "json_response": True,
261
+ "stateless_http": True,
262
+ }
263
+
264
+ with warnings.catch_warnings():
265
+ warnings.simplefilter("ignore") # Ignore warnings for this test
266
+ server = FastMCP(
267
+ "TestServer",
268
+ log_level="ERROR",
269
+ debug=False,
270
+ host="127.0.0.1",
271
+ port=8080,
272
+ )
273
+
274
+ # Verify provided settings override global settings
275
+ assert server._deprecated_settings.log_level == "ERROR"
276
+ assert server._deprecated_settings.debug is False
277
+ assert server._deprecated_settings.host == "127.0.0.1"
278
+ assert server._deprecated_settings.port == 8080
279
+ # Non-overridden settings should still come from global
280
+ assert server._deprecated_settings.sse_path == "/events"
281
+ assert server._deprecated_settings.message_path == "/messages"
282
+ assert server._deprecated_settings.streamable_http_path == "/stream"
283
+ assert server._deprecated_settings.json_response is True
284
+ assert server._deprecated_settings.stateless_http is True
285
+
286
+ def test_stacklevel_points_to_constructor_call(self):
287
+ """Test that deprecation warnings point to the FastMCP constructor call."""
288
+ with warnings.catch_warnings(record=True) as recorded_warnings:
289
+ warnings.simplefilter("always")
290
+
291
+ def create_server_with_deprecated_kwargs():
292
+ return FastMCP("TestServer", log_level="DEBUG")
293
+
294
+ server = create_server_with_deprecated_kwargs()
295
+
296
+ # Should have exactly one deprecation warning
297
+ deprecation_warnings = [
298
+ w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
299
+ ]
300
+ assert len(deprecation_warnings) == 1
301
+
302
+ # The warning should point to the server.py file where FastMCP.__init__ is called
303
+ # This verifies the stacklevel is working as intended (pointing to constructor)
304
+ warning = deprecation_warnings[0]
305
+ assert "server.py" in warning.filename
tests/utilities/test_tests.py CHANGED
@@ -4,7 +4,7 @@ from fastmcp.utilities.tests import temporary_settings
4
 
5
  class TestTemporarySettings:
6
  def test_temporary_settings(self):
7
- assert fastmcp.settings.settings.log_level == "DEBUG"
8
  with temporary_settings(log_level="ERROR"):
9
- assert fastmcp.settings.settings.log_level == "ERROR"
10
- assert fastmcp.settings.settings.log_level == "DEBUG"
 
4
 
5
  class TestTemporarySettings:
6
  def test_temporary_settings(self):
7
+ assert fastmcp.settings.log_level == "DEBUG"
8
  with temporary_settings(log_level="ERROR"):
9
+ assert fastmcp.settings.log_level == "ERROR"
10
+ assert fastmcp.settings.log_level == "DEBUG"