Jeremiah Lowin commited on
Commit
cf4a899
·
unverified ·
2 Parent(s): edea86df36b4fa

Merge pull request #1063 from jlowin/copy-flag

Browse files
.github/ISSUE_TEMPLATE/bug.yml CHANGED
@@ -48,9 +48,11 @@ body:
48
  To get this information, run the following command in your terminal and paste the output below:
49
 
50
  ```bash
51
- fastmcp version
52
  ```
53
 
 
 
54
  If there is other information that would be helpful, please include it as well.
55
  render: Text
56
  validations:
 
48
  To get this information, run the following command in your terminal and paste the output below:
49
 
50
  ```bash
51
+ fastmcp version --copy
52
  ```
53
 
54
+ *Note: if you're using FastMCP < 2.10.3, run `fastmcp version` instead.*
55
+
56
  If there is other information that would be helpful, please include it as well.
57
  render: Text
58
  validations:
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
@@ -94,11 +94,6 @@ class TestMainCLI:
94
  class TestVersionCommand:
95
  """Test the version command."""
96
 
97
- def test_version_command_parsing(self):
98
- """Test that version command can be parsed."""
99
- command, bound, _ = app.parse_args(["version"])
100
- assert command is not None
101
-
102
  def test_version_command_execution(self):
103
  """Test that version command executes and exits properly."""
104
  # The version command should exit with code 0 when executed
@@ -108,6 +103,43 @@ class TestVersionCommand:
108
 
109
  assert exc_info.value.code == 0
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  class TestDevCommand:
113
  """Test the dev command."""
 
94
  class TestVersionCommand:
95
  """Test the version command."""
96
 
 
 
 
 
 
97
  def test_version_command_execution(self):
98
  """Test that version command executes and exits properly."""
99
  # The version command should exit with code 0 when executed
 
103
 
104
  assert exc_info.value.code == 0
105
 
106
+ def test_version_command_parsing(self):
107
+ """Test that the version command parses arguments correctly."""
108
+ command, bound, _ = app.parse_args(["version"])
109
+ assert command.__name__ == "version"
110
+ # Default arguments aren't included in bound.arguments
111
+ assert bound.arguments == {}
112
+
113
+ def test_version_command_with_copy_flag(self):
114
+ """Test that the version command parses --copy flag correctly."""
115
+ command, bound, _ = app.parse_args(["version", "--copy"])
116
+ assert command.__name__ == "version"
117
+ assert bound.arguments == {"copy": True}
118
+
119
+ @patch("fastmcp.cli.cli.sys.exit")
120
+ @patch("fastmcp.cli.cli.pyperclip.copy")
121
+ @patch("fastmcp.cli.cli.console")
122
+ def test_version_command_copy_functionality(
123
+ self, mock_console, mock_pyperclip_copy, mock_exit
124
+ ):
125
+ """Test that the version command copies to clipboard when --copy is used."""
126
+ # Mock console.capture
127
+ mock_capture = Mock()
128
+ mock_capture.get.return_value = "FastMCP version: 1.0.0\nMCP version: 1.10.0"
129
+ mock_console.capture.return_value.__enter__.return_value = mock_capture
130
+ mock_console.capture.return_value.__exit__.return_value = None
131
+
132
+ command, bound, _ = app.parse_args(["version", "--copy"])
133
+ command(**bound.arguments)
134
+
135
+ mock_pyperclip_copy.assert_called_once_with(
136
+ "FastMCP version: 1.0.0\nMCP version: 1.10.0"
137
+ )
138
+ mock_console.print.assert_called_with(
139
+ "[green]✓[/green] Version information copied to clipboard"
140
+ )
141
+ mock_exit.assert_called_once_with(0)
142
+
143
 
144
  class TestDevCommand:
145
  """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" },