Jeremiah Lowin commited on
Commit
1609c7b
·
unverified ·
2 Parent(s): b328fdc8d6a116

Merge pull request #1027 from jlowin/cli-splash

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,18 @@ 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 +1362,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 +1381,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 +1390,24 @@ 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 +1425,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 create_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
+ logger.info(
1347
+ create_server_banner(
1348
+ server=self,
1349
+ transport="stdio",
1350
+ )
1351
+ )
1352
+
1353
  async with stdio_server() as (read_stream, write_stream):
1354
  logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
1355
  await self._mcp_server.run(
 
1362
 
1363
  async def run_http_async(
1364
  self,
1365
+ show_banner: bool = True,
1366
  transport: Literal["http", "streamable-http", "sse"] = "http",
1367
  host: str | None = None,
1368
  port: int | None = None,
 
1381
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1382
  uvicorn_config: Additional configuration for the Uvicorn server
1383
  """
1384
+
1385
  host = host or self._deprecated_settings.host
1386
  port = port or self._deprecated_settings.port
1387
  default_log_level_to_use = (
 
1390
 
1391
  app = self.http_app(path=path, transport=transport, middleware=middleware)
1392
 
1393
+ # Get the path for the server URL
1394
+ server_path = (
1395
+ app.state.path.lstrip("/")
1396
+ if hasattr(app, "state") and hasattr(app.state, "path")
1397
+ else path or ""
1398
+ )
1399
+
1400
+ # Display server banner
1401
+ if show_banner:
1402
+ logger.info(
1403
+ create_server_banner(
1404
+ server=self,
1405
+ transport=transport,
1406
+ host=host,
1407
+ port=port,
1408
+ path=server_path,
1409
+ )
1410
+ )
1411
  _uvicorn_config_from_user = uvicorn_config or {}
1412
 
1413
  config_kwargs: dict[str, Any] = {
 
1425
  logger.info(
1426
  f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
1427
  )
1428
+
1429
  await server.serve()
1430
 
1431
  async def run_sse_async(
src/fastmcp/utilities/cli.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import version
4
+ from typing import TYPE_CHECKING, Any, Literal
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 fastmcp import FastMCP
15
+
16
+ LOGO_ASCII = r"""
17
+ _ __ ___ ______ __ __ _____________ ____ ____
18
+ _ __ ___ / ____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \
19
+ _ __ ___ / /_ / __ `/ ___/ __/ /|_/ / / / /_/ / ___/ / / / / /
20
+ _ __ ___ / __/ / /_/ (__ ) /_/ / / / /___/ ____/ / __/_/ /_/ /
21
+ _ __ ___ /_/ \__,_/____/\__/_/ /_/\____/_/ /_____(_)____/
22
+
23
+ """.lstrip("\n")
24
+
25
+
26
+ def create_server_banner(
27
+ server: FastMCP[Any],
28
+ transport: Literal["stdio", "http", "sse", "streamable-http"],
29
+ *,
30
+ host: str | None = None,
31
+ port: int | None = None,
32
+ path: str | None = None,
33
+ ) -> str:
34
+ """Creates a formatted banner (as a string) with server information and logo.
35
+
36
+ Args:
37
+ transport: The transport protocol being used
38
+ server_name: Optional server name to display
39
+ host: Host address (for HTTP transports)
40
+ port: Port number (for HTTP transports)
41
+ path: Server path (for HTTP transports)
42
+
43
+ Returns:
44
+ A string representation of the banner.
45
+ """
46
+
47
+ # Create the logo text
48
+ logo_text = Text(LOGO_ASCII, style="bold green")
49
+
50
+ # Create the information table
51
+ info_table = Table.grid(padding=(0, 1))
52
+ info_table.add_column(style="bold cyan", justify="left")
53
+ info_table.add_column(style="white", justify="left")
54
+
55
+ match transport:
56
+ case "http" | "streamable-http":
57
+ display_transport = "Streamable-HTTP"
58
+ case "sse":
59
+ display_transport = "SSE"
60
+ case "stdio":
61
+ display_transport = "STDIO"
62
+
63
+ info_table.add_row("Transport:", display_transport)
64
+
65
+ # Show connection info based on transport
66
+ if transport in ("http", "streamable-http", "sse"):
67
+ if host and port:
68
+ server_url = f"http://{host}:{port}"
69
+ if path:
70
+ server_url += f"/{path.lstrip('/')}"
71
+ info_table.add_row("Server URL:", server_url)
72
+
73
+ # Add documentation link
74
+ info_table.add_row()
75
+ info_table.add_row("Docs:", "https://gofastmcp.com")
76
+ info_table.add_row("Hosting:", "https://fastmcp.cloud")
77
+
78
+ # Add version information with explicit style overrides
79
+ info_table.add_row()
80
+ info_table.add_row(
81
+ "FastMCP version:",
82
+ Text(fastmcp.__version__, style="dim white", no_wrap=True),
83
+ )
84
+ info_table.add_row(
85
+ "MCP version:",
86
+ Text(version("mcp"), style="dim white", no_wrap=True),
87
+ )
88
+ # Create panel with logo and information using Group
89
+ panel_content = Group(logo_text, "", info_table)
90
+
91
+ # Use server name in title if provided
92
+ title = "FastMCP 2.0"
93
+ if server.name != "FastMCP":
94
+ title += f" - {server.name}"
95
+
96
+ panel = Panel(
97
+ panel_content,
98
+ title=title,
99
+ title_align="left",
100
+ border_style="dim",
101
+ padding=(2, 10),
102
+ expand=False,
103
+ )
104
+
105
+ console = Console()
106
+ with console.capture() as capture:
107
+ console.print(panel)
108
+ rendered = capture.get()
109
+ return f"\n\n{rendered}"
tests/cli/test_cli.py CHANGED
@@ -209,9 +209,9 @@ class TestDevCommand:
209
  assert result.exit_code == 0
210
  mock_run.assert_called_once()
211
 
212
- # Check dependencies were passed correctly
213
  mock_build_uv.assert_called_once_with(
214
- str(temp_python_file), None, ["extra_dep"]
215
  )
216
 
217
  def test_dev_command_with_ui_port(self, temp_python_file):
@@ -468,4 +468,5 @@ class TestRunCommand:
468
  port=None,
469
  log_level=None,
470
  server_args=["--config", "config.json"],
 
471
  )
 
209
  assert result.exit_code == 0
210
  mock_run.assert_called_once()
211
 
212
+ # Check dependencies were passed correctly with no_banner=True
213
  mock_build_uv.assert_called_once_with(
214
+ str(temp_python_file), None, ["extra_dep"], no_banner=True
215
  )
216
 
217
  def test_dev_command_with_ui_port(self, temp_python_file):
 
468
  port=None,
469
  log_level=None,
470
  server_args=["--config", "config.json"],
471
+ show_banner=True,
472
  )