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