Jeremiah Lowin commited on
Commit
031ff24
·
1 Parent(s): 3661d04

Add CLI banner

Browse files
docs/patterns/cli.mdx CHANGED
@@ -46,6 +46,7 @@ This command runs the server directly in your current Python environment. You ar
46
  | Host | `--host` | Host to bind to when using http transport (default: 127.0.0.1) |
47
  | Port | `--port`, `-p` | Port to bind to when using http transport (default: 8000) |
48
  | Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
 
49
 
50
 
51
  #### Server Specification
 
46
  | Host | `--host` | Host to bind to when using http transport (default: 127.0.0.1) |
47
  | Port | `--port`, `-p` | Port to bind to when using http transport (default: 8000) |
48
  | Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
49
+ | No Banner | `--no-banner` | Disable the startup banner display |
50
 
51
 
52
  #### Server Specification
src/fastmcp/cli/cli.py CHANGED
@@ -64,6 +64,7 @@ def _build_uv_command(
64
  server_spec: str,
65
  with_editable: Path | None = None,
66
  with_packages: list[str] | None = None,
 
67
  ) -> list[str]:
68
  """Build the uv run command that runs a MCP server through mcp run."""
69
  cmd = ["uv"]
@@ -80,6 +81,10 @@ def _build_uv_command(
80
 
81
  # Add mcp run command
82
  cmd.extend(["fastmcp", "run", server_spec])
 
 
 
 
83
  return cmd
84
 
85
 
@@ -192,7 +197,9 @@ def dev(
192
  if inspector_version:
193
  inspector_cmd += f"@{inspector_version}"
194
 
195
- uv_cmd = _build_uv_command(server_spec, with_editable, with_packages)
 
 
196
 
197
  # Run the MCP Inspector command with shell=True on Windows
198
  shell = sys.platform == "win32"
@@ -261,6 +268,13 @@ def run(
261
  help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
262
  ),
263
  ] = None,
 
 
 
 
 
 
 
264
  ) -> None:
265
  """Run a MCP server or connect to a remote one.
266
 
@@ -297,6 +311,7 @@ def run(
297
  port=port,
298
  log_level=log_level,
299
  server_args=server_args,
 
300
  )
301
  except Exception as e:
302
  logger.error(
 
64
  server_spec: str,
65
  with_editable: Path | None = None,
66
  with_packages: list[str] | None = None,
67
+ no_banner: bool = False,
68
  ) -> list[str]:
69
  """Build the uv run command that runs a MCP server through mcp run."""
70
  cmd = ["uv"]
 
81
 
82
  # Add mcp run command
83
  cmd.extend(["fastmcp", "run", server_spec])
84
+
85
+ if no_banner:
86
+ cmd.append("--no-banner")
87
+
88
  return cmd
89
 
90
 
 
197
  if inspector_version:
198
  inspector_cmd += f"@{inspector_version}"
199
 
200
+ uv_cmd = _build_uv_command(
201
+ server_spec, with_editable, with_packages, no_banner=True
202
+ )
203
 
204
  # Run the MCP Inspector command with shell=True on Windows
205
  shell = sys.platform == "win32"
 
268
  help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
269
  ),
270
  ] = None,
271
+ no_banner: Annotated[
272
+ bool,
273
+ typer.Option(
274
+ "--no-banner",
275
+ help="Don't show the server banner",
276
+ ),
277
+ ] = False,
278
  ) -> None:
279
  """Run a MCP server or connect to a remote one.
280
 
 
311
  port=port,
312
  log_level=log_level,
313
  server_args=server_args,
314
+ show_banner=not no_banner,
315
  )
316
  except Exception as e:
317
  logger.error(
src/fastmcp/cli/run.py CHANGED
@@ -169,6 +169,7 @@ def run_command(
169
  port: int | None = None,
170
  log_level: str | None = None,
171
  server_args: list[str] | None = None,
 
172
  ) -> None:
173
  """Run a MCP server or connect to a remote one.
174
 
@@ -201,6 +202,9 @@ def run_command(
201
  if log_level:
202
  kwargs["log_level"] = log_level
203
 
 
 
 
204
  try:
205
  server.run(**kwargs)
206
  except Exception as e:
 
169
  port: int | None = None,
170
  log_level: str | None = None,
171
  server_args: list[str] | None = None,
172
+ show_banner: bool = True,
173
  ) -> None:
174
  """Run a MCP server or connect to a remote one.
175
 
 
202
  if log_level:
203
  kwargs["log_level"] = log_level
204
 
205
+ if not show_banner:
206
+ kwargs["show_banner"] = False
207
+
208
  try:
209
  server.run(**kwargs)
210
  except Exception as e:
src/fastmcp/server/server.py CHANGED
@@ -60,6 +60,7 @@ from fastmcp.settings import Settings
60
  from fastmcp.tools import ToolManager
61
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
62
  from fastmcp.utilities.cache import TimedCache
 
63
  from fastmcp.utilities.components import FastMCPComponent
64
  from fastmcp.utilities.logging import get_logger
65
  from fastmcp.utilities.mcp_config import MCPConfig
@@ -285,6 +286,7 @@ class FastMCP(Generic[LifespanResultT]):
285
  async def run_async(
286
  self,
287
  transport: Transport | None = None,
 
288
  **transport_kwargs: Any,
289
  ) -> None:
290
  """Run the FastMCP server asynchronously.
@@ -298,15 +300,23 @@ class FastMCP(Generic[LifespanResultT]):
298
  raise ValueError(f"Unknown transport: {transport}")
299
 
300
  if transport == "stdio":
301
- await self.run_stdio_async(**transport_kwargs)
 
 
 
302
  elif transport in {"http", "sse", "streamable-http"}:
303
- await self.run_http_async(transport=transport, **transport_kwargs)
 
 
 
 
304
  else:
305
  raise ValueError(f"Unknown transport: {transport}")
306
 
307
  def run(
308
  self,
309
  transport: Transport | None = None,
 
310
  **transport_kwargs: Any,
311
  ) -> None:
312
  """Run the FastMCP server. Note this is a synchronous function.
@@ -315,7 +325,14 @@ class FastMCP(Generic[LifespanResultT]):
315
  transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
316
  """
317
 
318
- anyio.run(partial(self.run_async, transport, **transport_kwargs))
 
 
 
 
 
 
 
319
 
320
  def _setup_handlers(self) -> None:
321
  """Set up core MCP protocol handlers."""
@@ -1321,8 +1338,16 @@ class FastMCP(Generic[LifespanResultT]):
1321
  enabled=enabled,
1322
  )
1323
 
1324
- async def run_stdio_async(self) -> None:
1325
  """Run the server using stdio transport."""
 
 
 
 
 
 
 
 
1326
  async with stdio_server() as (read_stream, write_stream):
1327
  logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
1328
  await self._mcp_server.run(
@@ -1335,6 +1360,7 @@ class FastMCP(Generic[LifespanResultT]):
1335
 
1336
  async def run_http_async(
1337
  self,
 
1338
  transport: Literal["http", "streamable-http", "sse"] = "http",
1339
  host: str | None = None,
1340
  port: int | None = None,
@@ -1353,6 +1379,7 @@ class FastMCP(Generic[LifespanResultT]):
1353
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1354
  uvicorn_config: Additional configuration for the Uvicorn server
1355
  """
 
1356
  host = host or self._deprecated_settings.host
1357
  port = port or self._deprecated_settings.port
1358
  default_log_level_to_use = (
@@ -1361,6 +1388,23 @@ class FastMCP(Generic[LifespanResultT]):
1361
 
1362
  app = self.http_app(path=path, transport=transport, middleware=middleware)
1363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1364
  _uvicorn_config_from_user = uvicorn_config or {}
1365
 
1366
  config_kwargs: dict[str, Any] = {
@@ -1378,6 +1422,7 @@ class FastMCP(Generic[LifespanResultT]):
1378
  logger.info(
1379
  f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
1380
  )
 
1381
  await server.serve()
1382
 
1383
  async def run_sse_async(
 
60
  from fastmcp.tools import ToolManager
61
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
62
  from fastmcp.utilities.cache import TimedCache
63
+ from fastmcp.utilities.cli import print_server_banner
64
  from fastmcp.utilities.components import FastMCPComponent
65
  from fastmcp.utilities.logging import get_logger
66
  from fastmcp.utilities.mcp_config import MCPConfig
 
286
  async def run_async(
287
  self,
288
  transport: Transport | None = None,
289
+ show_banner: bool = True,
290
  **transport_kwargs: Any,
291
  ) -> None:
292
  """Run the FastMCP server asynchronously.
 
300
  raise ValueError(f"Unknown transport: {transport}")
301
 
302
  if transport == "stdio":
303
+ await self.run_stdio_async(
304
+ show_banner=show_banner,
305
+ **transport_kwargs,
306
+ )
307
  elif transport in {"http", "sse", "streamable-http"}:
308
+ await self.run_http_async(
309
+ transport=transport,
310
+ show_banner=show_banner,
311
+ **transport_kwargs,
312
+ )
313
  else:
314
  raise ValueError(f"Unknown transport: {transport}")
315
 
316
  def run(
317
  self,
318
  transport: Transport | None = None,
319
+ show_banner: bool = True,
320
  **transport_kwargs: Any,
321
  ) -> None:
322
  """Run the FastMCP server. Note this is a synchronous function.
 
325
  transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
326
  """
327
 
328
+ anyio.run(
329
+ partial(
330
+ self.run_async,
331
+ transport,
332
+ show_banner=show_banner,
333
+ **transport_kwargs,
334
+ )
335
+ )
336
 
337
  def _setup_handlers(self) -> None:
338
  """Set up core MCP protocol handlers."""
 
1338
  enabled=enabled,
1339
  )
1340
 
1341
+ async def run_stdio_async(self, show_banner: bool = True) -> None:
1342
  """Run the server using stdio transport."""
1343
+
1344
+ # Display server banner
1345
+ if show_banner:
1346
+ print_server_banner(
1347
+ server=self,
1348
+ transport="stdio",
1349
+ )
1350
+
1351
  async with stdio_server() as (read_stream, write_stream):
1352
  logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
1353
  await self._mcp_server.run(
 
1360
 
1361
  async def run_http_async(
1362
  self,
1363
+ show_banner: bool = True,
1364
  transport: Literal["http", "streamable-http", "sse"] = "http",
1365
  host: str | None = None,
1366
  port: int | None = None,
 
1379
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1380
  uvicorn_config: Additional configuration for the Uvicorn server
1381
  """
1382
+
1383
  host = host or self._deprecated_settings.host
1384
  port = port or self._deprecated_settings.port
1385
  default_log_level_to_use = (
 
1388
 
1389
  app = self.http_app(path=path, transport=transport, middleware=middleware)
1390
 
1391
+ # Get the path for the server URL
1392
+ server_path = (
1393
+ app.state.path.lstrip("/")
1394
+ if hasattr(app, "state") and hasattr(app.state, "path")
1395
+ else path or ""
1396
+ )
1397
+
1398
+ # Display server banner
1399
+ if show_banner:
1400
+ print_server_banner(
1401
+ server=self,
1402
+ transport=transport,
1403
+ host=host,
1404
+ port=port,
1405
+ path=server_path,
1406
+ )
1407
+
1408
  _uvicorn_config_from_user = uvicorn_config or {}
1409
 
1410
  config_kwargs: dict[str, Any] = {
 
1422
  logger.info(
1423
  f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
1424
  )
1425
+
1426
  await server.serve()
1427
 
1428
  async def run_sse_async(
src/fastmcp/utilities/cli.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import version
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from rich.console import Console, Group
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+ from rich.text import Text
10
+
11
+ import fastmcp
12
+
13
+ if TYPE_CHECKING:
14
+ from typing import Literal
15
+
16
+ from fastmcp import FastMCP
17
+
18
+ LOGO_ASCII = r"""
19
+ _ __ ___ ______ __ __ _____________ ____ ____
20
+ _ __ ___ / ____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \
21
+ _ __ ___ / /_ / __ `/ ___/ __/ /|_/ / / / /_/ / ___/ / / / / /
22
+ _ __ ___ / __/ / /_/ (__ ) /_/ / / / /___/ ____/ / __/_/ /_/ /
23
+ _ __ ___ /_/ \__,_/____/\__/_/ /_/\____/_/ /_____(_)____/
24
+
25
+ """.lstrip("\n")
26
+
27
+
28
+ def print_server_banner(
29
+ server: FastMCP[Any],
30
+ transport: Literal["stdio", "http", "sse", "streamable-http"],
31
+ *,
32
+ host: str | None = None,
33
+ port: int | None = None,
34
+ path: str | None = None,
35
+ ) -> None:
36
+ """Print a formatted banner with server information and logo.
37
+
38
+ Args:
39
+ transport: The transport protocol being used
40
+ server_name: Optional server name to display
41
+ host: Host address (for HTTP transports)
42
+ port: Port number (for HTTP transports)
43
+ path: Server path (for HTTP transports)
44
+ """
45
+
46
+ console = Console()
47
+
48
+ # Create the logo text
49
+ logo_text = Text(LOGO_ASCII, style="bold green")
50
+
51
+ # Create the information table
52
+ info_table = Table.grid(padding=(0, 1))
53
+ info_table.add_column(style="bold cyan", justify="left")
54
+ info_table.add_column(style="white", justify="left")
55
+
56
+ match transport:
57
+ case "http" | "streamable-http":
58
+ display_transport = "Streamable-HTTP"
59
+ case "sse":
60
+ display_transport = "SSE"
61
+ case "stdio":
62
+ display_transport = "STDIO"
63
+
64
+ info_table.add_row("Transport:", display_transport)
65
+
66
+ # Show connection info based on transport
67
+ if transport in ("http", "streamable-http", "sse"):
68
+ if host and port:
69
+ server_url = f"http://{host}:{port}"
70
+ if path:
71
+ server_url += f"/{path.lstrip('/')}"
72
+ info_table.add_row("Server URL:", server_url)
73
+
74
+ # Add documentation link
75
+ info_table.add_row()
76
+ info_table.add_row("Docs:", "https://gofastmcp.com")
77
+ info_table.add_row("Hosting:", "https://fastmcp.cloud")
78
+
79
+ # Add version information with explicit style overrides
80
+ info_table.add_row()
81
+ info_table.add_row(
82
+ "FastMCP version:",
83
+ Text(fastmcp.__version__, style="dim white", no_wrap=True),
84
+ )
85
+ info_table.add_row(
86
+ "MCP version:",
87
+ Text(version("mcp"), style="dim white", no_wrap=True),
88
+ )
89
+ # Create panel with logo and information using Group
90
+ panel_content = Group(logo_text, "", info_table)
91
+
92
+ # Use server name in title if provided
93
+ title = "FastMCP 2.0"
94
+ if server.name != "FastMCP":
95
+ title += f" - {server.name}"
96
+
97
+ panel = Panel(
98
+ panel_content,
99
+ title=title,
100
+ title_align="left",
101
+ border_style="dim",
102
+ padding=(2, 10),
103
+ expand=False,
104
+ )
105
+
106
+ console.print(panel)