Jeremiah Lowin commited on
Commit
ca11c0a
·
1 Parent(s): 98262e8

Add tests

Browse files
tests/cli/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """CLI test package."""
tests/cli/test_cli.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the main CLI functionality."""
2
+
3
+ from pathlib import Path
4
+ from unittest.mock import Mock, patch
5
+
6
+ import pytest
7
+
8
+ from fastmcp.cli.cli import _build_uv_command, _parse_env_var, app
9
+
10
+
11
+ class TestMainCLI:
12
+ """Test the main CLI application."""
13
+
14
+ def test_app_exists(self):
15
+ """Test that the main app is properly configured."""
16
+ # app.name is a tuple in cyclopts
17
+ assert "fastmcp" in app.name
18
+ assert "FastMCP 2.0" in app.help
19
+ # Just check that version exists, not the specific value
20
+ assert hasattr(app, "version")
21
+
22
+ def test_parse_env_var_valid(self):
23
+ """Test parsing valid environment variables."""
24
+ key, value = _parse_env_var("KEY=value")
25
+ assert key == "KEY"
26
+ assert value == "value"
27
+
28
+ key, value = _parse_env_var("COMPLEX_KEY=complex=value=with=equals")
29
+ assert key == "COMPLEX_KEY"
30
+ assert value == "complex=value=with=equals"
31
+
32
+ def test_parse_env_var_invalid(self):
33
+ """Test parsing invalid environment variables exits."""
34
+ with pytest.raises(SystemExit) as exc_info:
35
+ _parse_env_var("INVALID_FORMAT")
36
+ assert exc_info.value.code == 1
37
+
38
+ def test_build_uv_command_basic(self):
39
+ """Test building basic uv command."""
40
+ cmd = _build_uv_command("server.py")
41
+ expected = ["uv", "run", "--with", "fastmcp", "fastmcp", "run", "server.py"]
42
+ assert cmd == expected
43
+
44
+ def test_build_uv_command_with_editable(self):
45
+ """Test building uv command with editable package."""
46
+ editable_path = Path("/path/to/package")
47
+ cmd = _build_uv_command("server.py", with_editable=editable_path)
48
+ expected = [
49
+ "uv",
50
+ "run",
51
+ "--with",
52
+ "fastmcp",
53
+ "--with-editable",
54
+ "/path/to/package",
55
+ "fastmcp",
56
+ "run",
57
+ "server.py",
58
+ ]
59
+ assert cmd == expected
60
+
61
+ def test_build_uv_command_with_packages(self):
62
+ """Test building uv command with additional packages."""
63
+ cmd = _build_uv_command("server.py", with_packages=["pkg1", "pkg2"])
64
+ expected = [
65
+ "uv",
66
+ "run",
67
+ "--with",
68
+ "fastmcp",
69
+ "--with",
70
+ "pkg1",
71
+ "--with",
72
+ "pkg2",
73
+ "fastmcp",
74
+ "run",
75
+ "server.py",
76
+ ]
77
+ assert cmd == expected
78
+
79
+ def test_build_uv_command_no_banner(self):
80
+ """Test building uv command with no banner flag."""
81
+ cmd = _build_uv_command("server.py", no_banner=True)
82
+ expected = [
83
+ "uv",
84
+ "run",
85
+ "--with",
86
+ "fastmcp",
87
+ "fastmcp",
88
+ "run",
89
+ "server.py",
90
+ "--no-banner",
91
+ ]
92
+ assert cmd == expected
93
+
94
+
95
+ class TestVersionCommand:
96
+ """Test the version command."""
97
+
98
+ @patch("fastmcp.cli.cli.sys.exit")
99
+ @patch("fastmcp.cli.cli.console.print")
100
+ def test_version_command(self, mock_print, mock_exit):
101
+ """Test that version command prints info and exits."""
102
+ # Parse and execute version command
103
+ command, bound, _ = app.parse_args(["version"])
104
+ command()
105
+
106
+ # Verify it printed something and exited with 0
107
+ mock_print.assert_called_once()
108
+ mock_exit.assert_called_once_with(0)
109
+
110
+
111
+ class TestDevCommand:
112
+ """Test the dev command."""
113
+
114
+ def test_dev_command_parsing(self):
115
+ """Test that dev command can be parsed with various options."""
116
+ # Test basic parsing
117
+ command, bound, _ = app.parse_args(["dev", "server.py"])
118
+ assert command is not None
119
+ assert bound.arguments["server_spec"] == "server.py"
120
+
121
+ # Test with options
122
+ command, bound, _ = app.parse_args(
123
+ [
124
+ "dev",
125
+ "server.py",
126
+ "--with",
127
+ "package1",
128
+ "--inspector-version",
129
+ "1.0.0",
130
+ "--ui-port",
131
+ "3000",
132
+ ]
133
+ )
134
+ assert bound.arguments["with_packages"] == ["package1"]
135
+ assert bound.arguments["inspector_version"] == "1.0.0"
136
+ assert bound.arguments["ui_port"] == 3000
137
+
138
+
139
+ class TestRunCommand:
140
+ """Test the run command."""
141
+
142
+ @patch("fastmcp.cli.cli.run_module.run_command")
143
+ def test_run_command_basic(self, mock_run_command):
144
+ """Test basic run command."""
145
+ command, bound, _ = app.parse_args(["run", "server.py"])
146
+ command(**bound.arguments)
147
+
148
+ mock_run_command.assert_called_once_with(
149
+ server_spec="server.py",
150
+ transport=None,
151
+ host=None,
152
+ port=None,
153
+ log_level=None,
154
+ server_args=[],
155
+ show_banner=True,
156
+ )
157
+
158
+ @patch("fastmcp.cli.cli.run_module.run_command")
159
+ def test_run_command_with_options(self, mock_run_command):
160
+ """Test run command with various options."""
161
+ command, bound, _ = app.parse_args(
162
+ [
163
+ "run",
164
+ "server.py",
165
+ "--transport",
166
+ "http",
167
+ "--host",
168
+ "localhost",
169
+ "--port",
170
+ "8080",
171
+ "--log-level",
172
+ "DEBUG",
173
+ "--no-banner",
174
+ ]
175
+ )
176
+ command(**bound.arguments)
177
+
178
+ mock_run_command.assert_called_once_with(
179
+ server_spec="server.py",
180
+ transport="http",
181
+ host="localhost",
182
+ port=8080,
183
+ log_level="DEBUG",
184
+ server_args=[],
185
+ show_banner=False,
186
+ )
187
+
188
+ @patch("fastmcp.cli.cli.run_module.run_command")
189
+ def test_run_command_failure(self, mock_run_command):
190
+ """Test run command handling failures."""
191
+ mock_run_command.side_effect = Exception("Test error")
192
+
193
+ with pytest.raises(SystemExit) as exc_info:
194
+ command, bound, _ = app.parse_args(["run", "server.py"])
195
+ command(**bound.arguments)
196
+
197
+ assert exc_info.value.code == 1
198
+
199
+
200
+ class TestInspectCommand:
201
+ """Test the inspect command."""
202
+
203
+ @patch("fastmcp.cli.cli.run_module.parse_file_path")
204
+ @patch("fastmcp.cli.cli.run_module.import_server")
205
+ @patch("fastmcp.cli.cli.inspect_fastmcp")
206
+ def test_inspect_command_basic(
207
+ self, mock_inspect, mock_import_server, mock_parse_file_path, tmp_path
208
+ ):
209
+ """Test basic inspect command functionality."""
210
+ # Setup mocks
211
+ mock_parse_file_path.return_value = (Path("server.py"), None)
212
+ mock_server = Mock()
213
+ mock_import_server.return_value = mock_server
214
+
215
+ mock_info = Mock()
216
+ mock_info.name = "TestServer"
217
+ mock_info.tools = []
218
+ mock_info.prompts = []
219
+ mock_info.resources = []
220
+ mock_info.templates = []
221
+ mock_inspect.return_value = mock_info
222
+
223
+ # Mock TypeAdapter
224
+ with patch("fastmcp.cli.cli.TypeAdapter") as mock_adapter:
225
+ mock_adapter.return_value.dump_json.return_value = b'{"name": "TestServer"}'
226
+
227
+ output_file = tmp_path / "test-output.json"
228
+
229
+ # Parse and execute
230
+ command, bound, _ = app.parse_args(
231
+ [
232
+ "inspect",
233
+ "server.py",
234
+ "--output",
235
+ str(output_file),
236
+ ]
237
+ )
238
+
239
+ # This is an async command, so we need to run it
240
+ import asyncio
241
+
242
+ asyncio.run(command(**bound.arguments))
243
+
244
+ # Verify the output file was created
245
+ assert output_file.exists()
246
+ assert output_file.read_text() == '{"name": "TestServer"}'
247
+
248
+ @patch("fastmcp.cli.cli.run_module.import_server")
249
+ def test_inspect_command_failure(self, mock_import_server):
250
+ """Test inspect command handling failures."""
251
+ mock_import_server.side_effect = Exception("Import failed")
252
+
253
+ with pytest.raises(SystemExit) as exc_info:
254
+ command, bound, _ = app.parse_args(["inspect", "server.py"])
255
+ import asyncio
256
+
257
+ asyncio.run(command(**bound.arguments))
258
+
259
+ assert exc_info.value.code == 1
tests/cli/test_install.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the install subcommands."""
2
+
3
+ from fastmcp.cli.install import install_app
4
+
5
+
6
+ class TestInstallApp:
7
+ """Test the install subapp."""
8
+
9
+ def test_install_app_exists(self):
10
+ """Test that the install app is properly configured."""
11
+ # install_app.name is a tuple in cyclopts
12
+ assert "install" in install_app.name
13
+ assert "Install MCP servers" in install_app.help
14
+
15
+ def test_install_commands_registered(self):
16
+ """Test that all install commands are registered."""
17
+ # Check that the app has the expected help text and structure
18
+ # This is a simpler check that doesn't rely on internal methods
19
+ assert hasattr(install_app, "help")
20
+ assert "Install MCP servers" in install_app.help
21
+
22
+ # We can test that the commands parse without errors
23
+ try:
24
+ install_app.parse_args(["claude-code", "--help"])
25
+ install_app.parse_args(["claude-desktop", "--help"])
26
+ install_app.parse_args(["cursor", "--help"])
27
+ install_app.parse_args(["mcp-json", "--help"])
28
+ except SystemExit:
29
+ # Help commands exit with 0, that's expected
30
+ pass
31
+
32
+
33
+ class TestClaudeCodeInstall:
34
+ """Test claude-code install command."""
35
+
36
+ def test_claude_code_basic(self):
37
+ """Test basic claude-code install command parsing."""
38
+ # Parse command with correct parameter names
39
+ command, bound, _ = install_app.parse_args(
40
+ ["claude-code", "server.py", "--server-name", "test-server"]
41
+ )
42
+
43
+ # Verify parsing was successful
44
+ assert command is not None
45
+ assert bound.arguments["server_spec"] == "server.py"
46
+ assert bound.arguments["server_name"] == "test-server"
47
+
48
+ def test_claude_code_with_options(self):
49
+ """Test claude-code install with various options."""
50
+ command, bound, _ = install_app.parse_args(
51
+ [
52
+ "claude-code",
53
+ "server.py",
54
+ "--server-name",
55
+ "test-server",
56
+ "--with",
57
+ "package1",
58
+ "--with",
59
+ "package2",
60
+ "--env",
61
+ "VAR1=value1",
62
+ ]
63
+ )
64
+
65
+ assert bound.arguments["with_packages"] == ["package1", "package2"]
66
+ assert bound.arguments["env_vars"] == ["VAR1=value1"]
67
+
68
+
69
+ class TestClaudeDesktopInstall:
70
+ """Test claude-desktop install command."""
71
+
72
+ def test_claude_desktop_basic(self):
73
+ """Test basic claude-desktop install command parsing."""
74
+ command, bound, _ = install_app.parse_args(
75
+ ["claude-desktop", "server.py", "--server-name", "test-server"]
76
+ )
77
+
78
+ assert command is not None
79
+ assert bound.arguments["server_spec"] == "server.py"
80
+ assert bound.arguments["server_name"] == "test-server"
81
+
82
+ def test_claude_desktop_with_env_vars(self):
83
+ """Test claude-desktop install with environment variables."""
84
+ command, bound, _ = install_app.parse_args(
85
+ [
86
+ "claude-desktop",
87
+ "server.py",
88
+ "--server-name",
89
+ "test-server",
90
+ "--env",
91
+ "VAR1=value1",
92
+ "--env",
93
+ "VAR2=value2",
94
+ ]
95
+ )
96
+
97
+ assert bound.arguments["env_vars"] == ["VAR1=value1", "VAR2=value2"]
98
+
99
+
100
+ class TestCursorInstall:
101
+ """Test cursor install command."""
102
+
103
+ def test_cursor_basic(self):
104
+ """Test basic cursor install command parsing."""
105
+ command, bound, _ = install_app.parse_args(
106
+ ["cursor", "server.py", "--server-name", "test-server"]
107
+ )
108
+
109
+ assert command is not None
110
+ assert bound.arguments["server_spec"] == "server.py"
111
+ assert bound.arguments["server_name"] == "test-server"
112
+
113
+ def test_cursor_with_options(self):
114
+ """Test cursor install with options."""
115
+ command, bound, _ = install_app.parse_args(
116
+ ["cursor", "server.py", "--server-name", "test-server"]
117
+ )
118
+
119
+ assert bound.arguments["server_spec"] == "server.py"
120
+ assert bound.arguments["server_name"] == "test-server"
121
+
122
+
123
+ class TestMcpJsonInstall:
124
+ """Test mcp-json install command."""
125
+
126
+ def test_mcp_json_basic(self):
127
+ """Test basic mcp-json install command parsing."""
128
+ command, bound, _ = install_app.parse_args(
129
+ ["mcp-json", "server.py", "--server-name", "test-server"]
130
+ )
131
+
132
+ assert command is not None
133
+ assert bound.arguments["server_spec"] == "server.py"
134
+ assert bound.arguments["server_name"] == "test-server"
135
+
136
+ def test_mcp_json_with_copy(self):
137
+ """Test mcp-json install with copy to clipboard option."""
138
+ command, bound, _ = install_app.parse_args(
139
+ ["mcp-json", "server.py", "--server-name", "test-server", "--copy"]
140
+ )
141
+
142
+ assert bound.arguments["copy"] is True
143
+
144
+
145
+ class TestInstallCommandParsing:
146
+ """Test command parsing and error handling."""
147
+
148
+ def test_install_minimal_args(self):
149
+ """Test install commands with minimal required arguments."""
150
+ # Each command should work with just a server spec
151
+ commands_to_test = [
152
+ ["claude-code", "server.py"],
153
+ ["claude-desktop", "server.py"],
154
+ ["cursor", "server.py"],
155
+ ]
156
+
157
+ for cmd_args in commands_to_test:
158
+ command, bound, _ = install_app.parse_args(cmd_args)
159
+ assert command is not None
160
+ assert bound.arguments["server_spec"] == "server.py"
161
+
162
+ def test_mcp_json_minimal(self):
163
+ """Test that mcp-json works with minimal arguments."""
164
+ # Should work with just server spec
165
+ command, bound, _ = install_app.parse_args(["mcp-json", "server.py"])
166
+ assert command is not None
167
+ assert bound.arguments["server_spec"] == "server.py"
tests/cli/test_run.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the run module functionality."""
2
+
3
+ import sys
4
+ from unittest.mock import Mock, patch
5
+
6
+ import pytest
7
+
8
+ from fastmcp.cli.run import (
9
+ create_client_server,
10
+ import_server,
11
+ import_server_with_args,
12
+ is_url,
13
+ parse_file_path,
14
+ run_command,
15
+ )
16
+
17
+
18
+ class TestUrlDetection:
19
+ """Test URL detection functionality."""
20
+
21
+ def test_is_url_valid_http(self):
22
+ """Test detection of valid HTTP URLs."""
23
+ assert is_url("http://example.com")
24
+ assert is_url("http://localhost:8080")
25
+ assert is_url("http://127.0.0.1:3000/path")
26
+
27
+ def test_is_url_valid_https(self):
28
+ """Test detection of valid HTTPS URLs."""
29
+ assert is_url("https://example.com")
30
+ assert is_url("https://api.example.com/mcp")
31
+ assert is_url("https://localhost:8443")
32
+
33
+ def test_is_url_invalid(self):
34
+ """Test detection of non-URLs."""
35
+ assert not is_url("server.py")
36
+ assert not is_url("/path/to/server.py")
37
+ assert not is_url("server.py:app")
38
+ assert not is_url("ftp://example.com") # Not http/https
39
+ assert not is_url("file:///path/to/file")
40
+
41
+
42
+ class TestFilePathParsing:
43
+ """Test file path parsing functionality."""
44
+
45
+ def test_parse_file_path_simple(self, tmp_path):
46
+ """Test parsing simple file path without object."""
47
+ test_file = tmp_path / "server.py"
48
+ test_file.write_text("# test server")
49
+
50
+ file_path, server_object = parse_file_path(str(test_file))
51
+ assert file_path == test_file.resolve()
52
+ assert server_object is None
53
+
54
+ def test_parse_file_path_with_object(self, tmp_path):
55
+ """Test parsing file path with object specification."""
56
+ test_file = tmp_path / "server.py"
57
+ test_file.write_text("# test server")
58
+
59
+ file_path, server_object = parse_file_path(f"{test_file}:app")
60
+ assert file_path == test_file.resolve()
61
+ assert server_object == "app"
62
+
63
+ def test_parse_file_path_complex_object(self, tmp_path):
64
+ """Test parsing file path with complex object specification."""
65
+ test_file = tmp_path / "server.py"
66
+ test_file.write_text("# test server")
67
+
68
+ # The current implementation splits on the last colon, so file:module:app
69
+ # becomes file_path="file:module" and server_object="app"
70
+ # We need to create a file with a colon in the name for this test
71
+ complex_file = tmp_path / "server:module.py"
72
+ complex_file.write_text("# test server")
73
+
74
+ file_path, server_object = parse_file_path(f"{complex_file}:app")
75
+ assert file_path == complex_file.resolve()
76
+ assert server_object == "app"
77
+
78
+ def test_parse_file_path_nonexistent(self):
79
+ """Test parsing nonexistent file path exits."""
80
+ with pytest.raises(SystemExit) as exc_info:
81
+ parse_file_path("nonexistent.py")
82
+ assert exc_info.value.code == 1
83
+
84
+ def test_parse_file_path_directory(self, tmp_path):
85
+ """Test parsing directory path exits."""
86
+ with pytest.raises(SystemExit) as exc_info:
87
+ parse_file_path(str(tmp_path))
88
+ assert exc_info.value.code == 1
89
+
90
+ @pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test")
91
+ def test_parse_file_path_windows_drive(self, tmp_path):
92
+ """Test parsing Windows path with drive letter."""
93
+ # This test would only work on Windows with actual drive letters
94
+ # For now, just test the logic doesn't break with colons
95
+ test_file = tmp_path / "server.py"
96
+ test_file.write_text("# test server")
97
+
98
+ # Should handle paths that might look like Windows drives
99
+ file_path, server_object = parse_file_path(str(test_file))
100
+ assert file_path == test_file.resolve()
101
+ assert server_object is None
102
+
103
+
104
+ class TestServerImport:
105
+ """Test server import functionality."""
106
+
107
+ def test_import_server_with_standard_name(self, tmp_path):
108
+ """Test importing server with standard object name."""
109
+ test_file = tmp_path / "server.py"
110
+ test_file.write_text("""
111
+ import fastmcp
112
+ mcp = fastmcp.FastMCP("TestServer")
113
+ """)
114
+
115
+ with patch("fastmcp.cli.run.sys.path") as mock_path:
116
+ mock_path.__contains__ = Mock(return_value=False)
117
+ mock_path.insert = Mock()
118
+
119
+ # Mock the actual import process
120
+ with patch(
121
+ "fastmcp.cli.run.importlib.util.spec_from_file_location"
122
+ ) as mock_spec_from_file:
123
+ with patch(
124
+ "fastmcp.cli.run.importlib.util.module_from_spec"
125
+ ) as mock_module_from_spec:
126
+ # Setup mock module
127
+ mock_module = Mock()
128
+ mock_module.mcp = Mock()
129
+ mock_module_from_spec.return_value = mock_module
130
+
131
+ # Setup mock spec
132
+ mock_spec = Mock()
133
+ mock_spec.loader = Mock()
134
+ mock_spec_from_file.return_value = mock_spec
135
+
136
+ server = import_server(test_file)
137
+ assert server == mock_module.mcp
138
+
139
+ def test_import_server_with_custom_object(self, tmp_path):
140
+ """Test importing server with custom object name."""
141
+ test_file = tmp_path / "server.py"
142
+ test_file.write_text("""
143
+ import fastmcp
144
+ my_app = fastmcp.FastMCP("TestServer")
145
+ """)
146
+
147
+ with patch("fastmcp.cli.run.sys.path") as mock_path:
148
+ mock_path.__contains__ = Mock(return_value=False)
149
+ mock_path.insert = Mock()
150
+
151
+ with patch(
152
+ "fastmcp.cli.run.importlib.util.spec_from_file_location"
153
+ ) as mock_spec_from_file:
154
+ with patch(
155
+ "fastmcp.cli.run.importlib.util.module_from_spec"
156
+ ) as mock_module_from_spec:
157
+ mock_module = Mock()
158
+ mock_module.my_app = Mock()
159
+ mock_module_from_spec.return_value = mock_module
160
+
161
+ mock_spec = Mock()
162
+ mock_spec.loader = Mock()
163
+ mock_spec_from_file.return_value = mock_spec
164
+
165
+ server = import_server(test_file, "my_app")
166
+ assert server == mock_module.my_app
167
+
168
+ def test_import_server_no_standard_names(self, tmp_path):
169
+ """Test importing server when no standard names exist."""
170
+ test_file = tmp_path / "server.py"
171
+ test_file.write_text("# No server objects")
172
+
173
+ with patch("fastmcp.cli.run.sys.path"):
174
+ with patch(
175
+ "fastmcp.cli.run.importlib.util.spec_from_file_location"
176
+ ) as mock_spec_from_file:
177
+ with patch(
178
+ "fastmcp.cli.run.importlib.util.module_from_spec"
179
+ ) as mock_module_from_spec:
180
+ mock_module = Mock()
181
+
182
+ # Mock hasattr behavior for standard names
183
+ def mock_hasattr(obj, name):
184
+ return name not in ["mcp", "server", "app"]
185
+
186
+ with patch("builtins.hasattr", side_effect=mock_hasattr):
187
+ mock_module_from_spec.return_value = mock_module
188
+
189
+ mock_spec = Mock()
190
+ mock_spec.loader = Mock()
191
+ mock_spec_from_file.return_value = mock_spec
192
+
193
+ with pytest.raises(SystemExit) as exc_info:
194
+ import_server(test_file)
195
+ assert exc_info.value.code == 1
196
+
197
+ def test_import_server_nonexistent_object(self, tmp_path):
198
+ """Test importing nonexistent server object."""
199
+ test_file = tmp_path / "server.py"
200
+ test_file.write_text("# No server objects")
201
+
202
+ with patch("fastmcp.cli.run.sys.path"):
203
+ with patch(
204
+ "fastmcp.cli.run.importlib.util.spec_from_file_location"
205
+ ) as mock_spec_from_file:
206
+ with patch(
207
+ "fastmcp.cli.run.importlib.util.module_from_spec"
208
+ ) as mock_module_from_spec:
209
+ mock_module = Mock()
210
+ mock_module.nonexistent = None
211
+ mock_module_from_spec.return_value = mock_module
212
+
213
+ mock_spec = Mock()
214
+ mock_spec.loader = Mock()
215
+ mock_spec_from_file.return_value = mock_spec
216
+
217
+ with pytest.raises(SystemExit) as exc_info:
218
+ import_server(test_file, "nonexistent")
219
+ assert exc_info.value.code == 1
220
+
221
+
222
+ class TestServerImportWithArgs:
223
+ """Test server import with command line arguments."""
224
+
225
+ @patch("fastmcp.cli.run.import_server")
226
+ def test_import_server_with_args(self, mock_import_server, tmp_path):
227
+ """Test importing server with command line arguments."""
228
+ test_file = tmp_path / "server.py"
229
+ mock_server = Mock()
230
+ mock_import_server.return_value = mock_server
231
+
232
+ original_argv = sys.argv[:]
233
+ try:
234
+ result = import_server_with_args(
235
+ test_file, "app", ["--config", "test.json", "--debug"]
236
+ )
237
+
238
+ assert result == mock_server
239
+ mock_import_server.assert_called_once_with(test_file, "app")
240
+
241
+ finally:
242
+ sys.argv = original_argv
243
+
244
+ @patch("fastmcp.cli.run.import_server")
245
+ def test_import_server_no_args(self, mock_import_server, tmp_path):
246
+ """Test importing server without command line arguments."""
247
+ test_file = tmp_path / "server.py"
248
+ mock_server = Mock()
249
+ mock_import_server.return_value = mock_server
250
+
251
+ result = import_server_with_args(test_file, "app")
252
+
253
+ assert result == mock_server
254
+ mock_import_server.assert_called_once_with(test_file, "app")
255
+
256
+
257
+ class TestClientServer:
258
+ """Test client server creation."""
259
+
260
+ def test_create_client_server(self):
261
+ """Test creating server from client URL."""
262
+ # Patch the import at the builtins level since it's a local import
263
+ with patch("builtins.__import__") as mock_import:
264
+ mock_fastmcp = Mock()
265
+ mock_import.return_value = mock_fastmcp
266
+
267
+ mock_client = Mock()
268
+ mock_server = Mock()
269
+ mock_fastmcp.Client.return_value = mock_client
270
+ mock_fastmcp.FastMCP.from_client.return_value = mock_server
271
+
272
+ result = create_client_server("http://example.com")
273
+
274
+ assert result == mock_server
275
+ mock_fastmcp.Client.assert_called_once_with("http://example.com")
276
+ mock_fastmcp.FastMCP.from_client.assert_called_once_with(mock_client)
277
+
278
+ def test_create_client_server_failure(self):
279
+ """Test client server creation failure."""
280
+ with patch("builtins.__import__") as mock_import:
281
+ mock_fastmcp = Mock()
282
+ mock_import.return_value = mock_fastmcp
283
+ mock_fastmcp.Client.side_effect = Exception("Connection failed")
284
+
285
+ with pytest.raises(SystemExit) as exc_info:
286
+ create_client_server("http://example.com")
287
+ assert exc_info.value.code == 1
288
+
289
+
290
+ class TestRunCommand:
291
+ """Test the main run command functionality."""
292
+
293
+ @patch("fastmcp.cli.run.create_client_server")
294
+ def test_run_command_url(self, mock_create_client_server):
295
+ """Test running command with URL."""
296
+ mock_server = Mock()
297
+ mock_create_client_server.return_value = mock_server
298
+
299
+ run_command("http://example.com")
300
+
301
+ mock_create_client_server.assert_called_once_with("http://example.com")
302
+ mock_server.run.assert_called_once()
303
+
304
+ @patch("fastmcp.cli.run.import_server_with_args")
305
+ @patch("fastmcp.cli.run.parse_file_path")
306
+ def test_run_command_file(self, mock_parse_file_path, mock_import_server):
307
+ """Test running command with file path."""
308
+ mock_file = Mock()
309
+ mock_parse_file_path.return_value = (mock_file, "app")
310
+ mock_server = Mock()
311
+ mock_server.name = "TestServer"
312
+ mock_import_server.return_value = mock_server
313
+
314
+ run_command("server.py:app")
315
+
316
+ mock_parse_file_path.assert_called_once_with("server.py:app")
317
+ mock_import_server.assert_called_once_with(mock_file, "app", None)
318
+ mock_server.run.assert_called_once()
319
+
320
+ @patch("fastmcp.cli.run.import_server_with_args")
321
+ @patch("fastmcp.cli.run.parse_file_path")
322
+ def test_run_command_with_options(self, mock_parse_file_path, mock_import_server):
323
+ """Test running command with various options."""
324
+ mock_file = Mock()
325
+ mock_parse_file_path.return_value = (mock_file, None)
326
+ mock_server = Mock()
327
+ mock_server.name = "TestServer"
328
+ mock_import_server.return_value = mock_server
329
+
330
+ run_command(
331
+ "server.py",
332
+ transport="http",
333
+ host="localhost",
334
+ port=8080,
335
+ log_level="DEBUG",
336
+ server_args=["--config", "test.json"],
337
+ show_banner=False,
338
+ )
339
+
340
+ mock_server.run.assert_called_once_with(
341
+ transport="http",
342
+ host="localhost",
343
+ port=8080,
344
+ log_level="DEBUG",
345
+ show_banner=False,
346
+ )
347
+
348
+ @patch("fastmcp.cli.run.import_server_with_args")
349
+ @patch("fastmcp.cli.run.parse_file_path")
350
+ def test_run_command_server_failure(self, mock_parse_file_path, mock_import_server):
351
+ """Test run command when server run fails."""
352
+ mock_file = Mock()
353
+ mock_parse_file_path.return_value = (mock_file, None)
354
+ mock_server = Mock()
355
+ mock_server.name = "TestServer"
356
+ mock_server.run.side_effect = Exception("Server failed")
357
+ mock_import_server.return_value = mock_server
358
+
359
+ with pytest.raises(SystemExit) as exc_info:
360
+ run_command("server.py")
361
+ assert exc_info.value.code == 1