Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
d1d50db
1
Parent(s): 7e4a5c7
Add --copy flag for fastmcp version
Browse files- pyproject.toml +1 -0
- src/fastmcp/cli/cli.py +21 -2
- src/fastmcp/cli/install/mcp_config.py +3 -12
- tests/cli/test_cli.py +37 -0
- uv.lock +2 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 83 |
-
|
| 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" },
|