Jeremiah Lowin commited on
Commit
d1d50db
·
1 Parent(s): 7e4a5c7

Add --copy flag for fastmcp version

Browse files
pyproject.toml CHANGED
@@ -13,6 +13,7 @@ dependencies = [
13
  "cyclopts>=3.0.0",
14
  "authlib>=1.5.2",
15
  "pydantic[email]>=2.11.7",
 
16
  ]
17
  requires-python = ">=3.10"
18
  readme = "README.md"
 
13
  "cyclopts>=3.0.0",
14
  "authlib>=1.5.2",
15
  "pydantic[email]>=2.11.7",
16
+ "pyperclip>=1.9.0",
17
  ]
18
  requires-python = ">=3.10"
19
  readme = "README.md"
src/fastmcp/cli/cli.py CHANGED
@@ -10,6 +10,7 @@ from pathlib import Path
10
  from typing import Annotated, Literal
11
 
12
  import cyclopts
 
13
  from pydantic import TypeAdapter
14
  from rich.console import Console
15
  from rich.table import Table
@@ -85,7 +86,17 @@ def _build_uv_command(
85
 
86
 
87
  @app.command
88
- def version():
 
 
 
 
 
 
 
 
 
 
89
  """Display version information and platform details."""
90
  info = {
91
  "FastMCP version": fastmcp.__version__,
@@ -100,7 +111,15 @@ def version():
100
  g.add_column(style="cyan", justify="right")
101
  for k, v in info.items():
102
  g.add_row(k + ":", str(v).replace("\n", " "))
103
- console.print(g)
 
 
 
 
 
 
 
 
104
 
105
  sys.exit(0)
106
 
 
10
  from typing import Annotated, Literal
11
 
12
  import cyclopts
13
+ import pyperclip
14
  from pydantic import TypeAdapter
15
  from rich.console import Console
16
  from rich.table import Table
 
86
 
87
 
88
  @app.command
89
+ def version(
90
+ *,
91
+ copy: Annotated[
92
+ bool,
93
+ cyclopts.Parameter(
94
+ "--copy",
95
+ help="Copy version information to clipboard",
96
+ negative=False,
97
+ ),
98
+ ] = False,
99
+ ):
100
  """Display version information and platform details."""
101
  info = {
102
  "FastMCP version": fastmcp.__version__,
 
111
  g.add_column(style="cyan", justify="right")
112
  for k, v in info.items():
113
  g.add_row(k + ":", str(v).replace("\n", " "))
114
+
115
+ if copy:
116
+ # Use Rich's capture to get text representation
117
+ with console.capture() as capture:
118
+ console.print(g)
119
+ pyperclip.copy(capture.get())
120
+ console.print("[green]✓[/green] Version information copied to clipboard")
121
+ else:
122
+ console.print(g)
123
 
124
  sys.exit(0)
125
 
src/fastmcp/cli/install/mcp_config.py CHANGED
@@ -6,6 +6,7 @@ from pathlib import Path
6
  from typing import Annotated
7
 
8
  import cyclopts
 
9
  from rich import print
10
 
11
  from fastmcp.utilities.logging import get_logger
@@ -79,18 +80,8 @@ def install_mcp_config(
79
 
80
  # Handle output
81
  if copy:
82
- try:
83
- import pyperclip
84
-
85
- pyperclip.copy(json_output)
86
- print(
87
- f"[green]MCP configuration for '{name}' copied to clipboard[/green]"
88
- )
89
- except ImportError:
90
- print(
91
- "[red]The --copy flag requires pyperclip. Please install pyperclip and try again: pip install pyperclip[/red]"
92
- )
93
- return False
94
  else:
95
  # Print to stdout (for piping)
96
  print(json_output)
 
6
  from typing import Annotated
7
 
8
  import cyclopts
9
+ import pyperclip
10
  from rich import print
11
 
12
  from fastmcp.utilities.logging import get_logger
 
80
 
81
  # Handle output
82
  if copy:
83
+ pyperclip.copy(json_output)
84
+ print(f"[green]MCP configuration for '{name}' copied to clipboard[/green]")
 
 
 
 
 
 
 
 
 
 
85
  else:
86
  # Print to stdout (for piping)
87
  print(json_output)
tests/cli/test_cli.py CHANGED
@@ -108,6 +108,43 @@ class TestVersionCommand:
108
  mock_print.assert_called_once()
109
  mock_exit.assert_called_once_with(0)
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  class TestDevCommand:
113
  """Test the dev command."""
 
108
  mock_print.assert_called_once()
109
  mock_exit.assert_called_once_with(0)
110
 
111
+ def test_version_command_parsing(self):
112
+ """Test that the version command parses arguments correctly."""
113
+ command, bound, _ = app.parse_args(["version"])
114
+ assert command.__name__ == "version"
115
+ # Default arguments aren't included in bound.arguments
116
+ assert bound.arguments == {}
117
+
118
+ def test_version_command_with_copy_flag(self):
119
+ """Test that the version command parses --copy flag correctly."""
120
+ command, bound, _ = app.parse_args(["version", "--copy"])
121
+ assert command.__name__ == "version"
122
+ assert bound.arguments == {"copy": True}
123
+
124
+ @patch("fastmcp.cli.cli.sys.exit")
125
+ @patch("fastmcp.cli.cli.pyperclip.copy")
126
+ @patch("fastmcp.cli.cli.console")
127
+ def test_version_command_copy_functionality(
128
+ self, mock_console, mock_pyperclip_copy, mock_exit
129
+ ):
130
+ """Test that the version command copies to clipboard when --copy is used."""
131
+ # Mock console.capture
132
+ mock_capture = Mock()
133
+ mock_capture.get.return_value = "FastMCP version: 1.0.0\nMCP version: 1.10.0"
134
+ mock_console.capture.return_value.__enter__.return_value = mock_capture
135
+ mock_console.capture.return_value.__exit__.return_value = None
136
+
137
+ command, bound, _ = app.parse_args(["version", "--copy"])
138
+ command(**bound.arguments)
139
+
140
+ mock_pyperclip_copy.assert_called_once_with(
141
+ "FastMCP version: 1.0.0\nMCP version: 1.10.0"
142
+ )
143
+ mock_console.print.assert_called_with(
144
+ "[green]✓[/green] Version information copied to clipboard"
145
+ )
146
+ mock_exit.assert_called_once_with(0)
147
+
148
 
149
  class TestDevCommand:
150
  """Test the dev command."""
uv.lock CHANGED
@@ -502,6 +502,7 @@ dependencies = [
502
  { name = "mcp" },
503
  { name = "openapi-pydantic" },
504
  { name = "pydantic", extra = ["email"] },
 
505
  { name = "python-dotenv" },
506
  { name = "rich" },
507
  ]
@@ -544,6 +545,7 @@ requires-dist = [
544
  { name = "mcp", specifier = ">=1.10.0" },
545
  { name = "openapi-pydantic", specifier = ">=0.5.1" },
546
  { name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
 
547
  { name = "python-dotenv", specifier = ">=1.1.0" },
548
  { name = "rich", specifier = ">=13.9.4" },
549
  { name = "websockets", marker = "extra == 'websockets'", specifier = ">=15.0.1" },
 
502
  { name = "mcp" },
503
  { name = "openapi-pydantic" },
504
  { name = "pydantic", extra = ["email"] },
505
+ { name = "pyperclip" },
506
  { name = "python-dotenv" },
507
  { name = "rich" },
508
  ]
 
545
  { name = "mcp", specifier = ">=1.10.0" },
546
  { name = "openapi-pydantic", specifier = ">=0.5.1" },
547
  { name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
548
+ { name = "pyperclip", specifier = ">=1.9.0" },
549
  { name = "python-dotenv", specifier = ">=1.1.0" },
550
  { name = "rich", specifier = ">=13.9.4" },
551
  { name = "websockets", marker = "extra == 'websockets'", specifier = ">=15.0.1" },