mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 04:54:17 +02:00
291 lines
8.8 KiB
Python
291 lines
8.8 KiB
Python
"""Tests for the FastMCP CLI."""
|
|
|
|
import json
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from fastmcp.cli.cli import app, _parse_env_var
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_config(tmp_path):
|
|
"""Create a mock Claude config file."""
|
|
config = {"mcpServers": {}}
|
|
config_file = tmp_path / "claude_desktop_config.json"
|
|
config_file.write_text(json.dumps(config))
|
|
return config_file
|
|
|
|
|
|
@pytest.fixture
|
|
def server_file(tmp_path):
|
|
"""Create a server file."""
|
|
server_file = tmp_path / "server.py"
|
|
server_file.write_text(
|
|
"""from fastmcp import FastMCP
|
|
mcp = FastMCP("test")
|
|
"""
|
|
)
|
|
return server_file
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_env_file(tmp_path):
|
|
"""Create a mock .env file."""
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("FOO=bar\nBAZ=123")
|
|
return env_file
|
|
|
|
|
|
def test_parse_env_var():
|
|
"""Test parsing environment variables."""
|
|
assert _parse_env_var("FOO=bar") == ("FOO", "bar")
|
|
assert _parse_env_var("FOO=") == ("FOO", "")
|
|
assert _parse_env_var("FOO=bar baz") == ("FOO", "bar baz")
|
|
assert _parse_env_var("FOO = bar ") == ("FOO", "bar")
|
|
|
|
with pytest.raises(SystemExit):
|
|
_parse_env_var("invalid")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"args,expected_env",
|
|
[
|
|
# Basic env var
|
|
(
|
|
["--env-var", "FOO=bar"],
|
|
{"FOO": "bar"},
|
|
),
|
|
# Multiple env vars
|
|
(
|
|
["--env-var", "FOO=bar", "--env-var", "BAZ=123"],
|
|
{"FOO": "bar", "BAZ": "123"},
|
|
),
|
|
# Env var with spaces
|
|
(
|
|
["--env-var", "FOO=bar baz"],
|
|
{"FOO": "bar baz"},
|
|
),
|
|
],
|
|
)
|
|
def test_install_with_env_vars(mock_config, server_file, args, expected_env):
|
|
"""Test installing with environment variables."""
|
|
runner = CliRunner()
|
|
|
|
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
mock_config_path.return_value = mock_config.parent
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["install", str(server_file)] + args,
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
|
|
# Read the config file and check env vars
|
|
config = json.loads(mock_config.read_text())
|
|
assert "mcpServers" in config
|
|
assert len(config["mcpServers"]) == 1
|
|
server = next(iter(config["mcpServers"].values()))
|
|
assert server["env"] == expected_env
|
|
|
|
|
|
def test_install_with_env_file(mock_config, server_file, mock_env_file):
|
|
"""Test installing with environment variables from a file."""
|
|
runner = CliRunner()
|
|
|
|
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
mock_config_path.return_value = mock_config.parent
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["install", str(server_file), "--env-file", str(mock_env_file)],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
|
|
# Read the config file and check env vars
|
|
config = json.loads(mock_config.read_text())
|
|
assert "mcpServers" in config
|
|
assert len(config["mcpServers"]) == 1
|
|
server = next(iter(config["mcpServers"].values()))
|
|
assert server["env"] == {"FOO": "bar", "BAZ": "123"}
|
|
|
|
|
|
def test_install_preserves_existing_env_vars(mock_config, server_file):
|
|
"""Test that installing preserves existing environment variables."""
|
|
# Set up initial config with env vars
|
|
config = {
|
|
"mcpServers": {
|
|
"test": {
|
|
"command": "uv",
|
|
"args": [
|
|
"run",
|
|
"--with",
|
|
"fastmcp",
|
|
"fastmcp",
|
|
"run",
|
|
str(server_file),
|
|
],
|
|
"env": {"FOO": "bar", "BAZ": "123"},
|
|
}
|
|
}
|
|
}
|
|
mock_config.write_text(json.dumps(config))
|
|
|
|
runner = CliRunner()
|
|
|
|
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
mock_config_path.return_value = mock_config.parent
|
|
|
|
# Install with a new env var
|
|
result = runner.invoke(
|
|
app,
|
|
["install", str(server_file), "--env-var", "NEW=value"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
|
|
# Read the config file and check env vars are preserved
|
|
config = json.loads(mock_config.read_text())
|
|
server = next(iter(config["mcpServers"].values()))
|
|
assert server["env"] == {"FOO": "bar", "BAZ": "123", "NEW": "value"}
|
|
|
|
|
|
def test_install_updates_existing_env_vars(mock_config, server_file):
|
|
"""Test that installing updates existing environment variables."""
|
|
# Set up initial config with env vars
|
|
config = {
|
|
"mcpServers": {
|
|
"test": {
|
|
"command": "uv",
|
|
"args": [
|
|
"run",
|
|
"--with",
|
|
"fastmcp",
|
|
"fastmcp",
|
|
"run",
|
|
str(server_file),
|
|
],
|
|
"env": {"FOO": "bar", "BAZ": "123"},
|
|
}
|
|
}
|
|
}
|
|
mock_config.write_text(json.dumps(config))
|
|
|
|
runner = CliRunner()
|
|
|
|
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
mock_config_path.return_value = mock_config.parent
|
|
|
|
# Update an existing env var
|
|
result = runner.invoke(
|
|
app,
|
|
["install", str(server_file), "--env-var", "FOO=newvalue"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
|
|
# Read the config file and check env var was updated
|
|
config = json.loads(mock_config.read_text())
|
|
server = next(iter(config["mcpServers"].values()))
|
|
assert server["env"] == {"FOO": "newvalue", "BAZ": "123"}
|
|
|
|
|
|
def test_server_dependencies(mock_config, server_file):
|
|
"""Test that server dependencies are correctly handled."""
|
|
# Create a server file with dependencies
|
|
server_file = server_file.parent / "server_with_deps.py"
|
|
server_file.write_text(
|
|
"""from fastmcp import FastMCP
|
|
mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
|
"""
|
|
)
|
|
|
|
runner = CliRunner()
|
|
|
|
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
mock_config_path.return_value = mock_config.parent
|
|
|
|
result = runner.invoke(app, ["install", str(server_file)])
|
|
|
|
assert result.exit_code == 0
|
|
|
|
# Read the config file and check dependencies were added as --with args
|
|
config = json.loads(mock_config.read_text())
|
|
server = next(iter(config["mcpServers"].values()))
|
|
assert "--with" in server["args"]
|
|
assert "pandas" in server["args"]
|
|
assert "numpy" in server["args"]
|
|
|
|
|
|
def test_server_dependencies_empty(mock_config, server_file):
|
|
"""Test that server with no dependencies works correctly."""
|
|
runner = CliRunner()
|
|
|
|
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
mock_config_path.return_value = mock_config.parent
|
|
|
|
result = runner.invoke(app, ["install", str(server_file)])
|
|
|
|
assert result.exit_code == 0
|
|
|
|
# Read the config file and check only fastmcp is in --with args
|
|
config = json.loads(mock_config.read_text())
|
|
server = next(iter(config["mcpServers"].values()))
|
|
assert server["args"].count("--with") == 1
|
|
assert "fastmcp" in server["args"]
|
|
|
|
|
|
def test_dev_with_dependencies(mock_config, server_file):
|
|
"""Test that dev command handles dependencies correctly."""
|
|
# Create a server file with dependencies
|
|
server_file = server_file.parent / "server_with_deps.py"
|
|
server_file.write_text(
|
|
"""from fastmcp import FastMCP
|
|
mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
|
"""
|
|
)
|
|
|
|
runner = CliRunner()
|
|
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value.returncode = 0 # Set successful return code
|
|
result = runner.invoke(app, ["dev", str(server_file)])
|
|
assert result.exit_code == 0
|
|
|
|
# Check that dependencies were passed to subprocess.run
|
|
mock_run.assert_called_once()
|
|
args = mock_run.call_args[0][0]
|
|
assert "npx" in args
|
|
assert "@modelcontextprotocol/inspector" in args
|
|
assert "uv" in args
|
|
assert "run" in args
|
|
assert "--with" in args
|
|
assert "pandas" in args
|
|
assert "numpy" in args
|
|
assert "fastmcp" in args
|
|
|
|
|
|
def test_run_with_dependencies(mock_config, server_file):
|
|
"""Test that run command does not handle dependencies."""
|
|
# Create a server file with dependencies
|
|
server_file = server_file.parent / "server_with_deps.py"
|
|
server_file.write_text(
|
|
"""from fastmcp import FastMCP
|
|
mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
|
|
|
if __name__ == "__main__":
|
|
mcp.run()
|
|
"""
|
|
)
|
|
|
|
runner = CliRunner()
|
|
|
|
with patch("subprocess.run") as mock_run:
|
|
result = runner.invoke(app, ["run", str(server_file)])
|
|
assert result.exit_code == 0
|
|
|
|
# Run command should not call subprocess.run
|
|
mock_run.assert_not_called()
|