Jeremiah Lowin commited on
Commit
e36ebff
·
unverified ·
2 Parent(s): 15764bfedea86d

Merge branch 'main' into copy-flag

Browse files
tests/cli/test_cli.py CHANGED
@@ -1,5 +1,3 @@
1
- """Tests for the main CLI functionality."""
2
-
3
  import subprocess
4
  from pathlib import Path
5
  from unittest.mock import Mock, patch
@@ -52,7 +50,7 @@ class TestMainCLI:
52
  "--with",
53
  "fastmcp",
54
  "--with-editable",
55
- "/path/to/package",
56
  "fastmcp",
57
  "run",
58
  "server.py",
@@ -96,17 +94,19 @@ class TestMainCLI:
96
  class TestVersionCommand:
97
  """Test the version command."""
98
 
99
- @patch("fastmcp.cli.cli.sys.exit")
100
- @patch("fastmcp.cli.cli.console.print")
101
- def test_version_command(self, mock_print, mock_exit):
102
- """Test that version command prints info and exits."""
103
- # Parse and execute version command
104
  command, bound, _ = app.parse_args(["version"])
105
- command()
106
 
107
- # Verify it printed something and exited with 0
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."""
@@ -177,25 +177,21 @@ class TestDevCommand:
177
  class TestRunCommand:
178
  """Test the run command."""
179
 
180
- @patch("fastmcp.cli.cli.run_module.run_command")
181
- def test_run_command_basic(self, mock_run_command):
182
- """Test basic run command."""
183
  command, bound, _ = app.parse_args(["run", "server.py"])
184
- command(**bound.arguments)
185
-
186
- mock_run_command.assert_called_once_with(
187
- server_spec="server.py",
188
- transport=None,
189
- host=None,
190
- port=None,
191
- log_level=None,
192
- server_args=[],
193
- show_banner=True,
194
- )
195
 
196
- @patch("fastmcp.cli.cli.run_module.run_command")
197
- def test_run_command_with_options(self, mock_run_command):
198
- """Test run command with various options."""
 
 
 
 
 
 
 
 
199
  command, bound, _ = app.parse_args(
200
  [
201
  "run",
@@ -211,28 +207,35 @@ class TestRunCommand:
211
  "--no-banner",
212
  ]
213
  )
214
- command(**bound.arguments)
215
 
216
- mock_run_command.assert_called_once_with(
217
- server_spec="server.py",
218
- transport="http",
219
- host="localhost",
220
- port=8080,
221
- log_level="DEBUG",
222
- server_args=[],
223
- show_banner=False,
 
 
 
 
 
 
 
 
 
 
224
  )
225
 
226
- @patch("fastmcp.cli.cli.run_module.run_command")
227
- def test_run_command_failure(self, mock_run_command):
228
- """Test run command handling failures."""
229
- mock_run_command.side_effect = Exception("Test error")
230
-
231
- with pytest.raises(SystemExit) as exc_info:
232
- command, bound, _ = app.parse_args(["run", "server.py"])
233
- command(**bound.arguments)
234
-
235
- assert exc_info.value.code == 1
236
 
237
 
238
  class TestWindowsSpecific:
@@ -317,89 +320,93 @@ class TestWindowsSpecific:
317
  assert result == "npx"
318
  mock_run.assert_not_called()
319
 
320
- def test_windows_path_parsing_with_colon(self):
321
  """Test parsing Windows paths with drive letters and colons."""
322
  from fastmcp.cli.run import parse_file_path
323
 
324
- # We can't test actual Windows paths on non-Windows systems,
325
- # but we can test the logic with mock paths
326
- with patch("pathlib.Path.exists") as mock_exists:
327
- with patch("pathlib.Path.is_file") as mock_is_file:
328
- mock_exists.return_value = True
329
- mock_is_file.return_value = True
330
-
331
- # Test that C:\path\file.py is parsed correctly
332
- with patch("pathlib.Path.resolve") as mock_resolve:
333
- mock_resolve.return_value = Path("C:/path/file.py")
334
 
335
- file_path, obj = parse_file_path("C:\\path\\file.py")
336
- assert obj is None
 
337
 
338
- # Test C:\path\file.py:object parsing
339
- with patch("pathlib.Path.resolve") as mock_resolve:
340
- mock_resolve.return_value = Path("C:/path/file.py")
341
 
342
- file_path, obj = parse_file_path("C:\\path\\file.py:myapp")
343
- assert obj == "myapp"
344
 
345
 
346
  class TestInspectCommand:
347
  """Test the inspect command."""
348
 
349
- @patch("fastmcp.cli.cli.run_module.parse_file_path")
350
- @patch("fastmcp.cli.cli.run_module.import_server")
351
- @patch("fastmcp.cli.cli.inspect_fastmcp")
352
- def test_inspect_command_basic(
353
- self, mock_inspect, mock_import_server, mock_parse_file_path, tmp_path
354
- ):
355
- """Test basic inspect command functionality."""
356
- # Setup mocks
357
- mock_parse_file_path.return_value = (Path("server.py"), None)
358
- mock_server = Mock()
359
- mock_import_server.return_value = mock_server
360
-
361
- mock_info = Mock()
362
- mock_info.name = "TestServer"
363
- mock_info.tools = []
364
- mock_info.prompts = []
365
- mock_info.resources = []
366
- mock_info.templates = []
367
- mock_inspect.return_value = mock_info
368
-
369
- # Mock TypeAdapter
370
- with patch("fastmcp.cli.cli.TypeAdapter") as mock_adapter:
371
- mock_adapter.return_value.dump_json.return_value = b'{"name": "TestServer"}'
372
-
373
- output_file = tmp_path / "test-output.json"
374
-
375
- # Parse and execute
376
- command, bound, _ = app.parse_args(
377
- [
378
- "inspect",
379
- "server.py",
380
- "--output",
381
- str(output_file),
382
- ]
383
- )
384
 
385
- # This is an async command, so we need to run it
386
- import asyncio
 
 
387
 
388
- asyncio.run(command(**bound.arguments))
 
 
389
 
390
- # Verify the output file was created
391
- assert output_file.exists()
392
- assert output_file.read_text() == '{"name": "TestServer"}'
 
 
 
 
 
393
 
394
- @patch("fastmcp.cli.cli.run_module.import_server")
395
- def test_inspect_command_failure(self, mock_import_server):
396
- """Test inspect command handling failures."""
397
- mock_import_server.side_effect = Exception("Import failed")
398
 
399
- with pytest.raises(SystemExit) as exc_info:
400
- command, bound, _ = app.parse_args(["inspect", "server.py"])
401
- import asyncio
 
 
 
402
 
403
- asyncio.run(command(**bound.arguments))
404
 
405
- assert exc_info.value.code == 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import subprocess
2
  from pathlib import Path
3
  from unittest.mock import Mock, patch
 
50
  "--with",
51
  "fastmcp",
52
  "--with-editable",
53
+ str(editable_path),
54
  "fastmcp",
55
  "run",
56
  "server.py",
 
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
105
+ with pytest.raises(SystemExit) as exc_info:
106
+ command, bound, _ = app.parse_args(["version"])
107
+ command()
108
+
109
+ assert exc_info.value.code == 0
110
 
111
  def test_version_command_parsing(self):
112
  """Test that the version command parses arguments correctly."""
 
177
  class TestRunCommand:
178
  """Test the run command."""
179
 
180
+ def test_run_command_parsing_basic(self):
181
+ """Test basic run command parsing."""
 
182
  command, bound, _ = app.parse_args(["run", "server.py"])
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ assert command is not None
185
+ assert bound.arguments["server_spec"] == "server.py"
186
+ # Cyclopts only includes non-default values
187
+ assert "transport" not in bound.arguments
188
+ assert "host" not in bound.arguments
189
+ assert "port" not in bound.arguments
190
+ assert "log_level" not in bound.arguments
191
+ assert "no_banner" not in bound.arguments
192
+
193
+ def test_run_command_parsing_with_options(self):
194
+ """Test run command parsing with various options."""
195
  command, bound, _ = app.parse_args(
196
  [
197
  "run",
 
207
  "--no-banner",
208
  ]
209
  )
 
210
 
211
+ assert command is not None
212
+ assert bound.arguments["server_spec"] == "server.py"
213
+ assert bound.arguments["transport"] == "http"
214
+ assert bound.arguments["host"] == "localhost"
215
+ assert bound.arguments["port"] == 8080
216
+ assert bound.arguments["log_level"] == "DEBUG"
217
+ assert bound.arguments["no_banner"] is True
218
+
219
+ def test_run_command_parsing_partial_options(self):
220
+ """Test run command parsing with only some options."""
221
+ command, bound, _ = app.parse_args(
222
+ [
223
+ "run",
224
+ "server.py",
225
+ "--transport",
226
+ "http",
227
+ "--no-banner",
228
+ ]
229
  )
230
 
231
+ assert command is not None
232
+ assert bound.arguments["server_spec"] == "server.py"
233
+ assert bound.arguments["transport"] == "http"
234
+ assert bound.arguments["no_banner"] is True
235
+ # Other options should not be present
236
+ assert "host" not in bound.arguments
237
+ assert "port" not in bound.arguments
238
+ assert "log_level" not in bound.arguments
 
 
239
 
240
 
241
  class TestWindowsSpecific:
 
320
  assert result == "npx"
321
  mock_run.assert_not_called()
322
 
323
+ def test_windows_path_parsing_with_colon(self, tmp_path):
324
  """Test parsing Windows paths with drive letters and colons."""
325
  from fastmcp.cli.run import parse_file_path
326
 
327
+ # Create a real test file to test the logic
328
+ test_file = tmp_path / "server.py"
329
+ test_file.write_text("# test server")
 
 
 
 
 
 
 
330
 
331
+ # Test normal file parsing (works on all platforms)
332
+ file_path, obj = parse_file_path(str(test_file))
333
+ assert obj is None
334
 
335
+ # Test file:object parsing
336
+ file_path, obj = parse_file_path(f"{test_file}:myapp")
337
+ assert obj == "myapp"
338
 
339
+ # Test that the file portion resolves correctly when object is specified
340
+ assert file_path == test_file.resolve()
341
 
342
 
343
  class TestInspectCommand:
344
  """Test the inspect command."""
345
 
346
+ def test_inspect_command_parsing_basic(self):
347
+ """Test basic inspect command parsing."""
348
+ command, bound, _ = app.parse_args(["inspect", "server.py"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
+ assert command is not None
351
+ assert bound.arguments["server_spec"] == "server.py"
352
+ # Only explicitly set parameters are in bound.arguments
353
+ assert "output" not in bound.arguments
354
 
355
+ def test_inspect_command_parsing_with_output(self, tmp_path):
356
+ """Test inspect command parsing with output file."""
357
+ output_file = tmp_path / "output.json"
358
 
359
+ command, bound, _ = app.parse_args(
360
+ [
361
+ "inspect",
362
+ "server.py",
363
+ "--output",
364
+ str(output_file),
365
+ ]
366
+ )
367
 
368
+ assert command is not None
369
+ assert bound.arguments["server_spec"] == "server.py"
370
+ # Output is parsed as a Path object
371
+ assert bound.arguments["output"] == output_file
372
 
373
+ async def test_inspect_command_with_real_server(self, tmp_path):
374
+ """Test inspect command with a real server file."""
375
+ # Create a real server file
376
+ server_file = tmp_path / "test_server.py"
377
+ server_file.write_text("""
378
+ import fastmcp
379
 
380
+ mcp = fastmcp.FastMCP("InspectTestServer")
381
 
382
+ @mcp.tool
383
+ def test_tool(x: int) -> int:
384
+ return x * 2
385
+
386
+ @mcp.prompt
387
+ def test_prompt(name: str) -> str:
388
+ return f"Hello, {name}!"
389
+ """)
390
+
391
+ output_file = tmp_path / "inspect_output.json"
392
+
393
+ # Parse and execute the command
394
+ command, bound, _ = app.parse_args(
395
+ [
396
+ "inspect",
397
+ str(server_file),
398
+ "--output",
399
+ str(output_file),
400
+ ]
401
+ )
402
+
403
+ await command(**bound.arguments)
404
+
405
+ # Verify the output file was created and contains expected content
406
+ assert output_file.exists()
407
+ content = output_file.read_text()
408
+
409
+ # Basic checks that the inspection worked
410
+ assert "InspectTestServer" in content
411
+ assert "test_tool" in content
412
+ assert "test_prompt" in content
tests/cli/test_cursor.py CHANGED
@@ -1,5 +1,3 @@
1
- """Tests for Cursor integration functionality."""
2
-
3
  import base64
4
  import json
5
  from pathlib import Path
@@ -257,7 +255,9 @@ class TestInstallCursor:
257
  config_data = json.loads(decoded)
258
 
259
  assert "--with-editable" in config_data["args"]
260
- assert "/local/package" in config_data["args"]
 
 
261
  assert "server.py:custom_app" in " ".join(config_data["args"])
262
 
263
  @patch("fastmcp.cli.install.cursor.open_deeplink")
 
 
 
1
  import base64
2
  import json
3
  from pathlib import Path
 
255
  config_data = json.loads(decoded)
256
 
257
  assert "--with-editable" in config_data["args"]
258
+ # Check for the editable path in a platform-agnostic way
259
+ editable_path_str = str(Path("/local/package"))
260
+ assert editable_path_str in config_data["args"]
261
  assert "server.py:custom_app" in " ".join(config_data["args"])
262
 
263
  @patch("fastmcp.cli.install.cursor.open_deeplink")
tests/cli/test_install.py CHANGED
@@ -1,5 +1,3 @@
1
- """Tests for the install subcommands."""
2
-
3
  from fastmcp.cli.install import install_app
4
 
5
 
 
 
 
1
  from fastmcp.cli.install import install_app
2
 
3
 
tests/cli/test_run.py CHANGED
@@ -1,17 +1,9 @@
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
 
@@ -87,275 +79,121 @@ class TestFilePathParsing:
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
 
 
 
 
 
 
1
  import pytest
2
 
3
  from fastmcp.cli.run import (
 
4
  import_server,
 
5
  is_url,
6
  parse_file_path,
 
7
  )
8
 
9
 
 
79
  parse_file_path(str(tmp_path))
80
  assert exc_info.value.code == 1
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  class TestServerImport:
84
+ """Test server import functionality using real files."""
85
 
86
+ async def test_import_server_basic_mcp(self, tmp_path):
87
+ """Test importing server with basic FastMCP server."""
88
  test_file = tmp_path / "server.py"
89
  test_file.write_text("""
90
  import fastmcp
91
+
92
  mcp = fastmcp.FastMCP("TestServer")
93
+
94
+ @mcp.tool
95
+ def greet(name: str) -> str:
96
+ return f"Hello, {name}!"
97
  """)
98
 
99
+ server = import_server(test_file)
100
+ assert server.name == "TestServer"
101
+ tools = await server.get_tools()
102
+ assert "greet" in tools
103
+
104
+ async def test_import_server_with_main_block(self, tmp_path):
105
+ """Test importing server with if __name__ == '__main__' block."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  test_file = tmp_path / "server.py"
107
  test_file.write_text("""
108
  import fastmcp
109
+
110
+ app = fastmcp.FastMCP("MainServer")
111
+
112
+ @app.tool
113
+ def calculate(x: int, y: int) -> int:
114
+ return x + y
115
+
116
+ if __name__ == "__main__":
117
+ app.run()
118
  """)
119
 
120
+ server = import_server(test_file)
121
+ assert server.name == "MainServer"
122
+ tools = await server.get_tools()
123
+ assert "calculate" in tools
124
+
125
+ def test_import_server_standard_names(self, tmp_path):
126
+ """Test automatic detection of standard names (mcp, server, app)."""
127
+ # Test with 'mcp' name
128
+ mcp_file = tmp_path / "mcp_server.py"
129
+ mcp_file.write_text("""
130
+ import fastmcp
131
+ mcp = fastmcp.FastMCP("MCPServer")
132
+ """)
133
+
134
+ server = import_server(mcp_file)
135
+ assert server.name == "MCPServer"
136
+
137
+ # Test with 'server' name
138
+ server_file = tmp_path / "server_server.py"
139
+ server_file.write_text("""
140
+ import fastmcp
141
+ server = fastmcp.FastMCP("ServerServer")
142
+ """)
143
+
144
+ server = import_server(server_file)
145
+ assert server.name == "ServerServer"
146
+
147
+ # Test with 'app' name
148
+ app_file = tmp_path / "app_server.py"
149
+ app_file.write_text("""
150
+ import fastmcp
151
+ app = fastmcp.FastMCP("AppServer")
152
+ """)
153
+
154
+ server = import_server(app_file)
155
+ assert server.name == "AppServer"
156
+
157
+ async def test_import_server_nonstandard_name(self, tmp_path):
158
+ """Test importing server with non-standard object name."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  test_file = tmp_path / "server.py"
160
+ test_file.write_text("""
161
+ import fastmcp
162
 
163
+ my_custom_server = fastmcp.FastMCP("CustomServer")
 
 
 
 
164
 
165
+ @my_custom_server.tool
166
+ def custom_tool() -> str:
167
+ return "custom"
168
+ """)
169
 
170
+ server = import_server(test_file, "my_custom_server")
171
+ assert server.name == "CustomServer"
172
+ tools = await server.get_tools()
173
+ assert "custom_tool" in tools
174
 
175
+ def test_import_server_no_standard_names_fails(self, tmp_path):
176
+ """Test importing server when no standard names exist fails."""
 
177
  test_file = tmp_path / "server.py"
178
+ test_file.write_text("""
179
+ import fastmcp
180
+
181
+ other_name = fastmcp.FastMCP("OtherServer")
182
+ """)
183
+
184
+ with pytest.raises(SystemExit) as exc_info:
185
+ import_server(test_file)
186
+ assert exc_info.value.code == 1
187
+
188
+ def test_import_server_nonexistent_object_fails(self, tmp_path):
189
+ """Test importing nonexistent server object fails."""
190
+ test_file = tmp_path / "server.py"
191
+ test_file.write_text("""
192
+ import fastmcp
193
+
194
+ mcp = fastmcp.FastMCP("TestServer")
195
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
  with pytest.raises(SystemExit) as exc_info:
198
+ import_server(test_file, "nonexistent")
199
  assert exc_info.value.code == 1
tests/cli/test_shared.py CHANGED
@@ -1,5 +1,3 @@
1
- """Tests for shared CLI functionality."""
2
-
3
  from fastmcp.cli.cli import _parse_env_var
4
 
5
 
 
 
 
1
  from fastmcp.cli.cli import _parse_env_var
2
 
3
 
tests/contrib/test_component_manager.py CHANGED
@@ -566,7 +566,6 @@ class TestComponentManagerWithPath:
566
  def client_with_path(self, mcp_with_path):
567
  return TestClient(mcp_with_path.http_app())
568
 
569
- @pytest.mark.asyncio
570
  async def test_enable_tool_route_with_path(self, client_with_path, mcp_with_path):
571
  tool = await mcp_with_path._tool_manager.get_tool("test_tool")
572
  tool.enabled = False
@@ -576,7 +575,6 @@ class TestComponentManagerWithPath:
576
  tool = await mcp_with_path._tool_manager.get_tool("test_tool")
577
  assert tool.enabled is True
578
 
579
- @pytest.mark.asyncio
580
  async def test_disable_resource_route_with_path(
581
  self, client_with_path, mcp_with_path
582
  ):
@@ -592,7 +590,6 @@ class TestComponentManagerWithPath:
592
  )
593
  assert resource.enabled is False
594
 
595
- @pytest.mark.asyncio
596
  async def test_enable_prompt_route_with_path(self, client_with_path, mcp_with_path):
597
  prompt = await mcp_with_path._prompt_manager.get_prompt("test_prompt")
598
  prompt.enabled = False
@@ -646,7 +643,6 @@ class TestComponentManagerWithPathAuth:
646
 
647
  self.client = TestClient(self.mcp.http_app())
648
 
649
- @pytest.mark.asyncio
650
  async def test_unauthorized_enable_tool(self):
651
  tool = await self.mcp._tool_manager.get_tool("test_tool")
652
  tool.enabled = False
@@ -654,7 +650,6 @@ class TestComponentManagerWithPathAuth:
654
  assert response.status_code == 401
655
  assert tool.enabled is False
656
 
657
- @pytest.mark.asyncio
658
  async def test_forbidden_enable_tool(self):
659
  tool = await self.mcp._tool_manager.get_tool("test_tool")
660
  tool.enabled = False
@@ -665,7 +660,6 @@ class TestComponentManagerWithPathAuth:
665
  assert response.status_code == 403
666
  assert tool.enabled is False
667
 
668
- @pytest.mark.asyncio
669
  async def test_authorized_enable_tool(self):
670
  tool = await self.mcp._tool_manager.get_tool("test_tool")
671
  tool.enabled = False
@@ -678,7 +672,6 @@ class TestComponentManagerWithPathAuth:
678
  tool = await self.mcp._tool_manager.get_tool("test_tool")
679
  assert tool.enabled is True
680
 
681
- @pytest.mark.asyncio
682
  async def test_unauthorized_disable_resource(self):
683
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
684
  resource.enabled = True
@@ -686,7 +679,6 @@ class TestComponentManagerWithPathAuth:
686
  assert response.status_code == 401
687
  assert resource.enabled is True
688
 
689
- @pytest.mark.asyncio
690
  async def test_forbidden_disable_resource(self):
691
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
692
  resource.enabled = True
@@ -697,7 +689,6 @@ class TestComponentManagerWithPathAuth:
697
  assert response.status_code == 403
698
  assert resource.enabled is True
699
 
700
- @pytest.mark.asyncio
701
  async def test_authorized_disable_resource(self):
702
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
703
  resource.enabled = True
@@ -710,7 +701,6 @@ class TestComponentManagerWithPathAuth:
710
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
711
  assert resource.enabled is False
712
 
713
- @pytest.mark.asyncio
714
  async def test_unauthorized_enable_prompt(self):
715
  prompt = await self.mcp._prompt_manager.get_prompt("test_prompt")
716
  prompt.enabled = False
@@ -718,7 +708,6 @@ class TestComponentManagerWithPathAuth:
718
  assert response.status_code == 401
719
  assert prompt.enabled is False
720
 
721
- @pytest.mark.asyncio
722
  async def test_forbidden_enable_prompt(self):
723
  prompt = await self.mcp._prompt_manager.get_prompt("test_prompt")
724
  prompt.enabled = False
@@ -729,7 +718,6 @@ class TestComponentManagerWithPathAuth:
729
  assert response.status_code == 403
730
  assert prompt.enabled is False
731
 
732
- @pytest.mark.asyncio
733
  async def test_authorized_enable_prompt(self):
734
  prompt = await self.mcp._prompt_manager.get_prompt("test_prompt")
735
  prompt.enabled = False
 
566
  def client_with_path(self, mcp_with_path):
567
  return TestClient(mcp_with_path.http_app())
568
 
 
569
  async def test_enable_tool_route_with_path(self, client_with_path, mcp_with_path):
570
  tool = await mcp_with_path._tool_manager.get_tool("test_tool")
571
  tool.enabled = False
 
575
  tool = await mcp_with_path._tool_manager.get_tool("test_tool")
576
  assert tool.enabled is True
577
 
 
578
  async def test_disable_resource_route_with_path(
579
  self, client_with_path, mcp_with_path
580
  ):
 
590
  )
591
  assert resource.enabled is False
592
 
 
593
  async def test_enable_prompt_route_with_path(self, client_with_path, mcp_with_path):
594
  prompt = await mcp_with_path._prompt_manager.get_prompt("test_prompt")
595
  prompt.enabled = False
 
643
 
644
  self.client = TestClient(self.mcp.http_app())
645
 
 
646
  async def test_unauthorized_enable_tool(self):
647
  tool = await self.mcp._tool_manager.get_tool("test_tool")
648
  tool.enabled = False
 
650
  assert response.status_code == 401
651
  assert tool.enabled is False
652
 
 
653
  async def test_forbidden_enable_tool(self):
654
  tool = await self.mcp._tool_manager.get_tool("test_tool")
655
  tool.enabled = False
 
660
  assert response.status_code == 403
661
  assert tool.enabled is False
662
 
 
663
  async def test_authorized_enable_tool(self):
664
  tool = await self.mcp._tool_manager.get_tool("test_tool")
665
  tool.enabled = False
 
672
  tool = await self.mcp._tool_manager.get_tool("test_tool")
673
  assert tool.enabled is True
674
 
 
675
  async def test_unauthorized_disable_resource(self):
676
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
677
  resource.enabled = True
 
679
  assert response.status_code == 401
680
  assert resource.enabled is True
681
 
 
682
  async def test_forbidden_disable_resource(self):
683
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
684
  resource.enabled = True
 
689
  assert response.status_code == 403
690
  assert resource.enabled is True
691
 
 
692
  async def test_authorized_disable_resource(self):
693
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
694
  resource.enabled = True
 
701
  resource = await self.mcp._resource_manager.get_resource("data://test_resource")
702
  assert resource.enabled is False
703
 
 
704
  async def test_unauthorized_enable_prompt(self):
705
  prompt = await self.mcp._prompt_manager.get_prompt("test_prompt")
706
  prompt.enabled = False
 
708
  assert response.status_code == 401
709
  assert prompt.enabled is False
710
 
 
711
  async def test_forbidden_enable_prompt(self):
712
  prompt = await self.mcp._prompt_manager.get_prompt("test_prompt")
713
  prompt.enabled = False
 
718
  assert response.status_code == 403
719
  assert prompt.enabled is False
720
 
 
721
  async def test_authorized_enable_prompt(self):
722
  prompt = await self.mcp._prompt_manager.get_prompt("test_prompt")
723
  prompt.enabled = False
tests/server/openapi/test_explode_integration.py CHANGED
@@ -7,7 +7,6 @@ specifications and properly applied during HTTP request serialization.
7
  from unittest.mock import AsyncMock, MagicMock
8
 
9
  import httpx
10
- import pytest
11
 
12
  from fastmcp.server.openapi import OpenAPITool
13
  from fastmcp.utilities.openapi import parse_openapi_to_http_routes
@@ -130,7 +129,6 @@ class TestExplodeIntegration:
130
  f"Expected explode=None, got {parameter.explode}"
131
  )
132
 
133
- @pytest.mark.asyncio
134
  async def test_explode_false_request_serialization(self):
135
  """Test that explode=false results in comma-separated query parameters in HTTP requests.
136
 
@@ -201,7 +199,6 @@ class TestExplodeIntegration:
201
  f"Expected 'red,blue,green', got '{tags_value}'"
202
  )
203
 
204
- @pytest.mark.asyncio
205
  async def test_explode_true_request_serialization(self):
206
  """Test that explode=true results in separate query parameters in HTTP requests."""
207
  openapi_spec = {
@@ -262,7 +259,6 @@ class TestExplodeIntegration:
262
  f"Expected ['red', 'blue', 'green'], got {tags_value}"
263
  )
264
 
265
- @pytest.mark.asyncio
266
  async def test_explode_default_request_serialization(self):
267
  """Test that default behavior (no explode) uses explode=true for query parameters."""
268
  openapi_spec = {
 
7
  from unittest.mock import AsyncMock, MagicMock
8
 
9
  import httpx
 
10
 
11
  from fastmcp.server.openapi import OpenAPITool
12
  from fastmcp.utilities.openapi import parse_openapi_to_http_routes
 
129
  f"Expected explode=None, got {parameter.explode}"
130
  )
131
 
 
132
  async def test_explode_false_request_serialization(self):
133
  """Test that explode=false results in comma-separated query parameters in HTTP requests.
134
 
 
199
  f"Expected 'red,blue,green', got '{tags_value}'"
200
  )
201
 
 
202
  async def test_explode_true_request_serialization(self):
203
  """Test that explode=true results in separate query parameters in HTTP requests."""
204
  openapi_spec = {
 
259
  f"Expected ['red', 'blue', 'green'], got {tags_value}"
260
  )
261
 
 
262
  async def test_explode_default_request_serialization(self):
263
  """Test that default behavior (no explode) uses explode=true for query parameters."""
264
  openapi_spec = {