From b2fbe1a1bb3963066bf4068da8af18d2d8d8aef3 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 15:58:21 +0000 Subject: [PATCH 1/2] Enhance debug settings with dual-purpose flag and granular control Add starlette_debug setting for Starlette-specific debug control and enhance the debug flag to set both log level and Starlette debug mode. - Add starlette_debug setting for granular control over Starlette debug mode - Update debug flag to set log_level=DEBUG and enable Starlette debug - Update HTTP app creators to accept both debug and starlette_debug parameters - Document both settings in server.mdx with clear usage guidelines - Add comprehensive tests for new settings behavior - Update existing test to reflect new debug behavior Co-authored-by: Bill Easton --- docs/servers/server.mdx | 2 + src/fastmcp/server/http.py | 12 +++-- src/fastmcp/server/server.py | 2 + src/fastmcp/settings.py | 52 +++++++++++++++++- tests/deprecated/test_settings.py | 3 +- tests/test_debug_settings.py | 89 +++++++++++++++++++++++++++++++ 6 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 tests/test_debug_settings.py diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx index 7f5c6fa41..85891b5d1 100644 --- a/docs/servers/server.mdx +++ b/docs/servers/server.mdx @@ -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 - **`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 diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 530b6c0c2..d4e537665 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -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: @@ -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: @@ -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 @@ -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: @@ -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 @@ -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 diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index c2f7ce297..ae6964f20 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -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": @@ -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, ) diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 0257e8d3b..fd26cd89b 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -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, + enable_rich_tracebacks=self.enable_rich_tracebacks, + ) # error handling mask_error_details: Annotated[ diff --git a/tests/deprecated/test_settings.py b/tests/deprecated/test_settings.py index 301abf410..0f0ee3b1a 100644 --- a/tests/deprecated/test_settings.py +++ b/tests/deprecated/test_settings.py @@ -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 diff --git a/tests/test_debug_settings.py b/tests/test_debug_settings.py new file mode 100644 index 000000000..49d024580 --- /dev/null +++ b/tests/test_debug_settings.py @@ -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" From 139801718fba484d2f1c2eca99e308a0f447c934 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 16:16:23 +0000 Subject: [PATCH 2/2] Fix static analysis issues and address review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix unused variable in test (settings → _settings) - Add type annotation for __context parameter - Remove unused info parameter from validator - Fix circular import with configure_logging - Add production safety warnings to documentation Co-authored-by: Bill Easton --- docs/servers/server.mdx | 4 ++-- src/fastmcp/settings.py | 24 +++++++++++++----------- tests/test_debug_settings.py | 5 +---- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx index 85891b5d1..8645691f1 100644 --- a/docs/servers/server.mdx +++ b/docs/servers/server.mdx @@ -362,8 +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 +- **`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. **WARNING**: Only enable in development - debug tracebacks expose detailed internal information +- **`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. **WARNING**: Only enable in development - exposes detailed internal information in HTTP responses - **`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 diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index fd26cd89b..2e61d47b7 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -290,27 +290,29 @@ def normalize_log_level(cls, v): @field_validator("debug") @classmethod - def _update_log_level_for_debug(cls, v: bool, info) -> bool: + def _update_log_level_for_debug(cls, v: bool) -> 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: + def model_post_init(self, __context: Any) -> 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 + # Only reconfigure if fastmcp module is fully initialized + # to avoid AttributeError during import if self.log_enabled: - from fastmcp.utilities.logging import configure_logging + import fastmcp + + # Check if fastmcp.settings exists (module fully initialized) + if hasattr(fastmcp, "settings"): + from fastmcp.utilities.logging import configure_logging - configure_logging( - level=self.log_level, - enable_rich_tracebacks=self.enable_rich_tracebacks, - ) + configure_logging( + level=self.log_level, + enable_rich_tracebacks=self.enable_rich_tracebacks, + ) # error handling mask_error_details: Annotated[ diff --git a/tests/test_debug_settings.py b/tests/test_debug_settings.py index 49d024580..6e7111568 100644 --- a/tests/test_debug_settings.py +++ b/tests/test_debug_settings.py @@ -2,9 +2,6 @@ import logging -import pytest - -from fastmcp import settings as global_settings from fastmcp.settings import Settings from fastmcp.utilities.logging import get_logger @@ -47,7 +44,7 @@ def test_both_debug_and_starlette_debug(self): 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) + _settings = Settings(debug=True, log_enabled=True) # Verify logging was reconfigured logger = get_logger("test")