Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/servers/server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ print(fastmcp.settings.strict_input_validation) # Default: False

Common global settings include:
- **`log_level`**: Logging level ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"), set with `FASTMCP_LOG_LEVEL`
- **`debug`**: Global debug mode that sets log level to DEBUG and enables Starlette debug tracebacks for HTTP/SSE transports, set with `FASTMCP_DEBUG`. Provides a convenient way to enable comprehensive debugging. For granular control, use `log_level` and `starlette_debug` separately
- **`starlette_debug`**: Enable Starlette debug mode for HTTP/SSE transports only, set with `FASTMCP_STARLETTE_DEBUG`. When enabled, detailed error tracebacks are returned in HTTP responses. Only affects HTTP/SSE transports; has no effect on stdio transport
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a production-safety warning for debug tracebacks.

These options expose detailed tracebacks in HTTP responses; please add a clear warning that you should only enable them in development to avoid leaking internals. As per coding guidelines.

- **`mask_error_details`**: Whether to hide detailed error information from clients, set with `FASTMCP_MASK_ERROR_DETAILS`
- **`strict_input_validation`**: Controls tool input validation mode (default: False for flexible coercion), set with `FASTMCP_STRICT_INPUT_VALIDATION`. See [Input Validation Modes](/servers/tools#input-validation-modes)
- **`env_file`**: Path to the environment file to load settings from (default: ".env"), set with `FASTMCP_ENV_FILE`. Useful when your project uses a `.env` file with syntax incompatible with python-dotenv
Expand Down
12 changes: 8 additions & 4 deletions src/fastmcp/server/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def create_sse_app(
sse_path: str,
auth: AuthProvider | None = None,
debug: bool = False,
starlette_debug: bool = False,
routes: list[BaseRoute] | None = None,
middleware: list[Middleware] | None = None,
) -> StarletteWithLifespan:
Expand All @@ -152,7 +153,8 @@ def create_sse_app(
message_path: Path for SSE messages
sse_path: Path for SSE connections
auth: Optional authentication provider (AuthProvider)
debug: Whether to enable debug mode
debug: Whether to enable global debug mode (sets log level and Starlette debug)
starlette_debug: Whether to enable Starlette debug mode only
routes: Optional list of custom routes
middleware: Optional list of middleware
Returns:
Expand Down Expand Up @@ -252,7 +254,7 @@ async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
app = create_base_app(
routes=server_routes,
middleware=server_middleware,
debug=debug,
debug=debug or starlette_debug,
lifespan=lifespan,
)
# Store the FastMCP server instance on the Starlette app state
Expand All @@ -272,6 +274,7 @@ def create_streamable_http_app(
json_response: bool = False,
stateless_http: bool = False,
debug: bool = False,
starlette_debug: bool = False,
routes: list[BaseRoute] | None = None,
middleware: list[Middleware] | None = None,
) -> StarletteWithLifespan:
Expand All @@ -287,7 +290,8 @@ def create_streamable_http_app(
auth: Optional authentication provider (AuthProvider)
json_response: Whether to use JSON response format
stateless_http: Whether to use stateless mode (new transport per request)
debug: Whether to enable debug mode
debug: Whether to enable global debug mode (sets log level and Starlette debug)
starlette_debug: Whether to enable Starlette debug mode only
routes: Optional list of custom routes
middleware: Optional list of middleware

Expand Down Expand Up @@ -365,7 +369,7 @@ async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
app = create_base_app(
routes=server_routes,
middleware=server_middleware,
debug=debug,
debug=debug or starlette_debug,
lifespan=lifespan,
)
# Store the FastMCP server instance on the Starlette app state
Expand Down
2 changes: 2 additions & 0 deletions src/fastmcp/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2769,6 +2769,7 @@ def http_app(
else self._deprecated_settings.stateless_http
),
debug=self._deprecated_settings.debug,
starlette_debug=self._deprecated_settings.starlette_debug,
middleware=middleware,
)
elif transport == "sse":
Expand All @@ -2778,6 +2779,7 @@ def http_app(
sse_path=path or self._deprecated_settings.sse_path,
auth=self.auth,
debug=self._deprecated_settings.debug,
starlette_debug=self._deprecated_settings.starlette_debug,
middleware=middleware,
)

Expand Down
52 changes: 51 additions & 1 deletion src/fastmcp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,57 @@ def normalize_log_level(cls, v):
sse_path: str = "/sse"
message_path: str = "/messages/"
streamable_http_path: str = "/mcp"
debug: bool = False

debug: Annotated[
bool,
Field(
description=inspect.cleandoc(
"""
Global debug mode. When enabled, sets log level to DEBUG and enables
Starlette debug tracebacks for HTTP/SSE transports. This provides a
convenient way to enable comprehensive debugging. For granular control,
use log_level and starlette_debug separately.
"""
)
),
] = False

starlette_debug: Annotated[
bool,
Field(
description=inspect.cleandoc(
"""
Enable Starlette debug mode for HTTP/SSE transports. When enabled,
detailed error tracebacks will be returned in HTTP responses. Only
affects HTTP/SSE transports; has no effect on stdio transport.
"""
)
),
] = False

@field_validator("debug")
@classmethod
def _update_log_level_for_debug(cls, v: bool, info) -> bool:
"""When debug is enabled, set log_level to DEBUG."""
if v:
# When debug is True, we need to ensure log_level is set to DEBUG
# This is checked in model_post_init
pass
return v

def model_post_init(self, __context) -> None:
"""Post-initialization hook to handle debug mode."""
if self.debug and self.log_level != "DEBUG":
# When debug is enabled, force log_level to DEBUG
self.log_level = "DEBUG"
# Reconfigure logging if it's enabled
if self.log_enabled:
from fastmcp.utilities.logging import configure_logging

configure_logging(
level=self.log_level,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid configure_logging before fastmcp.settings exists

When FASTMCP_DEBUG=true, Settings() is constructed during fastmcp/__init__.py before the module-level settings attribute is assigned. model_post_init calls configure_logging(...) here, but configure_logging dereferences fastmcp.settings (see fastmcp/utilities/logging.py:44), which is still unset at this point. That can raise AttributeError and prevent import fastmcp whenever debug is enabled via env. Consider deferring logging configuration until after fastmcp.settings is bound (e.g., keep it in __init__.py) or guard inside configure_logging against missing fastmcp.settings.

Useful? React with 👍 / 👎.

enable_rich_tracebacks=self.enable_rich_tracebacks,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and check the relevant lines
cat -n src/fastmcp/settings.py | sed -n '285,320p'

Repository: jlowin/fastmcp

Length of output: 1531


🏁 Script executed:

# Check imports at the top of the file to see what's available
head -50 src/fastmcp/settings.py

Repository: jlowin/fastmcp

Length of output: 1237


🏁 Script executed:

# Search for the _update_log_level_for_debug method to see its exact current state
rg -A 15 "_update_log_level_for_debug" src/fastmcp/settings.py

Repository: jlowin/fastmcp

Length of output: 777


🏁 Script executed:

# Check if Any is imported and the overall type annotation coverage
rg "from typing import|import typing|from __future__ import annotations" src/fastmcp/settings.py

Repository: jlowin/fastmcp

Length of output: 151


Remove the unused info parameter and add missing type annotation for __context.

The _update_log_level_for_debug validator is a no-op and triggers Ruff ARG003 due to the unused info parameter. Additionally, model_post_init lacks a type annotation for __context, violating the full-annotations guideline. Simplify the validator by removing the unused parameter and add Any as the type for __context.

🛠️ Proposed fix
-    `@field_validator`("debug")
-    `@classmethod`
-    def _update_log_level_for_debug(cls, v: bool, info) -> bool:
-        """When debug is enabled, set log_level to DEBUG."""
-        if v:
-            # When debug is True, we need to ensure log_level is set to DEBUG
-            # This is checked in model_post_init
-            pass
-        return v
+    `@field_validator`("debug")
+    `@classmethod`
+    def _update_log_level_for_debug(cls, v: bool) -> bool:
+        """When debug is enabled, set log_level to DEBUG."""
+        return v
@@
-    def model_post_init(self, __context) -> None:
+    def model_post_init(self, __context: Any) -> None:
🧰 Tools
🪛 Ruff (0.14.13)

293-293: Unused class method argument: info

(ARG003)


# error handling
mask_error_details: Annotated[
Expand Down
3 changes: 2 additions & 1 deletion tests/deprecated/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ def test_deprecated_settings_inheritance_from_global(self):
server = FastMCP("TestServer")

# Verify settings are inherited from global settings
assert server._deprecated_settings.log_level == "WARNING"
# Note: When debug=True, log_level is forced to DEBUG (new behavior)
assert server._deprecated_settings.log_level == "DEBUG"
assert server._deprecated_settings.debug is True
assert server._deprecated_settings.host == "0.0.0.0"
assert server._deprecated_settings.port == 3000
Expand Down
89 changes: 89 additions & 0 deletions tests/test_debug_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Tests for debug and starlette_debug settings."""

import logging

import pytest

from fastmcp import settings as global_settings
from fastmcp.settings import Settings
from fastmcp.utilities.logging import get_logger


class TestDebugSettings:
"""Test debug and starlette_debug settings behavior."""

def test_debug_sets_log_level(self):
"""Test that enabling debug sets log_level to DEBUG."""
settings = Settings(debug=True, log_enabled=False)
assert settings.log_level == "DEBUG"

def test_debug_false_preserves_log_level(self):
"""Test that debug=False doesn't change log_level."""
settings = Settings(debug=False, log_level="INFO", log_enabled=False)
assert settings.log_level == "INFO"

def test_debug_with_explicit_log_level(self):
"""Test that debug=True overrides explicit log_level."""
# When debug is enabled, it forces log_level to DEBUG
settings = Settings(debug=True, log_level="WARNING", log_enabled=False)
assert settings.log_level == "DEBUG"

def test_starlette_debug_independent(self, monkeypatch):
"""Test that starlette_debug works independently."""
# Clear any env vars that might affect the test
monkeypatch.delenv("FASTMCP_LOG_LEVEL", raising=False)
settings = Settings(starlette_debug=True, log_enabled=False)
assert settings.starlette_debug is True
# log_level should not be affected (defaults to INFO)
assert settings.log_level == "INFO"

def test_both_debug_and_starlette_debug(self):
"""Test that both settings can be enabled together."""
settings = Settings(debug=True, starlette_debug=True, log_enabled=False)
assert settings.debug is True
assert settings.starlette_debug is True
assert settings.log_level == "DEBUG"

def test_debug_reconfigures_logging(self):
"""Test that enabling debug reconfigures logging."""
# Create a settings instance with debug enabled
settings = Settings(debug=True, log_enabled=True)

# Verify logging was reconfigured
logger = get_logger("test")
assert logger.getEffectiveLevel() == logging.DEBUG

def test_debug_respects_log_enabled(self):
"""Test that debug respects log_enabled setting."""
# When log_enabled is False, logging should not be reconfigured
settings = Settings(debug=True, log_enabled=False)
assert settings.log_level == "DEBUG"
# Logger should not be reconfigured, but we can't easily test this
# without side effects

def test_starlette_debug_default_false(self):
"""Test that starlette_debug defaults to False."""
settings = Settings(log_enabled=False)
assert settings.starlette_debug is False

def test_debug_default_false(self):
"""Test that debug defaults to False."""
settings = Settings(log_enabled=False)
assert settings.debug is False

def test_env_var_debug(self, monkeypatch):
"""Test that FASTMCP_DEBUG environment variable works."""
monkeypatch.setenv("FASTMCP_DEBUG", "true")
settings = Settings(log_enabled=False)
assert settings.debug is True
assert settings.log_level == "DEBUG"

def test_env_var_starlette_debug(self, monkeypatch):
"""Test that FASTMCP_STARLETTE_DEBUG environment variable works."""
# Clear any env vars that might affect the test
monkeypatch.delenv("FASTMCP_LOG_LEVEL", raising=False)
monkeypatch.setenv("FASTMCP_STARLETTE_DEBUG", "true")
settings = Settings(log_enabled=False)
assert settings.starlette_debug is True
# log_level should not be affected (defaults to INFO)
assert settings.log_level == "INFO"
Loading