diff --git a/docs/development/upgrade-guide.mdx b/docs/development/upgrade-guide.mdx index c5a5c84b3c..d620832730 100644 --- a/docs/development/upgrade-guide.mdx +++ b/docs/development/upgrade-guide.mdx @@ -179,6 +179,15 @@ This applies to all auth providers: `GitHubProvider`, `GoogleProvider`, `AzurePr The `FastMCPSettings` class has also been simplified - it no longer includes auth-related settings that were previously loaded from environment variables. +### Server Banner Environment Variable Renamed + +The environment variable for controlling the server banner has been renamed: + +- **Before:** `FASTMCP_SHOW_CLI_BANNER` +- **After:** `FASTMCP_SHOW_SERVER_BANNER` + +This change reflects that the setting now applies to all server startup methods, not just the CLI. The banner is now suppressed when running `python server.py` directly, not just when using `fastmcp run`. + ## v2.14.0 ### OpenAPI Parser Promotion diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index a5e5d66985..0387e0df33 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -423,8 +423,10 @@ async def run( final_log_level = log_level or config.deployment.log_level final_server_args = server_args or config.deployment.args # Use CLI override if provided, otherwise use settings - # no_banner CLI flag overrides the show_cli_banner setting - final_no_banner = no_banner if no_banner else not fastmcp.settings.show_cli_banner + # no_banner CLI flag overrides the show_server_banner setting + final_no_banner = ( + no_banner if no_banner else not fastmcp.settings.show_server_banner + ) logger.debug( "Running server or client", diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 8e863cdf1a..b7a1d03ed8 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -556,14 +556,18 @@ async def _lifespan_manager(self) -> AsyncIterator[None]: async def run_async( self, transport: Transport | None = None, - show_banner: bool = True, + show_banner: bool | None = None, **transport_kwargs: Any, ) -> None: """Run the FastMCP server asynchronously. Args: transport: Transport protocol to use ("stdio", "sse", or "streamable-http") + show_banner: Whether to display the server banner. If None, uses the + FASTMCP_SHOW_SERVER_BANNER setting (default: True). """ + if show_banner is None: + show_banner = fastmcp.settings.show_server_banner if transport is None: transport = "stdio" if transport not in {"stdio", "http", "sse", "streamable-http"}: @@ -586,13 +590,15 @@ async def run_async( def run( self, transport: Transport | None = None, - show_banner: bool = True, + show_banner: bool | None = None, **transport_kwargs: Any, ) -> None: """Run the FastMCP server. Note this is a synchronous function. Args: transport: Transport protocol to use ("http", "stdio", "sse", or "streamable-http") + show_banner: Whether to display the server banner. If None, uses the + FASTMCP_SHOW_SERVER_BANNER setting (default: True). """ anyio.run( diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index af7fca0a4e..ce589834b9 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -320,14 +320,15 @@ def normalize_log_level(cls, v): ), ] = False - show_cli_banner: Annotated[ + show_server_banner: Annotated[ bool, Field( description=inspect.cleandoc( """ - If True, the server banner will be displayed when running the server via CLI. - This setting can be overridden by the --no-banner CLI flag. - Set to False via FASTMCP_SHOW_CLI_BANNER=false to suppress the banner. + If True, the server banner will be displayed when running the server. + This setting can be overridden by the --no-banner CLI flag or by + passing show_banner=False to server.run(). + Set to False via FASTMCP_SHOW_SERVER_BANNER=false to suppress the banner. """ ), ), diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 741a1bf3c2..a6ab3b8a7e 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -374,8 +374,8 @@ def test_run_command_parsing_project_and_skip_source(self): assert bound.arguments["project"] == Path("./test-env") assert bound.arguments["skip_source"] is True - def test_show_cli_banner_setting(self): - """Test that show_cli_banner setting works with environment variable.""" + def test_show_server_banner_setting(self): + """Test that show_server_banner setting works with environment variable.""" import os from unittest import mock @@ -383,19 +383,19 @@ def test_show_cli_banner_setting(self): # Test default (banner shown) settings = Settings() - assert settings.show_cli_banner is True + assert settings.show_server_banner is True # Test with env var set to false (banner hidden) - with mock.patch.dict(os.environ, {"FASTMCP_SHOW_CLI_BANNER": "false"}): + with mock.patch.dict(os.environ, {"FASTMCP_SHOW_SERVER_BANNER": "false"}): settings = Settings() - assert settings.show_cli_banner is False + assert settings.show_server_banner is False # Test CLI precedence logic (simulated) - with mock.patch.dict(os.environ, {"FASTMCP_SHOW_CLI_BANNER": "true"}): + with mock.patch.dict(os.environ, {"FASTMCP_SHOW_SERVER_BANNER": "true"}): settings = Settings() # CLI --no-banner flag would override cli_no_banner = True - final = cli_no_banner if cli_no_banner else not settings.show_cli_banner + final = cli_no_banner if cli_no_banner else not settings.show_server_banner assert final is True # Banner suppressed by CLI flag diff --git a/tests/server/test_server.py b/tests/server/test_server.py index 2ba050b994..9cb62b5300 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -1,6 +1,8 @@ +import os from pathlib import Path from tempfile import TemporaryDirectory from textwrap import dedent +from unittest import mock from mcp.types import TextContent, TextResourceContents @@ -417,3 +419,51 @@ def sample_tool(x: int) -> int: tool = next(t for t in tools if t.name == "sample_tool") assert tool.meta is not None assert set(tool.meta["_fastmcp"]["tags"]) == {"test-tag"} + + +class TestShowServerBannerSetting: + """Test that show_server_banner setting controls banner display.""" + + async def test_show_banner_defaults_to_setting_true(self): + """Test that show_banner=None uses the setting (default True).""" + mcp = FastMCP() + + with mock.patch.object(mcp, "run_stdio_async") as mock_run: + mock_run.return_value = None + await mcp.run_async(transport="stdio") + mock_run.assert_called_once() + assert mock_run.call_args.kwargs["show_banner"] is True + + async def test_show_banner_respects_setting_false(self): + """Test that show_banner=None uses the setting when False.""" + mcp = FastMCP() + + with mock.patch.dict(os.environ, {"FASTMCP_SHOW_SERVER_BANNER": "false"}): + with temporary_settings(show_server_banner=False): + with mock.patch.object(mcp, "run_stdio_async") as mock_run: + mock_run.return_value = None + await mcp.run_async(transport="stdio") + mock_run.assert_called_once() + assert mock_run.call_args.kwargs["show_banner"] is False + + async def test_show_banner_explicit_true_overrides_setting(self): + """Test that explicit show_banner=True overrides False setting.""" + mcp = FastMCP() + + with temporary_settings(show_server_banner=False): + with mock.patch.object(mcp, "run_stdio_async") as mock_run: + mock_run.return_value = None + await mcp.run_async(transport="stdio", show_banner=True) + mock_run.assert_called_once() + assert mock_run.call_args.kwargs["show_banner"] is True + + async def test_show_banner_explicit_false_overrides_setting(self): + """Test that explicit show_banner=False overrides True setting.""" + mcp = FastMCP() + + with temporary_settings(show_server_banner=True): + with mock.patch.object(mcp, "run_stdio_async") as mock_run: + mock_run.return_value = None + await mcp.run_async(transport="stdio", show_banner=False) + mock_run.assert_called_once() + assert mock_run.call_args.kwargs["show_banner"] is False