66
77import json
88import os
9+ import platform
910import subprocess
1011import sys
1112import tempfile
@@ -2523,8 +2524,17 @@ def test_create_mcp_config_file_single_server(self) -> None:
25232524 config = json .loads (config_path .read_text ())
25242525 assert 'mcpServers' in config
25252526 assert 'test-server' in config ['mcpServers' ]
2526- assert config ['mcpServers' ]['test-server' ]['type' ] == 'stdio'
2527- assert config ['mcpServers' ]['test-server' ]['command' ] == 'npx test-server'
2527+ server_config = config ['mcpServers' ]['test-server' ]
2528+ assert server_config ['type' ] == 'stdio'
2529+ # Verify command + args format (Windows wraps npx with cmd /c)
2530+ if platform .system () == 'Windows' :
2531+ assert server_config ['command' ] == 'cmd'
2532+ assert server_config ['args' ] == ['/c' , 'npx' , 'test-server' ]
2533+ else :
2534+ assert server_config ['command' ] == 'npx'
2535+ assert server_config ['args' ] == ['test-server' ]
2536+ # Verify env object present for format consistency
2537+ assert server_config ['env' ] == {}
25282538
25292539 def test_create_mcp_config_file_multiple_servers (self ) -> None :
25302540 """Test creating MCP config file with multiple servers."""
@@ -2551,6 +2561,10 @@ def test_create_mcp_config_file_multiple_servers(self) -> None:
25512561 assert 'server1' in config ['mcpServers' ]
25522562 assert 'server2' in config ['mcpServers' ]
25532563 assert config ['mcpServers' ]['server1' ]['type' ] == 'stdio'
2564+ # Verify command + args format for stdio server
2565+ assert config ['mcpServers' ]['server1' ]['command' ] == 'uvx'
2566+ assert config ['mcpServers' ]['server1' ]['args' ] == ['server1' ]
2567+ assert config ['mcpServers' ]['server1' ]['env' ] == {}
25542568 assert config ['mcpServers' ]['server2' ]['type' ] == 'http'
25552569 assert config ['mcpServers' ]['server2' ]['url' ] == 'http://localhost:8080'
25562570
@@ -3502,3 +3516,166 @@ def test_stale_profile_config_removed_when_combined_scope_removed(self) -> None:
35023516 assert success_flag is True
35033517 assert len (profile_servers ) == 0
35043518 assert not config_path .exists ()
3519+
3520+
3521+ class TestParseMcpCommand :
3522+ """Test parse_mcp_command() function for MCP config formatting."""
3523+
3524+ # === Basic Parsing Tests ===
3525+
3526+ def test_simple_command_no_args (self ) -> None :
3527+ """Test simple executable without arguments."""
3528+ result = setup_environment .parse_mcp_command ('bash' )
3529+ assert result ['command' ] == 'bash'
3530+ assert result ['args' ] == []
3531+
3532+ def test_command_with_single_arg (self ) -> None :
3533+ """Test command with single argument."""
3534+ result = setup_environment .parse_mcp_command ('uvx server' )
3535+ assert result ['command' ] == 'uvx'
3536+ assert result ['args' ] == ['server' ]
3537+
3538+ def test_command_with_multiple_args (self ) -> None :
3539+ """Test command with multiple arguments."""
3540+ result = setup_environment .parse_mcp_command ('npx -y @package/name' )
3541+ # Result depends on platform (Windows wraps with cmd /c)
3542+ if platform .system () == 'Windows' :
3543+ assert result ['command' ] == 'cmd'
3544+ assert result ['args' ] == ['/c' , 'npx' , '-y' , '@package/name' ]
3545+ else :
3546+ assert result ['command' ] == 'npx'
3547+ assert result ['args' ] == ['-y' , '@package/name' ]
3548+
3549+ # === Tilde Expansion Tests ===
3550+
3551+ def test_tilde_expansion_in_path_arg (self ) -> None :
3552+ """Test tilde path expansion in argument."""
3553+ home = str (Path .home ())
3554+ result = setup_environment .parse_mcp_command ('bash ~/.claude/script.sh' )
3555+ assert result ['command' ] == 'bash'
3556+ # Path should be expanded and POSIX-formatted
3557+ assert len (result ['args' ]) == 1
3558+ assert '~' not in result ['args' ][0 ]
3559+ assert home .replace ('\\ ' , '/' ) in result ['args' ][0 ] or home in result ['args' ][0 ]
3560+
3561+ def test_tilde_expansion_in_executable (self ) -> None :
3562+ """Test tilde path expansion when executable has tilde."""
3563+ home = str (Path .home ())
3564+ result = setup_environment .parse_mcp_command ('~/scripts/server.sh --port 8080' )
3565+ assert '~' not in result ['command' ]
3566+ assert home .replace ('\\ ' , '/' ) in result ['command' ] or home in result ['command' ]
3567+
3568+ def test_multiple_tilde_paths (self ) -> None :
3569+ """Test multiple tilde paths in single command."""
3570+ result = setup_environment .parse_mcp_command ('bash ~/.claude/script.sh ~/.config/settings.json' )
3571+ assert result ['command' ] == 'bash'
3572+ assert len (result ['args' ]) == 2
3573+ for arg in result ['args' ]:
3574+ assert '~' not in arg
3575+
3576+ # === Windows npx/npm Handling Tests ===
3577+
3578+ @pytest .mark .skipif (platform .system () != 'Windows' , reason = 'Windows-specific test' )
3579+ def test_windows_npx_wrapped_with_cmd (self ) -> None :
3580+ """Test npx command is wrapped with cmd /c on Windows."""
3581+ result = setup_environment .parse_mcp_command ('npx -y @mobilenext/mobile-mcp' )
3582+ assert result ['command' ] == 'cmd'
3583+ assert result ['args' ][0 ] == '/c'
3584+ assert result ['args' ][1 ] == 'npx'
3585+ assert '-y' in result ['args' ]
3586+ assert '@mobilenext/mobile-mcp' in result ['args' ]
3587+
3588+ @pytest .mark .skipif (platform .system () != 'Windows' , reason = 'Windows-specific test' )
3589+ def test_windows_npm_wrapped_with_cmd (self ) -> None :
3590+ """Test npm command is wrapped with cmd /c on Windows."""
3591+ result = setup_environment .parse_mcp_command ('npm exec server' )
3592+ assert result ['command' ] == 'cmd'
3593+ assert result ['args' ][0 ] == '/c'
3594+ assert result ['args' ][1 ] == 'npm'
3595+
3596+ @pytest .mark .skipif (platform .system () != 'Windows' , reason = 'Windows-specific test' )
3597+ def test_windows_non_npm_command_not_wrapped (self ) -> None :
3598+ """Test non-npm commands are NOT wrapped on Windows."""
3599+ result = setup_environment .parse_mcp_command ('uvx my-server' )
3600+ assert result ['command' ] == 'uvx'
3601+ assert result ['args' ] == ['my-server' ]
3602+
3603+ @pytest .mark .skipif (platform .system () == 'Windows' , reason = 'Non-Windows test' )
3604+ def test_unix_npx_not_wrapped (self ) -> None :
3605+ """Test npx is NOT wrapped on Unix systems."""
3606+ result = setup_environment .parse_mcp_command ('npx -y @mobilenext/mobile-mcp' )
3607+ assert result ['command' ] == 'npx'
3608+ assert '-y' in result ['args' ]
3609+
3610+ # === POSIX Path Conversion Tests ===
3611+
3612+ @pytest .mark .skipif (platform .system () != 'Windows' , reason = 'Windows-specific test' )
3613+ def test_windows_path_converted_to_posix (self ) -> None :
3614+ """Test Windows backslash paths converted to POSIX forward slashes."""
3615+ # Use a path with backslashes that expand_tildes_in_command would return
3616+ with patch ('setup_environment.expand_tildes_in_command' , return_value = r'bash C:\Users\test\script.sh' ):
3617+ result = setup_environment .parse_mcp_command (r'bash C:\Users\test\script.sh' )
3618+ assert result ['command' ] == 'bash'
3619+ assert '\\ ' not in result ['args' ][0 ]
3620+ assert '/' in result ['args' ][0 ]
3621+
3622+ def test_posix_path_unchanged (self ) -> None :
3623+ """Test POSIX paths remain unchanged."""
3624+ result = setup_environment .parse_mcp_command ('bash /usr/local/bin/script.sh' )
3625+ assert result ['command' ] == 'bash'
3626+ assert result ['args' ] == ['/usr/local/bin/script.sh' ]
3627+
3628+ # === Quoted Arguments Tests ===
3629+
3630+ def test_quoted_arg_with_spaces (self ) -> None :
3631+ """Test quoted argument with spaces is preserved."""
3632+ result = setup_environment .parse_mcp_command ('bash -c "echo hello world"' )
3633+ assert result ['command' ] == 'bash'
3634+ assert '-c' in result ['args' ]
3635+ assert 'echo hello world' in result ['args' ]
3636+
3637+ def test_single_quoted_arg (self ) -> None :
3638+ """Test single-quoted argument is preserved."""
3639+ result = setup_environment .parse_mcp_command ("bash -c 'echo test'" )
3640+ assert result ['command' ] == 'bash'
3641+ assert '-c' in result ['args' ]
3642+ assert 'echo test' in result ['args' ]
3643+
3644+ # === Edge Cases ===
3645+
3646+ def test_empty_command (self ) -> None :
3647+ """Test empty command string."""
3648+ result = setup_environment .parse_mcp_command ('' )
3649+ assert result ['command' ] == ''
3650+ assert result ['args' ] == []
3651+
3652+ def test_whitespace_only_command (self ) -> None :
3653+ """Test whitespace-only command string."""
3654+ result = setup_environment .parse_mcp_command (' ' )
3655+ # shlex.split of whitespace returns []
3656+ assert result ['command' ] == ' ' # Falls back to original
3657+ assert result ['args' ] == []
3658+
3659+ def test_malformed_quotes_fallback (self ) -> None :
3660+ """Test malformed quotes fall back to split()."""
3661+ result = setup_environment .parse_mcp_command ('bash "unclosed quote' )
3662+ # Should not crash, falls back to simple split
3663+ assert result ['command' ] == 'bash'
3664+ assert '"unclosed' in result ['args' ] or 'quote' in result ['args' ]
3665+
3666+ def test_special_characters_in_args (self ) -> None :
3667+ """Test special characters in arguments."""
3668+ result = setup_environment .parse_mcp_command ('npx @scope/package-name:1.0.0' )
3669+ if platform .system () == 'Windows' :
3670+ assert '@scope/package-name:1.0.0' in result ['args' ]
3671+ else :
3672+ assert result ['args' ] == ['@scope/package-name:1.0.0' ]
3673+
3674+ def test_non_path_arg_unchanged (self ) -> None :
3675+ """Test non-path arguments are not modified."""
3676+ result = setup_environment .parse_mcp_command ('server --port 8080 --host localhost' )
3677+ assert result ['command' ] == 'server'
3678+ assert '--port' in result ['args' ]
3679+ assert '8080' in result ['args' ]
3680+ assert '--host' in result ['args' ]
3681+ assert 'localhost' in result ['args' ]
0 commit comments