mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-28 02:10:38 +02:00
Consolidate server loading logic into FileSystemSource (#1614)
This commit is contained in:
parent
85cc51f223
commit
9c5c4f5113
20 changed files with 1117 additions and 520 deletions
|
|
@ -328,6 +328,47 @@ class TestRunCommand:
|
|||
]
|
||||
)
|
||||
|
||||
def test_run_command_parsing_skip_env_flag(self):
|
||||
"""Test run command parsing with --skip-env flag."""
|
||||
command, bound, _ = app.parse_args(
|
||||
[
|
||||
"run",
|
||||
"server.py",
|
||||
"--skip-env",
|
||||
]
|
||||
)
|
||||
assert command is not None
|
||||
assert bound.arguments["server_spec"] == "server.py"
|
||||
assert bound.arguments["skip_env"] is True
|
||||
|
||||
def test_run_command_parsing_skip_source_flag(self):
|
||||
"""Test run command parsing with --skip-source flag."""
|
||||
command, bound, _ = app.parse_args(
|
||||
[
|
||||
"run",
|
||||
"server.py",
|
||||
"--skip-source",
|
||||
]
|
||||
)
|
||||
assert command is not None
|
||||
assert bound.arguments["server_spec"] == "server.py"
|
||||
assert bound.arguments["skip_source"] is True
|
||||
|
||||
def test_run_command_parsing_both_skip_flags(self):
|
||||
"""Test run command parsing with both --skip-env and --skip-source flags."""
|
||||
command, bound, _ = app.parse_args(
|
||||
[
|
||||
"run",
|
||||
"server.py",
|
||||
"--skip-env",
|
||||
"--skip-source",
|
||||
]
|
||||
)
|
||||
assert command is not None
|
||||
assert bound.arguments["server_spec"] == "server.py"
|
||||
assert bound.arguments["skip_env"] is True
|
||||
assert bound.arguments["skip_source"] is True
|
||||
|
||||
|
||||
class TestWindowsSpecific:
|
||||
"""Test Windows-specific functionality."""
|
||||
|
|
@ -413,22 +454,27 @@ class TestWindowsSpecific:
|
|||
|
||||
def test_windows_path_parsing_with_colon(self, tmp_path):
|
||||
"""Test parsing Windows paths with drive letters and colons."""
|
||||
from fastmcp.cli.run import parse_file_path
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import (
|
||||
FileSystemSource,
|
||||
)
|
||||
|
||||
# Create a real test file to test the logic
|
||||
test_file = tmp_path / "server.py"
|
||||
test_file.write_text("# test server")
|
||||
|
||||
# Test normal file parsing (works on all platforms)
|
||||
file_path, obj = parse_file_path(str(test_file))
|
||||
assert obj is None
|
||||
source = FileSystemSource(path=str(test_file))
|
||||
assert source.entrypoint is None
|
||||
assert Path(source.path).resolve() == test_file.resolve()
|
||||
|
||||
# Test file:object parsing
|
||||
file_path, obj = parse_file_path(f"{test_file}:myapp")
|
||||
assert obj == "myapp"
|
||||
source = FileSystemSource(path=f"{test_file}:myapp")
|
||||
assert source.entrypoint == "myapp"
|
||||
|
||||
# Test that the file portion resolves correctly when object is specified
|
||||
assert file_path == test_file.resolve()
|
||||
assert Path(source.path).resolve() == test_file.resolve()
|
||||
|
||||
|
||||
class TestInspectCommand:
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ from fastmcp.utilities.fastmcp_config import (
|
|||
Deployment,
|
||||
Environment,
|
||||
FastMCPConfig,
|
||||
FileSystemSource,
|
||||
)
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
|
||||
class TestFileSystemSource:
|
||||
|
|
|
|||
|
|
@ -280,7 +280,11 @@ class TestInstallCursor:
|
|||
# Verify failure message was printed
|
||||
mock_print.assert_called()
|
||||
|
||||
def test_install_cursor_deduplicate_packages(self):
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None, # Mock to disable dev mode so "fastmcp" count is predictable
|
||||
)
|
||||
def test_install_cursor_deduplicate_packages(self, mock_find_dev):
|
||||
"""Test that duplicate packages are deduplicated."""
|
||||
with patch("fastmcp.cli.install.cursor.open_deeplink") as mock_open:
|
||||
mock_open.return_value = True
|
||||
|
|
|
|||
|
|
@ -7,14 +7,13 @@ from pydantic import ValidationError
|
|||
|
||||
from fastmcp.cli.run import (
|
||||
create_mcp_config_server,
|
||||
import_server,
|
||||
is_url,
|
||||
parse_file_path,
|
||||
)
|
||||
from fastmcp.client.client import Client
|
||||
from fastmcp.client.transports import FastMCPTransport
|
||||
from fastmcp.mcp_config import MCPConfig, StdioMCPServer
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
|
||||
class TestUrlDetection:
|
||||
|
|
@ -41,52 +40,54 @@ class TestUrlDetection:
|
|||
assert not is_url("file:///path/to/file")
|
||||
|
||||
|
||||
class TestFilePathParsing:
|
||||
"""Test file path parsing functionality."""
|
||||
class TestFileSystemSource:
|
||||
"""Test FileSystemSource path parsing functionality."""
|
||||
|
||||
def test_parse_file_path_simple(self, tmp_path):
|
||||
def test_parse_simple_path(self, tmp_path):
|
||||
"""Test parsing simple file path without object."""
|
||||
test_file = tmp_path / "server.py"
|
||||
test_file.write_text("# test server")
|
||||
|
||||
file_path, server_object = parse_file_path(str(test_file))
|
||||
assert file_path == test_file.resolve()
|
||||
assert server_object is None
|
||||
source = FileSystemSource(path=str(test_file))
|
||||
assert Path(source.path).resolve() == test_file.resolve()
|
||||
assert source.entrypoint is None
|
||||
|
||||
def test_parse_file_path_with_object(self, tmp_path):
|
||||
def test_parse_path_with_object(self, tmp_path):
|
||||
"""Test parsing file path with object specification."""
|
||||
test_file = tmp_path / "server.py"
|
||||
test_file.write_text("# test server")
|
||||
|
||||
file_path, server_object = parse_file_path(f"{test_file}:app")
|
||||
assert file_path == test_file.resolve()
|
||||
assert server_object == "app"
|
||||
source = FileSystemSource(path=f"{test_file}:app")
|
||||
assert Path(source.path).resolve() == test_file.resolve()
|
||||
assert source.entrypoint == "app"
|
||||
|
||||
def test_parse_file_path_complex_object(self, tmp_path):
|
||||
def test_parse_complex_object(self, tmp_path):
|
||||
"""Test parsing file path with complex object specification."""
|
||||
test_file = tmp_path / "server.py"
|
||||
test_file.write_text("# test server")
|
||||
|
||||
# The current implementation splits on the last colon, so file:module:app
|
||||
# becomes file_path="file:module" and server_object="app"
|
||||
# The implementation splits on the last colon, so file:module:app
|
||||
# becomes file_path="file:module" and entrypoint="app"
|
||||
# We need to create a file with a colon in the name for this test
|
||||
complex_file = tmp_path / "server:module.py"
|
||||
complex_file.write_text("# test server")
|
||||
|
||||
file_path, server_object = parse_file_path(f"{complex_file}:app")
|
||||
assert file_path == complex_file.resolve()
|
||||
assert server_object == "app"
|
||||
source = FileSystemSource(path=f"{complex_file}:app")
|
||||
assert Path(source.path).resolve() == complex_file.resolve()
|
||||
assert source.entrypoint == "app"
|
||||
|
||||
def test_parse_file_path_nonexistent(self):
|
||||
"""Test parsing nonexistent file path exits."""
|
||||
async def test_load_server_nonexistent(self):
|
||||
"""Test loading nonexistent file path exits."""
|
||||
source = FileSystemSource(path="nonexistent.py")
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
parse_file_path("nonexistent.py")
|
||||
await source.load_server()
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
def test_parse_file_path_directory(self, tmp_path):
|
||||
"""Test parsing directory path exits."""
|
||||
async def test_load_server_directory(self, tmp_path):
|
||||
"""Test loading directory path exits."""
|
||||
source = FileSystemSource(path=str(tmp_path))
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
parse_file_path(str(tmp_path))
|
||||
await source.load_server()
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
|
||||
|
|
@ -157,7 +158,8 @@ def greet(name: str) -> str:
|
|||
return f"Hello, {name}!"
|
||||
""")
|
||||
|
||||
server = await import_server(test_file)
|
||||
source = FileSystemSource(path=str(test_file))
|
||||
server = await source.load_server()
|
||||
assert server.name == "TestServer"
|
||||
tools = await server.get_tools()
|
||||
assert "greet" in tools
|
||||
|
|
@ -178,7 +180,8 @@ if __name__ == "__main__":
|
|||
app.run()
|
||||
""")
|
||||
|
||||
server = await import_server(test_file)
|
||||
source = FileSystemSource(path=str(test_file))
|
||||
server = await source.load_server()
|
||||
assert server.name == "MainServer"
|
||||
tools = await server.get_tools()
|
||||
assert "calculate" in tools
|
||||
|
|
@ -192,7 +195,8 @@ import fastmcp
|
|||
mcp = fastmcp.FastMCP("MCPServer")
|
||||
""")
|
||||
|
||||
server = await import_server(mcp_file)
|
||||
source = FileSystemSource(path=str(mcp_file))
|
||||
server = await source.load_server()
|
||||
assert server.name == "MCPServer"
|
||||
|
||||
# Test with 'server' name
|
||||
|
|
@ -202,7 +206,8 @@ import fastmcp
|
|||
server = fastmcp.FastMCP("ServerServer")
|
||||
""")
|
||||
|
||||
server = await import_server(server_file)
|
||||
source = FileSystemSource(path=str(server_file))
|
||||
server = await source.load_server()
|
||||
assert server.name == "ServerServer"
|
||||
|
||||
# Test with 'app' name
|
||||
|
|
@ -212,7 +217,8 @@ import fastmcp
|
|||
app = fastmcp.FastMCP("AppServer")
|
||||
""")
|
||||
|
||||
server = await import_server(app_file)
|
||||
source = FileSystemSource(path=str(app_file))
|
||||
server = await source.load_server()
|
||||
assert server.name == "AppServer"
|
||||
|
||||
async def test_import_server_nonstandard_name(self, tmp_path):
|
||||
|
|
@ -228,7 +234,8 @@ def custom_tool() -> str:
|
|||
return "custom"
|
||||
""")
|
||||
|
||||
server = await import_server(test_file, "my_custom_server")
|
||||
source = FileSystemSource(path=f"{test_file}:my_custom_server")
|
||||
server = await source.load_server()
|
||||
assert server.name == "CustomServer"
|
||||
tools = await server.get_tools()
|
||||
assert "custom_tool" in tools
|
||||
|
|
@ -242,8 +249,9 @@ import fastmcp
|
|||
other_name = fastmcp.FastMCP("OtherServer")
|
||||
""")
|
||||
|
||||
source = FileSystemSource(path=str(test_file))
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
await import_server(test_file)
|
||||
await source.load_server()
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
async def test_import_server_nonexistent_object_fails(self, tmp_path):
|
||||
|
|
@ -255,6 +263,131 @@ import fastmcp
|
|||
mcp = fastmcp.FastMCP("TestServer")
|
||||
""")
|
||||
|
||||
source = FileSystemSource(path=f"{test_file}:nonexistent")
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
await import_server(test_file, "nonexistent")
|
||||
await source.load_server()
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
|
||||
class TestSkipSource:
|
||||
"""Test the --skip-source functionality."""
|
||||
|
||||
async def test_run_command_calls_prepare_by_default(self, tmp_path):
|
||||
"""Test that run_command calls source.prepare() by default."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
|
||||
# Create a test server file
|
||||
test_file = tmp_path / "server.py"
|
||||
test_file.write_text("""
|
||||
import fastmcp
|
||||
mcp = fastmcp.FastMCP("TestServer")
|
||||
""")
|
||||
|
||||
# Create a test config file
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_data = {"source": {"path": str(test_file), "entrypoint": "mcp"}}
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Mock the prepare method and server run
|
||||
with (
|
||||
patch.object(
|
||||
FileSystemSource, "prepare", new_callable=AsyncMock
|
||||
) as prepare_mock,
|
||||
patch("fastmcp.server.server.FastMCP.run_async", new_callable=AsyncMock),
|
||||
):
|
||||
# Run the command
|
||||
await run_command(str(config_file))
|
||||
|
||||
# Verify prepare was called
|
||||
prepare_mock.assert_called_once()
|
||||
|
||||
async def test_run_command_skips_prepare_with_flag(self, tmp_path):
|
||||
"""Test that run_command skips source.prepare() when skip_source=True."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
|
||||
# Create a test server file
|
||||
test_file = tmp_path / "server.py"
|
||||
test_file.write_text("""
|
||||
import fastmcp
|
||||
mcp = fastmcp.FastMCP("TestServer")
|
||||
""")
|
||||
|
||||
# Create a test config file
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_data = {"source": {"path": str(test_file), "entrypoint": "mcp"}}
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Mock the prepare method and server run
|
||||
with (
|
||||
patch.object(
|
||||
FileSystemSource, "prepare", new_callable=AsyncMock
|
||||
) as prepare_mock,
|
||||
patch("fastmcp.server.server.FastMCP.run_async", new_callable=AsyncMock),
|
||||
):
|
||||
# Run the command with skip_source=True
|
||||
await run_command(str(config_file), skip_source=True)
|
||||
|
||||
# Verify prepare was NOT called
|
||||
prepare_mock.assert_not_called()
|
||||
|
||||
async def test_filesystem_source_prepare_by_default(self, tmp_path):
|
||||
"""Test that FileSystemSource is prepared when using direct file spec."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import (
|
||||
FileSystemSource,
|
||||
)
|
||||
|
||||
# Create a test server file
|
||||
test_file = tmp_path / "server.py"
|
||||
test_file.write_text("""
|
||||
import fastmcp
|
||||
mcp = fastmcp.FastMCP("TestServer")
|
||||
""")
|
||||
|
||||
# Mock the prepare method and server run
|
||||
with (
|
||||
patch.object(
|
||||
FileSystemSource, "prepare", new_callable=AsyncMock
|
||||
) as prepare_mock,
|
||||
patch("fastmcp.server.server.FastMCP.run_async", new_callable=AsyncMock),
|
||||
):
|
||||
# Run with direct file specification
|
||||
await run_command(str(test_file))
|
||||
|
||||
# Verify prepare was called
|
||||
prepare_mock.assert_called_once()
|
||||
|
||||
async def test_filesystem_source_skip_prepare_with_flag(self, tmp_path):
|
||||
"""Test that FileSystemSource.prepare() is skipped with skip_source flag."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import (
|
||||
FileSystemSource,
|
||||
)
|
||||
|
||||
# Create a test server file
|
||||
test_file = tmp_path / "server.py"
|
||||
test_file.write_text("""
|
||||
import fastmcp
|
||||
mcp = fastmcp.FastMCP("TestServer")
|
||||
""")
|
||||
|
||||
# Mock the prepare method and server run
|
||||
with (
|
||||
patch.object(
|
||||
FileSystemSource, "prepare", new_callable=AsyncMock
|
||||
) as prepare_mock,
|
||||
patch("fastmcp.server.server.FastMCP.run_async", new_callable=AsyncMock),
|
||||
):
|
||||
# Run with direct file specification and skip_source=True
|
||||
await run_command(str(test_file), skip_source=True)
|
||||
|
||||
# Verify prepare was NOT called
|
||||
prepare_mock.assert_not_called()
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ from fastmcp.utilities.fastmcp_config import (
|
|||
Deployment,
|
||||
Environment,
|
||||
FastMCPConfig,
|
||||
FileSystemSource,
|
||||
)
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@ from fastmcp.cli.run import run_with_uv
|
|||
class TestRunWithUv:
|
||||
"""Test the run_with_uv function."""
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None,
|
||||
)
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_basic(self, mock_run):
|
||||
def test_run_with_uv_basic(self, mock_run, mock_find_dev_path):
|
||||
"""Test basic run_with_uv execution."""
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
|
||||
|
|
@ -38,8 +42,12 @@ class TestRunWithUv:
|
|||
]
|
||||
assert cmd == expected
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None,
|
||||
)
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_python_version(self, mock_run):
|
||||
def test_run_with_uv_python_version(self, mock_run, mock_find_dev_path):
|
||||
"""Test run_with_uv with Python version."""
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
|
||||
|
|
@ -63,8 +71,12 @@ class TestRunWithUv:
|
|||
]
|
||||
assert cmd == expected
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None,
|
||||
)
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_project(self, mock_run):
|
||||
def test_run_with_uv_project(self, mock_run, mock_find_dev_path):
|
||||
"""Test run_with_uv with project directory."""
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
# Use an absolute path that works on all platforms
|
||||
|
|
@ -90,8 +102,12 @@ class TestRunWithUv:
|
|||
"--skip-env",
|
||||
]
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None,
|
||||
)
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_with_packages(self, mock_run):
|
||||
def test_run_with_uv_with_packages(self, mock_run, mock_find_dev_path):
|
||||
"""Test run_with_uv with additional packages."""
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
|
||||
|
|
@ -117,8 +133,12 @@ class TestRunWithUv:
|
|||
]
|
||||
assert cmd == expected
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None,
|
||||
)
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_with_requirements(self, mock_run):
|
||||
def test_run_with_uv_with_requirements(self, mock_run, mock_find_dev_path):
|
||||
"""Test run_with_uv with requirements file."""
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
req_path = Path("requirements.txt")
|
||||
|
|
@ -143,8 +163,12 @@ class TestRunWithUv:
|
|||
]
|
||||
assert cmd == expected
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None,
|
||||
)
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_transport_options(self, mock_run):
|
||||
def test_run_with_uv_transport_options(self, mock_run, mock_find_dev_path):
|
||||
"""Test run_with_uv with transport-related options."""
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
|
||||
|
|
@ -185,8 +209,12 @@ class TestRunWithUv:
|
|||
]
|
||||
assert cmd == expected
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None,
|
||||
)
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_all_options(self, mock_run):
|
||||
def test_run_with_uv_all_options(self, mock_run, mock_find_dev_path):
|
||||
"""Test run_with_uv with all options combined."""
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
|
||||
|
|
@ -230,8 +258,12 @@ class TestRunWithUv:
|
|||
"--no-banner",
|
||||
]
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None,
|
||||
)
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_error_handling(self, mock_run):
|
||||
def test_run_with_uv_error_handling(self, mock_run, mock_find_dev_path):
|
||||
"""Test run_with_uv error handling."""
|
||||
mock_run.side_effect = subprocess.CalledProcessError(1, ["uv", "run"])
|
||||
|
||||
|
|
@ -240,9 +272,13 @@ class TestRunWithUv:
|
|||
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
|
||||
return_value=None,
|
||||
)
|
||||
@patch("fastmcp.cli.run.logger")
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_logging(self, mock_run, mock_logger):
|
||||
def test_run_with_uv_logging(self, mock_run, mock_logger, mock_find_dev_path):
|
||||
"""Test that run_with_uv logs the command."""
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
|
||||
|
|
|
|||
139
tests/cli/test_server_args.py
Normal file
139
tests/cli/test_server_args.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""Test server argument passing functionality."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
|
||||
class TestServerArguments:
|
||||
"""Test passing arguments to servers."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_with_argparse(self, tmp_path):
|
||||
"""Test a server that uses argparse with command line arguments."""
|
||||
server_file = tmp_path / "argparse_server.py"
|
||||
server_file.write_text("""
|
||||
import argparse
|
||||
from fastmcp import FastMCP
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--name", default="DefaultServer")
|
||||
parser.add_argument("--port", type=int, default=8000)
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
server_name = f"{args.name}:{args.port}"
|
||||
if args.debug:
|
||||
server_name += " (Debug)"
|
||||
|
||||
mcp = FastMCP(server_name)
|
||||
|
||||
@mcp.tool
|
||||
def get_config() -> dict:
|
||||
return {"name": args.name, "port": args.port, "debug": args.debug}
|
||||
""")
|
||||
|
||||
# Test with arguments
|
||||
source = FileSystemSource(path=str(server_file))
|
||||
config = FastMCPConfig(source=source)
|
||||
|
||||
from fastmcp.cli.cli import with_argv
|
||||
|
||||
# Simulate passing arguments
|
||||
with with_argv(["--name", "TestServer", "--port", "9000", "--debug"]):
|
||||
server = await config.source.load_server()
|
||||
|
||||
assert server.name == "TestServer:9000 (Debug)"
|
||||
|
||||
# Test the tool works and can access the parsed args
|
||||
tools = await server.get_tools()
|
||||
assert "get_config" in tools
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_with_no_args(self, tmp_path):
|
||||
"""Test a server that uses argparse with no arguments (defaults)."""
|
||||
server_file = tmp_path / "default_server.py"
|
||||
server_file.write_text("""
|
||||
import argparse
|
||||
from fastmcp import FastMCP
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--name", default="DefaultName")
|
||||
args = parser.parse_args()
|
||||
|
||||
mcp = FastMCP(args.name)
|
||||
""")
|
||||
|
||||
source = FileSystemSource(path=str(server_file))
|
||||
config = FastMCPConfig(source=source)
|
||||
|
||||
from fastmcp.cli.cli import with_argv
|
||||
|
||||
# Test with empty args list (should use defaults)
|
||||
with with_argv([]):
|
||||
server = await config.source.load_server()
|
||||
|
||||
assert server.name == "DefaultName"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_with_sys_argv_access(self, tmp_path):
|
||||
"""Test a server that directly accesses sys.argv."""
|
||||
server_file = tmp_path / "sysargv_server.py"
|
||||
server_file.write_text("""
|
||||
import sys
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Direct sys.argv access (less common but should work)
|
||||
name = "DirectServer"
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--custom":
|
||||
name = "CustomServer"
|
||||
|
||||
mcp = FastMCP(name)
|
||||
""")
|
||||
|
||||
source = FileSystemSource(path=str(server_file))
|
||||
config = FastMCPConfig(source=source)
|
||||
|
||||
from fastmcp.cli.cli import with_argv
|
||||
|
||||
# Test with custom argument
|
||||
with with_argv(["--custom"]):
|
||||
server = await config.source.load_server()
|
||||
|
||||
assert server.name == "CustomServer"
|
||||
|
||||
# Test without argument
|
||||
with with_argv([]):
|
||||
server = await config.source.load_server()
|
||||
|
||||
assert server.name == "DirectServer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_server_example(self):
|
||||
"""Test the actual config_server.py example."""
|
||||
# Find the examples directory
|
||||
examples_dir = Path(__file__).parent.parent.parent / "examples"
|
||||
config_server = examples_dir / "config_server.py"
|
||||
|
||||
if not config_server.exists():
|
||||
pytest.skip("config_server.py example not found")
|
||||
|
||||
source = FileSystemSource(path=str(config_server))
|
||||
config = FastMCPConfig(source=source)
|
||||
|
||||
from fastmcp.cli.cli import with_argv
|
||||
|
||||
# Test with debug flag
|
||||
with with_argv(["--name", "TestExample", "--debug"]):
|
||||
server = await config.source.load_server()
|
||||
|
||||
assert server.name == "TestExample (Debug)"
|
||||
|
||||
# Verify tools are available
|
||||
tools = await server.get_tools()
|
||||
assert "get_status" in tools
|
||||
assert "echo_message" in tools
|
||||
91
tests/cli/test_with_argv.py
Normal file
91
tests/cli/test_with_argv.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"""Test the with_argv context manager."""
|
||||
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp.cli.cli import with_argv
|
||||
|
||||
|
||||
class TestWithArgv:
|
||||
"""Test the with_argv context manager."""
|
||||
|
||||
def test_with_argv_replaces_args(self):
|
||||
"""Test that with_argv properly replaces sys.argv."""
|
||||
original_argv = sys.argv[:]
|
||||
test_args = ["--name", "TestServer", "--debug"]
|
||||
|
||||
with with_argv(test_args):
|
||||
# Should preserve script name and add new args
|
||||
assert sys.argv[0] == original_argv[0]
|
||||
assert sys.argv[1:] == test_args
|
||||
|
||||
# Should restore original argv after context
|
||||
assert sys.argv == original_argv
|
||||
|
||||
def test_with_argv_none_does_nothing(self):
|
||||
"""Test that with_argv(None) doesn't change sys.argv."""
|
||||
original_argv = sys.argv[:]
|
||||
|
||||
with with_argv(None):
|
||||
assert sys.argv == original_argv
|
||||
|
||||
assert sys.argv == original_argv
|
||||
|
||||
def test_with_argv_empty_list(self):
|
||||
"""Test that with_argv([]) clears arguments but keeps script name."""
|
||||
original_argv = sys.argv[:]
|
||||
|
||||
with with_argv([]):
|
||||
# Should have only the script name (no additional args)
|
||||
assert sys.argv == [original_argv[0]]
|
||||
assert len(sys.argv) == 1
|
||||
|
||||
assert sys.argv == original_argv
|
||||
|
||||
def test_with_argv_restores_on_exception(self):
|
||||
"""Test that sys.argv is restored even if an exception occurs."""
|
||||
original_argv = sys.argv[:]
|
||||
test_args = ["--error"]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with with_argv(test_args):
|
||||
assert sys.argv == [original_argv[0]] + test_args
|
||||
raise ValueError("Test error")
|
||||
|
||||
# Should still restore original argv
|
||||
assert sys.argv == original_argv
|
||||
|
||||
def test_with_argv_nested(self):
|
||||
"""Test nested with_argv contexts."""
|
||||
original_argv = sys.argv[:]
|
||||
args1 = ["--level1"]
|
||||
args2 = ["--level2", "--debug"]
|
||||
|
||||
with with_argv(args1):
|
||||
assert sys.argv == [original_argv[0]] + args1
|
||||
|
||||
with with_argv(args2):
|
||||
assert sys.argv == [original_argv[0]] + args2
|
||||
|
||||
# Should restore to level 1
|
||||
assert sys.argv == [original_argv[0]] + args1
|
||||
|
||||
# Should restore to original
|
||||
assert sys.argv == original_argv
|
||||
|
||||
@patch("sys.argv", ["test_script.py", "existing", "args"])
|
||||
def test_with_argv_with_existing_args(self):
|
||||
"""Test with_argv when sys.argv already has arguments."""
|
||||
original_argv = sys.argv[:]
|
||||
assert original_argv == ["test_script.py", "existing", "args"]
|
||||
|
||||
test_args = ["--new", "args"]
|
||||
|
||||
with with_argv(test_args):
|
||||
# Should replace existing args but keep script name
|
||||
assert sys.argv == ["test_script.py", "--new", "args"]
|
||||
|
||||
# Should restore original
|
||||
assert sys.argv == original_argv
|
||||
Loading…
Add table
Add a link
Reference in a new issue