Add CLI banner

This commit is contained in:
Jeremiah Lowin 2025-07-01 10:39:35 -04:00
commit 3a528d86a3
5 changed files with 176 additions and 5 deletions

View file

@ -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

View file

@ -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(

View file

@ -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:

View file

@ -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(

View file

@ -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)