From 3a528d86a34a052b40f96210db1ecb8f0f746b5a Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:39:35 -0400 Subject: [PATCH] Add CLI banner --- docs/patterns/cli.mdx | 1 + src/fastmcp/cli/cli.py | 17 +++++- src/fastmcp/cli/run.py | 4 ++ src/fastmcp/server/server.py | 53 ++++++++++++++++-- src/fastmcp/utilities/cli.py | 106 +++++++++++++++++++++++++++++++++++ 5 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 src/fastmcp/utilities/cli.py diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index 9c01d133d..663399a0f 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -46,6 +46,7 @@ This command runs the server directly in your current Python environment. You ar | Host | `--host` | Host to bind to when using http transport (default: 127.0.0.1) | | Port | `--port`, `-p` | Port to bind to when using http transport (default: 8000) | | Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) | +| No Banner | `--no-banner` | Disable the startup banner display | #### Server Specification diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index cef0ecb6b..0c0ef8421 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -64,6 +64,7 @@ def _build_uv_command( server_spec: str, with_editable: Path | None = None, with_packages: list[str] | None = None, + no_banner: bool = False, ) -> list[str]: """Build the uv run command that runs a MCP server through mcp run.""" cmd = ["uv"] @@ -80,6 +81,10 @@ def _build_uv_command( # Add mcp run command cmd.extend(["fastmcp", "run", server_spec]) + + if no_banner: + cmd.append("--no-banner") + return cmd @@ -192,7 +197,9 @@ def dev( if inspector_version: inspector_cmd += f"@{inspector_version}" - uv_cmd = _build_uv_command(server_spec, with_editable, with_packages) + uv_cmd = _build_uv_command( + server_spec, with_editable, with_packages, no_banner=True + ) # Run the MCP Inspector command with shell=True on Windows shell = sys.platform == "win32" @@ -261,6 +268,13 @@ def run( help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)", ), ] = None, + no_banner: Annotated[ + bool, + typer.Option( + "--no-banner", + help="Don't show the server banner", + ), + ] = False, ) -> None: """Run a MCP server or connect to a remote one. @@ -297,6 +311,7 @@ def run( port=port, log_level=log_level, server_args=server_args, + show_banner=not no_banner, ) except Exception as e: logger.error( diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index d8c96f2dd..ea139b636 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -169,6 +169,7 @@ def run_command( port: int | None = None, log_level: str | None = None, server_args: list[str] | None = None, + show_banner: bool = True, ) -> None: """Run a MCP server or connect to a remote one. @@ -201,6 +202,9 @@ def run_command( if log_level: kwargs["log_level"] = log_level + if not show_banner: + kwargs["show_banner"] = False + try: server.run(**kwargs) except Exception as e: diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index c424fa6e1..e365efbec 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -60,6 +60,7 @@ from fastmcp.settings import Settings from fastmcp.tools import ToolManager from fastmcp.tools.tool import FunctionTool, Tool, ToolResult from fastmcp.utilities.cache import TimedCache +from fastmcp.utilities.cli import print_server_banner from fastmcp.utilities.components import FastMCPComponent from fastmcp.utilities.logging import get_logger from fastmcp.utilities.mcp_config import MCPConfig @@ -285,6 +286,7 @@ class FastMCP(Generic[LifespanResultT]): async def run_async( self, transport: Transport | None = None, + show_banner: bool = True, **transport_kwargs: Any, ) -> None: """Run the FastMCP server asynchronously. @@ -298,15 +300,23 @@ class FastMCP(Generic[LifespanResultT]): raise ValueError(f"Unknown transport: {transport}") if transport == "stdio": - await self.run_stdio_async(**transport_kwargs) + await self.run_stdio_async( + show_banner=show_banner, + **transport_kwargs, + ) elif transport in {"http", "sse", "streamable-http"}: - await self.run_http_async(transport=transport, **transport_kwargs) + await self.run_http_async( + transport=transport, + show_banner=show_banner, + **transport_kwargs, + ) else: raise ValueError(f"Unknown transport: {transport}") def run( self, transport: Transport | None = None, + show_banner: bool = True, **transport_kwargs: Any, ) -> None: """Run the FastMCP server. Note this is a synchronous function. @@ -315,7 +325,14 @@ class FastMCP(Generic[LifespanResultT]): transport: Transport protocol to use ("stdio", "sse", or "streamable-http") """ - anyio.run(partial(self.run_async, transport, **transport_kwargs)) + anyio.run( + partial( + self.run_async, + transport, + show_banner=show_banner, + **transport_kwargs, + ) + ) def _setup_handlers(self) -> None: """Set up core MCP protocol handlers.""" @@ -1321,8 +1338,16 @@ class FastMCP(Generic[LifespanResultT]): enabled=enabled, ) - async def run_stdio_async(self) -> None: + async def run_stdio_async(self, show_banner: bool = True) -> None: """Run the server using stdio transport.""" + + # Display server banner + if show_banner: + print_server_banner( + server=self, + transport="stdio", + ) + async with stdio_server() as (read_stream, write_stream): logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'") await self._mcp_server.run( @@ -1335,6 +1360,7 @@ class FastMCP(Generic[LifespanResultT]): async def run_http_async( self, + show_banner: bool = True, transport: Literal["http", "streamable-http", "sse"] = "http", host: str | None = None, port: int | None = None, @@ -1353,6 +1379,7 @@ 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._deprecated_settings.host port = port or self._deprecated_settings.port default_log_level_to_use = ( @@ -1361,6 +1388,23 @@ class FastMCP(Generic[LifespanResultT]): app = self.http_app(path=path, transport=transport, middleware=middleware) + # Get the path for the server URL + server_path = ( + app.state.path.lstrip("/") + if hasattr(app, "state") and hasattr(app.state, "path") + else path or "" + ) + + # Display server banner + if show_banner: + print_server_banner( + server=self, + transport=transport, + host=host, + port=port, + path=server_path, + ) + _uvicorn_config_from_user = uvicorn_config or {} config_kwargs: dict[str, Any] = { @@ -1378,6 +1422,7 @@ class FastMCP(Generic[LifespanResultT]): logger.info( f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}" ) + await server.serve() async def run_sse_async( diff --git a/src/fastmcp/utilities/cli.py b/src/fastmcp/utilities/cli.py new file mode 100644 index 000000000..edc54870f --- /dev/null +++ b/src/fastmcp/utilities/cli.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from importlib.metadata import version +from typing import TYPE_CHECKING, Any + +from rich.console import Console, Group +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +import fastmcp + +if TYPE_CHECKING: + from typing import Literal + + from fastmcp import FastMCP + +LOGO_ASCII = r""" + _ __ ___ ______ __ __ _____________ ____ ____ + _ __ ___ / ____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \ + _ __ ___ / /_ / __ `/ ___/ __/ /|_/ / / / /_/ / ___/ / / / / / + _ __ ___ / __/ / /_/ (__ ) /_/ / / / /___/ ____/ / __/_/ /_/ / +_ __ ___ /_/ \__,_/____/\__/_/ /_/\____/_/ /_____(_)____/ + +""".lstrip("\n") + + +def print_server_banner( + server: FastMCP[Any], + transport: Literal["stdio", "http", "sse", "streamable-http"], + *, + host: str | None = None, + port: int | None = None, + path: str | None = None, +) -> None: + """Print a formatted banner with server information and logo. + + Args: + transport: The transport protocol being used + server_name: Optional server name to display + host: Host address (for HTTP transports) + port: Port number (for HTTP transports) + path: Server path (for HTTP transports) + """ + + console = Console() + + # Create the logo text + logo_text = Text(LOGO_ASCII, style="bold green") + + # Create the information table + info_table = Table.grid(padding=(0, 1)) + info_table.add_column(style="bold cyan", justify="left") + info_table.add_column(style="white", justify="left") + + match transport: + case "http" | "streamable-http": + display_transport = "Streamable-HTTP" + case "sse": + display_transport = "SSE" + case "stdio": + display_transport = "STDIO" + + info_table.add_row("Transport:", display_transport) + + # Show connection info based on transport + if transport in ("http", "streamable-http", "sse"): + if host and port: + server_url = f"http://{host}:{port}" + if path: + server_url += f"/{path.lstrip('/')}" + info_table.add_row("Server URL:", server_url) + + # Add documentation link + info_table.add_row() + info_table.add_row("Docs:", "https://gofastmcp.com") + info_table.add_row("Hosting:", "https://fastmcp.cloud") + + # Add version information with explicit style overrides + info_table.add_row() + info_table.add_row( + "FastMCP version:", + Text(fastmcp.__version__, style="dim white", no_wrap=True), + ) + info_table.add_row( + "MCP version:", + Text(version("mcp"), style="dim white", no_wrap=True), + ) + # Create panel with logo and information using Group + panel_content = Group(logo_text, "", info_table) + + # Use server name in title if provided + title = "FastMCP 2.0" + if server.name != "FastMCP": + title += f" - {server.name}" + + panel = Panel( + panel_content, + title=title, + title_align="left", + border_style="dim", + padding=(2, 10), + expand=False, + ) + + console.print(panel)