Remove open-ended and server-specific settings

This commit is contained in:
Jeremiah Lowin 2025-06-07 20:20:04 -04:00
commit aaa09ca7a3
16 changed files with 507 additions and 119 deletions

View file

@ -215,7 +215,7 @@ You can configure the prefix format globally in code:
```python
import fastmcp
fastmcp.settings.settings.resource_prefix_format = "protocol"
fastmcp.settings.resource_prefix_format = "protocol"
```
Or via environment variable:

View file

@ -1,6 +1,9 @@
"""FastMCP - An ergonomic MCP interface."""
from importlib.metadata import version
from fastmcp.settings import Settings
settings = Settings()
from fastmcp.server.server import FastMCP
from fastmcp.server.context import Context
@ -8,7 +11,7 @@ import fastmcp.server
from fastmcp.client import Client
from fastmcp.utilities.types import Image
from . import client, settings
from . import client
__version__ = version("fastmcp")
__all__ = [

View file

@ -18,6 +18,7 @@ from typer import Context, Exit
import fastmcp
from fastmcp.cli import claude
from fastmcp.cli import run as run_module
from fastmcp.server.server import FastMCP
from fastmcp.utilities.logging import get_logger
logger = get_logger("cli")
@ -165,8 +166,8 @@ def dev(
try:
# Import server to get dependencies
server = run_module.import_server(file, server_object)
if hasattr(server, "dependencies") and server.dependencies is not None:
server: FastMCP = run_module.import_server(file, server_object)
if server.dependencies is not None:
with_packages = list(set(with_packages + server.dependencies))
env_vars = {}

View file

@ -23,10 +23,10 @@ from mcp.shared.auth import (
)
from pydantic import AnyHttpUrl, ValidationError
from fastmcp import settings as fastmcp_global_settings
from fastmcp.client.oauth_callback import (
create_oauth_callback_server,
)
from fastmcp.settings import settings as fastmcp_global_settings
from fastmcp.utilities.http import find_available_port
from fastmcp.utilities.logging import get_logger

View file

@ -165,7 +165,7 @@ class Client(Generic[ClientTransportT]):
# handle init handshake timeout
if init_timeout is None:
init_timeout = fastmcp.settings.settings.client_init_timeout
init_timeout = fastmcp.settings.client_init_timeout
if isinstance(init_timeout, datetime.timedelta):
init_timeout = init_timeout.total_seconds()
elif not init_timeout:

View file

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
from mcp import GetPromptResult
from fastmcp import settings
from fastmcp.exceptions import NotFoundError, PromptError
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
from fastmcp.settings import DuplicateBehavior
@ -23,10 +24,10 @@ class PromptManager:
def __init__(
self,
duplicate_behavior: DuplicateBehavior | None = None,
mask_error_details: bool = False,
mask_error_details: bool | None = None,
):
self._prompts: dict[str, Prompt] = {}
self.mask_error_details = mask_error_details
self.mask_error_details = mask_error_details or settings.mask_error_details
# Default to "warn" if None is provided
if duplicate_behavior is None:

View file

@ -7,6 +7,7 @@ from typing import Any
from pydantic import AnyUrl
from fastmcp import settings
from fastmcp.exceptions import NotFoundError, ResourceError
from fastmcp.resources.resource import Resource
from fastmcp.resources.template import (
@ -25,7 +26,7 @@ class ResourceManager:
def __init__(
self,
duplicate_behavior: DuplicateBehavior | None = None,
mask_error_details: bool = False,
mask_error_details: bool | None = None,
):
"""Initialize the ResourceManager.
@ -37,7 +38,7 @@ class ResourceManager:
"""
self._resources: dict[str, Resource] = {}
self._templates: dict[str, ResourceTemplate] = {}
self.mask_error_details = mask_error_details
self.mask_error_details = mask_error_details or settings.mask_error_details
# Default to "warn" if None is provided
if duplicate_behavior is None:

View file

@ -43,7 +43,6 @@ from starlette.routing import BaseRoute, Route
import fastmcp
import fastmcp.server
import fastmcp.settings
from fastmcp.exceptions import NotFoundError
from fastmcp.prompts import Prompt, PromptManager
from fastmcp.prompts.prompt import FunctionPrompt
@ -56,6 +55,7 @@ from fastmcp.server.http import (
create_sse_app,
create_streamable_http_app,
)
from fastmcp.settings import Settings
from fastmcp.tools import ToolManager
from fastmcp.tools.tool import FunctionTool, Tool
from fastmcp.utilities.cache import TimedCache
@ -121,7 +121,6 @@ class FastMCP(Generic[LifespanResultT]):
| None
) = None,
tags: set[str] | None = None,
dependencies: list[str] | None = None,
tool_serializer: Callable[[Any], str] | None = None,
cache_expiration_seconds: float | None = None,
on_duplicate_tools: DuplicateBehavior | None = None,
@ -130,44 +129,44 @@ class FastMCP(Generic[LifespanResultT]):
resource_prefix_format: Literal["protocol", "path"] | None = None,
mask_error_details: bool | None = None,
tools: list[Tool | Callable[..., Any]] | None = None,
**settings: Any,
dependencies: list[str] | None = None,
# ---
# ---
# --- The following arguments are DEPRECATED ---
# ---
# ---
log_level: str | None = None,
debug: bool | None = None,
host: str | None = None,
port: int | None = None,
sse_path: str | None = None,
message_path: str | None = None,
streamable_http_path: str | None = None,
json_response: bool | None = None,
stateless_http: bool | None = None,
):
if cache_expiration_seconds is not None:
settings["cache_expiration_seconds"] = cache_expiration_seconds
self.settings = fastmcp.settings.ServerSettings(**settings)
# If mask_error_details is provided, override the settings value
if mask_error_details is not None:
self.settings.mask_error_details = mask_error_details
self.resource_prefix_format: Literal["protocol", "path"]
if resource_prefix_format is None:
self.resource_prefix_format = (
fastmcp.settings.settings.resource_prefix_format
)
else:
self.resource_prefix_format = resource_prefix_format
self.resource_prefix_format: Literal["protocol", "path"] = (
resource_prefix_format or fastmcp.settings.resource_prefix_format
)
self.tags: set[str] = tags or set()
self.dependencies = dependencies
self._cache = TimedCache(
expiration=datetime.timedelta(
seconds=self.settings.cache_expiration_seconds
)
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
)
self._mounted_servers: dict[str, MountedServer] = {}
self._additional_http_routes: list[BaseRoute] = []
self._tool_manager = ToolManager(
duplicate_behavior=on_duplicate_tools,
mask_error_details=self.settings.mask_error_details,
mask_error_details=mask_error_details,
)
self._resource_manager = ResourceManager(
duplicate_behavior=on_duplicate_resources,
mask_error_details=self.settings.mask_error_details,
mask_error_details=mask_error_details,
)
self._prompt_manager = PromptManager(
duplicate_behavior=on_duplicate_prompts,
mask_error_details=self.settings.mask_error_details,
mask_error_details=mask_error_details,
)
self._tool_serializer = tool_serializer
@ -182,7 +181,7 @@ class FastMCP(Generic[LifespanResultT]):
lifespan=_lifespan_wrapper(self, lifespan),
)
if auth is None and self.settings.default_auth_provider == "bearer_env":
if auth is None and fastmcp.settings.default_auth_provider == "bearer_env":
auth = EnvBearerAuthProvider()
self.auth = auth
@ -194,10 +193,62 @@ class FastMCP(Generic[LifespanResultT]):
# Set up MCP protocol handlers
self._setup_handlers()
self.dependencies = dependencies or fastmcp.settings.server_dependencies
# handle deprecated settings
self._handle_deprecated_settings(
log_level=log_level,
debug=debug,
host=host,
port=port,
sse_path=sse_path,
message_path=message_path,
streamable_http_path=streamable_http_path,
json_response=json_response,
stateless_http=stateless_http,
)
def __repr__(self) -> str:
return f"{type(self).__name__}({self.name!r})"
def _handle_deprecated_settings(
self,
log_level: str | None,
debug: bool | None,
host: str | None,
port: int | None,
sse_path: str | None,
message_path: str | None,
streamable_http_path: str | None,
json_response: bool | None,
stateless_http: bool | None,
) -> None:
"""Handle deprecated settings. Deprecated in 2.8.0."""
deprecated_settings: dict[str, Any] = {}
for name, arg in [
("log_level", log_level),
("debug", debug),
("host", host),
("port", port),
("sse_path", sse_path),
("message_path", message_path),
("streamable_http_path", streamable_http_path),
("json_response", json_response),
("stateless_http", stateless_http),
]:
if arg is not None:
# Deprecated in 2.8.0
warnings.warn(
f"Providing `{name}` when creating a server is deprecated. Provide it when calling `run` or as a global setting instead.",
DeprecationWarning,
stacklevel=2,
)
deprecated_settings[name] = arg
combined_settings = fastmcp.settings.model_dump() | deprecated_settings
self._deprecated_settings = Settings(**combined_settings)
@property
def name(self) -> str:
return self._mcp_server.name
@ -1008,9 +1059,11 @@ class FastMCP(Generic[LifespanResultT]):
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
uvicorn_config: Additional configuration for the Uvicorn server
"""
host = host or self.settings.host
port = port or self.settings.port
default_log_level_to_use = (log_level or self.settings.log_level).lower()
host = host or self._deprecated_settings.host
port = port or self._deprecated_settings.port
default_log_level_to_use = (
log_level or self._deprecated_settings.log_level
).lower()
app = self.http_app(path=path, transport=transport, middleware=middleware)
@ -1084,10 +1137,10 @@ class FastMCP(Generic[LifespanResultT]):
)
return create_sse_app(
server=self,
message_path=message_path or self.settings.message_path,
sse_path=path or self.settings.sse_path,
message_path=message_path or self._deprecated_settings.message_path,
sse_path=path or self._deprecated_settings.sse_path,
auth=self.auth,
debug=self.settings.debug,
debug=self._deprecated_settings.debug,
middleware=middleware,
)
@ -1115,6 +1168,8 @@ class FastMCP(Generic[LifespanResultT]):
self,
path: str | None = None,
middleware: list[Middleware] | None = None,
json_response: bool | None = None,
stateless_http: bool | None = None,
transport: Literal["streamable-http", "sse"] = "streamable-http",
) -> StarletteWithLifespan:
"""Create a Starlette app using the specified HTTP transport.
@ -1131,21 +1186,22 @@ class FastMCP(Generic[LifespanResultT]):
if transport == "streamable-http":
return create_streamable_http_app(
server=self,
streamable_http_path=path or self.settings.streamable_http_path,
streamable_http_path=path
or self._deprecated_settings.streamable_http_path,
event_store=None,
auth=self.auth,
json_response=self.settings.json_response,
stateless_http=self.settings.stateless_http,
debug=self.settings.debug,
json_response=self._deprecated_settings.json_response,
stateless_http=self._deprecated_settings.stateless_http,
debug=self._deprecated_settings.debug,
middleware=middleware,
)
elif transport == "sse":
return create_sse_app(
server=self,
message_path=self.settings.message_path,
sse_path=path or self.settings.sse_path,
message_path=self._deprecated_settings.message_path,
sse_path=path or self._deprecated_settings.sse_path,
auth=self.auth,
debug=self.settings.debug,
debug=self._deprecated_settings.debug,
middleware=middleware,
)
@ -1597,7 +1653,7 @@ def add_resource_prefix(
# Get the server settings to check for legacy format preference
if prefix_format is None:
prefix_format = fastmcp.settings.settings.resource_prefix_format
prefix_format = fastmcp.settings.resource_prefix_format
if prefix_format == "protocol":
# Legacy style: prefix+protocol://path
@ -1646,7 +1702,7 @@ def remove_resource_prefix(
return uri
if prefix_format is None:
prefix_format = fastmcp.settings.settings.resource_prefix_format
prefix_format = fastmcp.settings.resource_prefix_format
if prefix_format == "protocol":
# Legacy style: prefix+protocol://path
@ -1706,7 +1762,7 @@ def has_resource_prefix(
# Get the server settings to check for legacy format preference
if prefix_format is None:
prefix_format = fastmcp.settings.settings.resource_prefix_format
prefix_format = fastmcp.settings.resource_prefix_format
if prefix_format == "protocol":
# Legacy style: prefix+protocol://path

View file

@ -1,16 +1,47 @@
from __future__ import annotations as _annotations
import inspect
import warnings
from pathlib import Path
from typing import Annotated, Literal
from typing import Annotated, Any, Literal
from pydantic import Field, model_validator
from pydantic.fields import FieldInfo
from pydantic_settings import (
BaseSettings,
EnvSettingsSource,
PydanticBaseSettingsSource,
SettingsConfigDict,
)
from typing_extensions import Self
class ExtendedEnvSettingsSource(EnvSettingsSource):
def get_field_value(
self, field: FieldInfo, field_name: str
) -> tuple[Any, str, bool]:
if prefixes := self.config.get("env_prefixes"):
for prefix in prefixes:
self.env_prefix = prefix
env_val, field_key, value_is_complex = super().get_field_value(
field, field_name
)
if env_val is not None:
if prefix == "FASTMCP_SERVER_":
warnings.warn(
"Using `FASTMCP_SERVER_` environment variables is deprecated. Use `FASTMCP_` instead.",
DeprecationWarning,
stacklevel=2,
)
return env_val, field_key, value_is_complex
return super().get_field_value(field, field_name)
class ExtendedSettingsConfigDict(SettingsConfigDict, total=False):
env_prefixes: list[str] | None
LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
@ -19,14 +50,30 @@ DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
class Settings(BaseSettings):
"""FastMCP settings."""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_",
model_config = ExtendedSettingsConfigDict(
env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"],
env_file=".env",
extra="ignore",
env_nested_delimiter="__",
nested_model_default_partial_update=True,
)
@classmethod
def settings_customise_sources(
cls,
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return (
init_settings,
ExtendedEnvSettingsSource(settings_cls),
dotenv_settings,
file_secret_settings,
)
home: Path = Path.home() / ".fastmcp"
test_mode: bool = False
@ -107,27 +154,6 @@ class Settings(BaseSettings):
return self
class ServerSettings(BaseSettings):
"""FastMCP server settings.
All settings can be configured via environment variables with the prefix FASTMCP_.
For example, FASTMCP_DEBUG=true will set debug=True.
"""
model_config = SettingsConfigDict(
env_prefix="FASTMCP_SERVER_",
env_file=".env",
extra="ignore",
env_nested_delimiter="__",
nested_model_default_partial_update=True,
)
log_level: Annotated[
LOG_LEVEL,
Field(default_factory=lambda: Settings().log_level),
]
# HTTP settings
host: str = "127.0.0.1"
port: int = 8000
@ -136,15 +162,6 @@ class ServerSettings(BaseSettings):
streamable_http_path: str = "/mcp"
debug: bool = False
# resource settings
on_duplicate_resources: DuplicateBehavior = "warn"
# tool settings
on_duplicate_tools: DuplicateBehavior = "warn"
# prompt settings
on_duplicate_prompts: DuplicateBehavior = "warn"
# error handling
mask_error_details: Annotated[
bool,
@ -162,7 +179,7 @@ class ServerSettings(BaseSettings):
),
] = False
dependencies: Annotated[
server_dependencies: Annotated[
list[str],
Field(
default_factory=list,
@ -170,9 +187,6 @@ class ServerSettings(BaseSettings):
),
] = []
# cache settings (for getting attributes from servers, used to avoid repeated calls)
cache_expiration_seconds: float = 0
# StreamableHTTP settings
json_response: bool = False
stateless_http: bool = (
@ -197,6 +211,3 @@ class ServerSettings(BaseSettings):
),
),
] = None
settings = Settings()

View file

@ -185,7 +185,7 @@ class FunctionTool(Tool):
if context_kwarg and context_kwarg not in arguments:
arguments[context_kwarg] = get_context()
if fastmcp.settings.settings.tool_attempt_parse_json_args:
if fastmcp.settings.tool_attempt_parse_json_args:
# Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
# being passed in as JSON inside a string rather than an actual list.
#

View file

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
from fastmcp import settings
from fastmcp.exceptions import NotFoundError, ToolError
from fastmcp.settings import DuplicateBehavior
from fastmcp.tools.tool import Tool
@ -23,10 +24,10 @@ class ToolManager:
def __init__(
self,
duplicate_behavior: DuplicateBehavior | None = None,
mask_error_details: bool = False,
mask_error_details: bool | None = None,
):
self._tools: dict[str, Tool] = {}
self.mask_error_details = mask_error_details
self.mask_error_details = mask_error_details or settings.mask_error_details
# Default to "warn" if None is provided
if duplicate_behavior is None:

View file

@ -43,7 +43,7 @@ def get_catch_handlers() -> Mapping[
type[BaseException] | Iterable[type[BaseException]],
Callable[[BaseExceptionGroup[Any]], Any],
]:
if fastmcp.settings.settings.client_raise_first_exceptiongroup_error:
if fastmcp.settings.client_raise_first_exceptiongroup_error:
return _catch_handlers
else:
return {}

View file

@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Literal
import uvicorn
from fastmcp.settings import settings
from fastmcp import settings
from fastmcp.utilities.http import find_available_port
if TYPE_CHECKING:
@ -32,8 +32,8 @@ def temporary_settings(**kwargs: Any):
from fastmcp.utilities.tests import temporary_settings
with temporary_settings(log_level='DEBUG'):
assert fastmcp.settings.settings.log_level == 'DEBUG'
assert fastmcp.settings.settings.log_level == 'INFO'
assert fastmcp.settings.log_level == 'DEBUG'
assert fastmcp.settings.log_level == 'INFO'
```
"""
old_settings = copy.deepcopy(settings.model_dump())

View file

@ -4,6 +4,8 @@ from pydantic import AnyHttpUrl, ValidationError
from fastmcp import FastMCP
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
from fastmcp.server.auth.providers.bearer_env import EnvBearerAuthProvider
from fastmcp.settings import Settings
from fastmcp.utilities.tests import temporary_settings
def test_load_bearer_env_from_env_var(monkeypatch):
@ -13,7 +15,8 @@ def test_load_bearer_env_from_env_var(monkeypatch):
monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
mcp_with_auth = FastMCP()
with temporary_settings(**Settings().model_dump()):
mcp_with_auth = FastMCP()
assert isinstance(mcp_with_auth.auth, EnvBearerAuthProvider)
@ -23,10 +26,11 @@ def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatc
monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
with pytest.raises(
ValueError, match="Either public_key or jwks_uri must be provided"
):
FastMCP()
with temporary_settings(**Settings().model_dump()):
with pytest.raises(
ValueError, match="Either public_key or jwks_uri must be provided"
):
FastMCP()
def test_configure_bearer_env_from_env_var(monkeypatch):
@ -38,7 +42,8 @@ def test_configure_bearer_env_from_env_var(monkeypatch):
"FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", '["test-scope1", "test-scope2"]'
)
mcp = FastMCP()
with temporary_settings(**Settings().model_dump()):
mcp = FastMCP()
assert isinstance(mcp.auth, EnvBearerAuthProvider)
assert mcp.auth.public_key == "test-public-key"
assert mcp.auth.issuer_url == AnyHttpUrl("http://test-issuer")
@ -50,17 +55,19 @@ def test_list_of_scopes_must_be_a_list(monkeypatch):
monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1")
with pytest.raises(ValidationError, match="Input should be a valid list"):
FastMCP()
with temporary_settings(**Settings().model_dump()):
with pytest.raises(ValidationError, match="Input should be a valid list"):
FastMCP()
def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
mcp = FastMCP()
assert isinstance(mcp.auth, EnvBearerAuthProvider)
assert mcp.auth.jwks_uri == "test-jwks-uri"
with temporary_settings(**Settings().model_dump()):
mcp = FastMCP()
assert isinstance(mcp.auth, EnvBearerAuthProvider)
assert mcp.auth.jwks_uri == "test-jwks-uri"
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):
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
with pytest.raises(ValueError, match="Provide either public_key or jwks_uri"):
FastMCP()
with temporary_settings(**Settings().model_dump()):
with pytest.raises(ValueError, match="Provide either public_key or jwks_uri"):
FastMCP()
def test_provided_auth_takes_precedence_over_env_vars(monkeypatch):
monkeypatch.setenv("FASTMCP_SERVER_DEFAULT_AUTH_PROVIDER", "bearer_env")
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2"))
assert isinstance(mcp.auth, BearerAuthProvider)
assert not isinstance(mcp.auth, EnvBearerAuthProvider)
assert mcp.auth.public_key == "test-public-key-2"
with temporary_settings(**Settings().model_dump()):
mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2"))
assert isinstance(mcp.auth, BearerAuthProvider)
assert not isinstance(mcp.auth, EnvBearerAuthProvider)
assert mcp.auth.public_key == "test-public-key-2"

View file

@ -0,0 +1,305 @@
import warnings
from unittest.mock import patch
import pytest
from fastmcp import FastMCP
# reset deprecation warnings for this module
pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
class TestDeprecatedServerInitKwargs:
"""Test deprecated server initialization keyword arguments."""
def test_log_level_deprecation_warning(self):
"""Test that log_level raises a deprecation warning."""
with pytest.warns(
DeprecationWarning,
match=r"Providing `log_level` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
):
server = FastMCP("TestServer", log_level="DEBUG")
# Verify the setting is still applied
assert server._deprecated_settings.log_level == "DEBUG"
def test_debug_deprecation_warning(self):
"""Test that debug raises a deprecation warning."""
with pytest.warns(
DeprecationWarning,
match=r"Providing `debug` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
):
server = FastMCP("TestServer", debug=True)
# Verify the setting is still applied
assert server._deprecated_settings.debug is True
def test_host_deprecation_warning(self):
"""Test that host raises a deprecation warning."""
with pytest.warns(
DeprecationWarning,
match=r"Providing `host` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
):
server = FastMCP("TestServer", host="0.0.0.0")
# Verify the setting is still applied
assert server._deprecated_settings.host == "0.0.0.0"
def test_port_deprecation_warning(self):
"""Test that port raises a deprecation warning."""
with pytest.warns(
DeprecationWarning,
match=r"Providing `port` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
):
server = FastMCP("TestServer", port=8080)
# Verify the setting is still applied
assert server._deprecated_settings.port == 8080
def test_sse_path_deprecation_warning(self):
"""Test that sse_path raises a deprecation warning."""
with pytest.warns(
DeprecationWarning,
match=r"Providing `sse_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
):
server = FastMCP("TestServer", sse_path="/custom-sse")
# Verify the setting is still applied
assert server._deprecated_settings.sse_path == "/custom-sse"
def test_message_path_deprecation_warning(self):
"""Test that message_path raises a deprecation warning."""
with pytest.warns(
DeprecationWarning,
match=r"Providing `message_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
):
server = FastMCP("TestServer", message_path="/custom-message")
# Verify the setting is still applied
assert server._deprecated_settings.message_path == "/custom-message"
def test_streamable_http_path_deprecation_warning(self):
"""Test that streamable_http_path raises a deprecation warning."""
with pytest.warns(
DeprecationWarning,
match=r"Providing `streamable_http_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
):
server = FastMCP("TestServer", streamable_http_path="/custom-http")
# Verify the setting is still applied
assert server._deprecated_settings.streamable_http_path == "/custom-http"
def test_json_response_deprecation_warning(self):
"""Test that json_response raises a deprecation warning."""
with pytest.warns(
DeprecationWarning,
match=r"Providing `json_response` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
):
server = FastMCP("TestServer", json_response=True)
# Verify the setting is still applied
assert server._deprecated_settings.json_response is True
def test_stateless_http_deprecation_warning(self):
"""Test that stateless_http raises a deprecation warning."""
with pytest.warns(
DeprecationWarning,
match=r"Providing `stateless_http` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
):
server = FastMCP("TestServer", stateless_http=True)
# Verify the setting is still applied
assert server._deprecated_settings.stateless_http is True
def test_multiple_deprecated_kwargs_warnings(self):
"""Test that multiple deprecated kwargs each raise their own warning."""
with warnings.catch_warnings(record=True) as recorded_warnings:
warnings.simplefilter("always")
server = FastMCP(
"TestServer",
log_level="INFO",
debug=False,
host="127.0.0.1",
port=9999,
sse_path="/sse",
message_path="/msg",
streamable_http_path="/http",
json_response=False,
stateless_http=False,
)
# Should have 9 deprecation warnings (one for each deprecated parameter)
deprecation_warnings = [
w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
]
assert len(deprecation_warnings) == 9
# Verify all expected parameters are mentioned in warnings
expected_params = {
"log_level",
"debug",
"host",
"port",
"sse_path",
"message_path",
"streamable_http_path",
"json_response",
"stateless_http",
}
mentioned_params = set()
for warning in deprecation_warnings:
message = str(warning.message)
for param in expected_params:
if f"Providing `{param}`" in message:
mentioned_params.add(param)
assert mentioned_params == expected_params
# Verify all settings are still applied
assert server._deprecated_settings.log_level == "INFO"
assert server._deprecated_settings.debug is False
assert server._deprecated_settings.host == "127.0.0.1"
assert server._deprecated_settings.port == 9999
assert server._deprecated_settings.sse_path == "/sse"
assert server._deprecated_settings.message_path == "/msg"
assert server._deprecated_settings.streamable_http_path == "/http"
assert server._deprecated_settings.json_response is False
assert server._deprecated_settings.stateless_http is False
def test_non_deprecated_kwargs_no_warnings(self):
"""Test that non-deprecated kwargs don't raise warnings."""
with warnings.catch_warnings(record=True) as recorded_warnings:
warnings.simplefilter("always")
server = FastMCP(
name="TestServer",
instructions="Test instructions",
tags={"test", "server"},
cache_expiration_seconds=60.0,
on_duplicate_tools="warn",
on_duplicate_resources="error",
on_duplicate_prompts="replace",
resource_prefix_format="path",
mask_error_details=True,
)
# Should have no deprecation warnings
deprecation_warnings = [
w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
]
assert len(deprecation_warnings) == 0
# Verify server was created successfully
assert server.name == "TestServer"
assert server.instructions == "Test instructions"
assert server.tags == {"test", "server"}
def test_none_values_no_warnings(self):
"""Test that None values for deprecated kwargs don't raise warnings."""
with warnings.catch_warnings(record=True) as recorded_warnings:
warnings.simplefilter("always")
server = FastMCP(
"TestServer",
log_level=None,
debug=None,
host=None,
port=None,
sse_path=None,
message_path=None,
streamable_http_path=None,
json_response=None,
stateless_http=None,
)
# Should have no deprecation warnings for None values
deprecation_warnings = [
w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
]
assert len(deprecation_warnings) == 0
def test_deprecated_settings_inheritance_from_global(self):
"""Test that deprecated settings inherit from global settings when not provided."""
# Mock fastmcp.settings to test inheritance
with patch("fastmcp.settings") as mock_settings:
mock_settings.model_dump.return_value = {
"log_level": "WARNING",
"debug": True,
"host": "0.0.0.0",
"port": 3000,
"sse_path": "/events",
"message_path": "/messages",
"streamable_http_path": "/stream",
"json_response": True,
"stateless_http": True,
}
server = FastMCP("TestServer")
# Verify settings are inherited from global settings
assert server._deprecated_settings.log_level == "WARNING"
assert server._deprecated_settings.debug is True
assert server._deprecated_settings.host == "0.0.0.0"
assert server._deprecated_settings.port == 3000
assert server._deprecated_settings.sse_path == "/events"
assert server._deprecated_settings.message_path == "/messages"
assert server._deprecated_settings.streamable_http_path == "/stream"
assert server._deprecated_settings.json_response is True
assert server._deprecated_settings.stateless_http is True
def test_deprecated_settings_override_global(self):
"""Test that deprecated settings override global settings when provided."""
# Mock fastmcp.settings to test override behavior
with patch("fastmcp.settings") as mock_settings:
mock_settings.model_dump.return_value = {
"log_level": "WARNING",
"debug": True,
"host": "0.0.0.0",
"port": 3000,
"sse_path": "/events",
"message_path": "/messages",
"streamable_http_path": "/stream",
"json_response": True,
"stateless_http": True,
}
with warnings.catch_warnings():
warnings.simplefilter("ignore") # Ignore warnings for this test
server = FastMCP(
"TestServer",
log_level="ERROR",
debug=False,
host="127.0.0.1",
port=8080,
)
# Verify provided settings override global settings
assert server._deprecated_settings.log_level == "ERROR"
assert server._deprecated_settings.debug is False
assert server._deprecated_settings.host == "127.0.0.1"
assert server._deprecated_settings.port == 8080
# Non-overridden settings should still come from global
assert server._deprecated_settings.sse_path == "/events"
assert server._deprecated_settings.message_path == "/messages"
assert server._deprecated_settings.streamable_http_path == "/stream"
assert server._deprecated_settings.json_response is True
assert server._deprecated_settings.stateless_http is True
def test_stacklevel_points_to_constructor_call(self):
"""Test that deprecation warnings point to the FastMCP constructor call."""
with warnings.catch_warnings(record=True) as recorded_warnings:
warnings.simplefilter("always")
def create_server_with_deprecated_kwargs():
return FastMCP("TestServer", log_level="DEBUG")
server = create_server_with_deprecated_kwargs()
# Should have exactly one deprecation warning
deprecation_warnings = [
w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
]
assert len(deprecation_warnings) == 1
# The warning should point to the server.py file where FastMCP.__init__ is called
# This verifies the stacklevel is working as intended (pointing to constructor)
warning = deprecation_warnings[0]
assert "server.py" in warning.filename

View file

@ -4,7 +4,7 @@ from fastmcp.utilities.tests import temporary_settings
class TestTemporarySettings:
def test_temporary_settings(self):
assert fastmcp.settings.settings.log_level == "DEBUG"
assert fastmcp.settings.log_level == "DEBUG"
with temporary_settings(log_level="ERROR"):
assert fastmcp.settings.settings.log_level == "ERROR"
assert fastmcp.settings.settings.log_level == "DEBUG"
assert fastmcp.settings.log_level == "ERROR"
assert fastmcp.settings.log_level == "DEBUG"