11import json
2- import subprocess
32from pathlib import Path
43from unittest .mock import patch
54
@@ -23,36 +22,217 @@ def mock_config_path(temp_config_dir: Path):
2322 yield temp_config_dir
2423
2524
26- def test_command_execution (mock_config_path : Path ):
27- """Test that the generated command can actually be executed."""
28- # Setup
29- server_name = "test_server"
30- file_spec = "test_server.py:app"
25+ def test_basic_config_creation (mock_config_path : Path ):
26+ """Test that config file is created with correct structure."""
27+ success = update_claude_config (file_spec = "server.py:app" , server_name = "test" )
3128
32- # Update config
33- success = update_claude_config (file_spec = file_spec , server_name = server_name )
3429 assert success
30+ config_file = mock_config_path / "claude_desktop_config.json"
31+ assert config_file .exists ()
32+
33+ config = json .loads (config_file .read_text ())
34+ assert "mcpServers" in config
35+ assert "test" in config ["mcpServers" ]
36+
37+ server = config ["mcpServers" ]["test" ]
38+ assert "command" in server
39+ assert "args" in server
40+ assert server ["command" ].endswith ("uv" ) or server ["command" ].endswith ("uv.exe" )
41+
42+
43+ def test_args_structure (mock_config_path : Path ):
44+ """Test that args are built correctly."""
45+ success = update_claude_config (file_spec = "server.py:app" , server_name = "test" )
46+ assert success
47+
48+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
49+ args = config ["mcpServers" ]["test" ]["args" ]
50+
51+ # Should be: ["run", "--with", "mcp[cli]", "mcp", "run", "/abs/path/server.py:app"]
52+ assert args [0 ] == "run"
53+ assert "--with" in args
54+ assert "mcp[cli]" in args
55+ assert "mcp" in args
56+ assert args [args .index ("mcp" ) + 1 ] == "run"
57+ assert args [- 1 ].endswith ("server.py:app" )
58+
59+
60+ def test_absolute_file_path_resolution (mock_config_path : Path , tmp_path : Path ):
61+ """Test that file paths are resolved to absolute paths."""
62+ # Create a test file
63+ server_file = tmp_path / "my_server.py"
64+ server_file .write_text ("# test" )
65+
66+ success = update_claude_config (file_spec = str (server_file ) + ":app" , server_name = "test" )
67+ assert success
68+
69+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
70+ args = config ["mcpServers" ]["test" ]["args" ]
71+
72+ # Last arg should be absolute path
73+ assert args [- 1 ] == f"{ server_file .resolve ()} :app"
74+ assert Path (args [- 1 ].split (":" )[0 ]).is_absolute ()
75+
76+
77+ def test_env_vars_initial (mock_config_path : Path ):
78+ """Test that environment variables are set correctly on initial config."""
79+ success = update_claude_config (
80+ file_spec = "server.py:app" , server_name = "test" , env_vars = {"KEY1" : "value1" , "KEY2" : "value2" }
81+ )
82+ assert success
83+
84+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
85+ env = config ["mcpServers" ]["test" ]["env" ]
86+
87+ assert env ["KEY1" ] == "value1"
88+ assert env ["KEY2" ] == "value2"
89+
90+
91+ def test_env_vars_merged (mock_config_path : Path ):
92+ """Test that environment variables are merged correctly on update."""
93+ # First call with env vars
94+ update_claude_config (file_spec = "server.py:app" , server_name = "test" , env_vars = {"KEY1" : "value1" , "KEY2" : "value2" })
95+
96+ # Second call with overlapping env vars
97+ update_claude_config (
98+ file_spec = "server.py:app" , server_name = "test" , env_vars = {"KEY2" : "new_value" , "KEY3" : "value3" }
99+ )
100+
101+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
102+ env = config ["mcpServers" ]["test" ]["env" ]
103+
104+ assert env ["KEY1" ] == "value1" # Preserved
105+ assert env ["KEY2" ] == "new_value" # Updated
106+ assert env ["KEY3" ] == "value3" # Added
107+
108+
109+ def test_env_vars_preserved_when_none (mock_config_path : Path ):
110+ """Test that existing env vars are preserved when update doesn't specify any."""
111+ # First call with env vars
112+ update_claude_config (file_spec = "server.py:app" , server_name = "test" , env_vars = {"KEY1" : "value1" })
113+
114+ # Second call without env vars
115+ update_claude_config (file_spec = "server.py:app" , server_name = "test" )
116+
117+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
118+ env = config ["mcpServers" ]["test" ]["env" ]
119+
120+ assert env ["KEY1" ] == "value1" # Should still be there
121+
122+
123+ def test_multiple_packages (mock_config_path : Path ):
124+ """Test that multiple packages are included with --with."""
125+ success = update_claude_config (file_spec = "server.py:app" , server_name = "test" , with_packages = ["requests" , "httpx" ])
126+ assert success
127+
128+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
129+ args = config ["mcpServers" ]["test" ]["args" ]
35130
36- # Read the generated config
131+ # Should have: --with mcp[cli] --with httpx --with requests (sorted)
132+ with_indices = [i for i , arg in enumerate (args ) if arg == "--with" ]
133+ assert len (with_indices ) == 3
134+
135+ packages = [args [i + 1 ] for i in with_indices ]
136+ assert "mcp[cli]" in packages
137+ assert "httpx" in packages
138+ assert "requests" in packages
139+
140+
141+ def test_package_deduplication (mock_config_path : Path ):
142+ """Test that duplicate packages are deduplicated."""
143+ success = update_claude_config (
144+ file_spec = "server.py:app" , server_name = "test" , with_packages = ["mcp[cli]" , "requests" , "requests" ]
145+ )
146+ assert success
147+
148+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
149+ args = config ["mcpServers" ]["test" ]["args" ]
150+
151+ # Count --with flags
152+ with_count = sum (1 for arg in args if arg == "--with" )
153+ # Should have mcp[cli] and requests only once each
154+ assert with_count == 2
155+
156+
157+ def test_editable_package (mock_config_path : Path , tmp_path : Path ):
158+ """Test that editable package is added correctly."""
159+ editable_dir = tmp_path / "my_package"
160+ editable_dir .mkdir ()
161+
162+ success = update_claude_config (file_spec = "server.py:app" , server_name = "test" , with_editable = editable_dir )
163+ assert success
164+
165+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
166+ args = config ["mcpServers" ]["test" ]["args" ]
167+
168+ assert "--with-editable" in args
169+ idx = args .index ("--with-editable" )
170+ assert args [idx + 1 ] == str (editable_dir )
171+
172+
173+ def test_preserves_other_servers (mock_config_path : Path ):
174+ """Test that existing servers are preserved when adding a new one."""
175+ # Create config with existing server
37176 config_file = mock_config_path / "claude_desktop_config.json"
177+ config_file .write_text (
178+ json .dumps ({"mcpServers" : {"existing_server" : {"command" : "some_command" , "args" : ["arg1" , "arg2" ]}}})
179+ )
180+
181+ # Add new server
182+ success = update_claude_config (file_spec = "server.py:app" , server_name = "new_server" )
183+ assert success
184+
38185 config = json .loads (config_file .read_text ())
186+ assert "existing_server" in config ["mcpServers" ]
187+ assert "new_server" in config ["mcpServers" ]
188+ assert config ["mcpServers" ]["existing_server" ]["command" ] == "some_command"
189+ assert config ["mcpServers" ]["existing_server" ]["args" ] == ["arg1" , "arg2" ]
39190
40- # Get the command and args
41- server_config = config ["mcpServers" ][server_name ]
42- command = server_config ["command" ]
43- args = server_config ["args" ]
44191
45- test_args = [command ] + args + ["--help" ]
192+ def test_updates_existing_server (mock_config_path : Path ):
193+ """Test that updating an existing server replaces its config."""
194+ # Create initial server
195+ update_claude_config (file_spec = "old_server.py:app" , server_name = "test" , env_vars = {"OLD" : "value" })
46196
47- result = subprocess .run (test_args , capture_output = True , text = True , timeout = 5 , check = False )
197+ # Update the same server
198+ update_claude_config (file_spec = "new_server.py:app" , server_name = "test" , env_vars = {"NEW" : "value" })
48199
49- assert result .returncode == 0
50- assert "usage" in result .stdout .lower ()
200+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
201+ args = config ["mcpServers" ]["test" ]["args" ]
202+
203+ # Should have new file spec
204+ assert args [- 1 ].endswith ("new_server.py:app" )
205+ # Env vars should be merged (NEW takes precedence but OLD is preserved)
206+ assert "NEW" in config ["mcpServers" ]["test" ]["env" ]
207+ assert "OLD" in config ["mcpServers" ]["test" ]["env" ]
208+
209+
210+ def test_error_handling_missing_config_dir (tmp_path : Path ):
211+ """Test that missing config directory raises appropriate error."""
212+ with patch ("mcp.cli.claude.get_claude_config_path" , return_value = None ):
213+ with pytest .raises (RuntimeError , match = "Claude Desktop config directory not found" ):
214+ update_claude_config (file_spec = "server.py:app" , server_name = "test" )
215+
216+
217+ def test_file_spec_without_colon (mock_config_path : Path , tmp_path : Path ):
218+ """Test file spec without :object suffix."""
219+ server_file = tmp_path / "server.py"
220+ server_file .write_text ("# test" )
221+
222+ success = update_claude_config (file_spec = str (server_file ), server_name = "test" )
223+ assert success
224+
225+ config = json .loads ((mock_config_path / "claude_desktop_config.json" ).read_text ())
226+ args = config ["mcpServers" ]["test" ]["args" ]
227+
228+ # Last arg should be absolute path without colon
229+ assert args [- 1 ] == str (server_file .resolve ())
230+ assert ":" not in args [- 1 ]
51231
52232
53233def test_absolute_uv_path (mock_config_path : Path ):
54234 """Test that the absolute path to uv is used when available."""
55- # Mock the shutil.which function to return a fake path
235+ # Mock the get_uv_path function to return a fake path
56236 mock_uv_path = "/usr/local/bin/uv"
57237
58238 with patch ("mcp.cli.claude.get_uv_path" , return_value = mock_uv_path ):
@@ -73,3 +253,17 @@ def test_absolute_uv_path(mock_config_path: Path):
73253 command = server_config ["command" ]
74254
75255 assert command == mock_uv_path
256+
257+
258+ def test_creates_mcpservers_key_if_missing (mock_config_path : Path ):
259+ """Test that mcpServers key is created if config exists but key is missing."""
260+ config_file = mock_config_path / "claude_desktop_config.json"
261+ config_file .write_text (json .dumps ({"someOtherKey" : "value" }))
262+
263+ success = update_claude_config (file_spec = "server.py:app" , server_name = "test" )
264+ assert success
265+
266+ config = json .loads (config_file .read_text ())
267+ assert "mcpServers" in config
268+ assert "someOtherKey" in config # Original content preserved
269+ assert config ["someOtherKey" ] == "value"
0 commit comments