Skip to content

Commit d4a2d4a

Browse files
authored
Merge pull request #289 from alex-feel/alex-feel-dev
Expand tildes in user-scope STDIO MCP commands on Windows
2 parents f52ba3a + 6aee29a commit d4a2d4a

File tree

2 files changed

+300
-1
lines changed

2 files changed

+300
-1
lines changed

scripts/setup_environment.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4229,7 +4229,10 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
42294229

42304230
# Build command string for STDIO
42314231
# npx needs cmd /c wrapper on Windows even in bash
4232-
command_str = f'cmd /c {command}' if 'npx' in command else command
4232+
# Expand tildes using Python (produces C:\Users\...) and convert to forward slashes
4233+
# This prevents Git Bash from expanding ~ to /c/Users/... (Unix format)
4234+
expanded_command = expand_tildes_in_command(command).replace('\\', '/')
4235+
command_str = f'cmd /c {expanded_command}' if 'npx' in expanded_command else expanded_command
42334236

42344237
bash_cmd = (
42354238
f'export PATH="{unix_explicit_path}:$PATH" && '

tests/test_setup_environment.py

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,6 +1600,302 @@ def test_configure_mcp_server_windows_prefers_shell_script_over_cmd(
16001600
assert '/claude"' in bash_cmd or "/claude'" in bash_cmd or 'claude" mcp' in bash_cmd
16011601

16021602

1603+
class TestMCPTildeExpansionWindows:
1604+
"""Test tilde expansion in user-scope STDIO MCP commands on Windows.
1605+
1606+
Validates that tildes in commands are expanded using Python's os.path.expanduser()
1607+
before being passed to Git Bash, preventing bash from expanding ~ to Unix-style
1608+
/c/Users/... paths.
1609+
"""
1610+
1611+
@patch('platform.system', return_value='Windows')
1612+
@patch('setup_environment.expand_tildes_in_command')
1613+
@patch('setup_environment.run_bash_command')
1614+
@patch('setup_environment.run_command')
1615+
@patch('setup_environment.find_command_robust')
1616+
def test_configure_mcp_server_stdio_windows_expands_tilde_in_command(
1617+
self, mock_find, mock_run_cmd, mock_bash_cmd, mock_expand_tilde, mock_system,
1618+
):
1619+
"""Verify tildes in user-scope STDIO commands are expanded to Windows paths.
1620+
1621+
Ensures that tildes are expanded using Python's os.path.expanduser() which
1622+
produces Windows paths (C:\\Users\\...) rather than letting Git Bash expand
1623+
them to Unix-style paths (/c/Users/...).
1624+
"""
1625+
del mock_system # Unused but required for patch
1626+
mock_find.return_value = 'C:\\Users\\Test\\AppData\\Roaming\\npm\\claude.CMD'
1627+
mock_run_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1628+
mock_bash_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1629+
# Mock expand_tildes_in_command to return Windows-style expanded path
1630+
mock_expand_tilde.return_value = 'python C:\\Users\\test\\.claude\\mcp\\mcp_wrapper.py'
1631+
1632+
server = {
1633+
'name': 'tilde-test-server',
1634+
'scope': 'user',
1635+
'command': 'python ~/.claude/mcp/mcp_wrapper.py',
1636+
}
1637+
1638+
result = setup_environment.configure_mcp_server(server)
1639+
1640+
assert result is True
1641+
# Verify expand_tildes_in_command was called with the original command
1642+
mock_expand_tilde.assert_called_once_with('python ~/.claude/mcp/mcp_wrapper.py')
1643+
# Verify bash command contains expanded path with forward slashes
1644+
bash_cmd = mock_bash_cmd.call_args.args[0]
1645+
assert 'C:/Users/test/.claude/mcp/mcp_wrapper.py' in bash_cmd
1646+
# Verify no unexpanded tilde in command portion (after '--')
1647+
command_part = bash_cmd.split('-- ')[1] if '-- ' in bash_cmd else bash_cmd
1648+
assert '~' not in command_part
1649+
1650+
@patch('platform.system', return_value='Windows')
1651+
@patch('setup_environment.expand_tildes_in_command')
1652+
@patch('setup_environment.run_bash_command')
1653+
@patch('setup_environment.run_command')
1654+
@patch('setup_environment.find_command_robust')
1655+
def test_configure_mcp_server_stdio_windows_npx_with_tilde_expands(
1656+
self, mock_find, mock_run_cmd, mock_bash_cmd, mock_expand_tilde, mock_system,
1657+
):
1658+
"""Verify npx commands with tildes are expanded AND wrapped with cmd /c.
1659+
1660+
npx commands on Windows require both tilde expansion AND cmd /c wrapper
1661+
for proper execution in Git Bash.
1662+
"""
1663+
del mock_system # Unused but required for patch
1664+
mock_find.return_value = 'C:\\Users\\Test\\AppData\\Roaming\\npm\\claude.CMD'
1665+
mock_run_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1666+
mock_bash_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1667+
mock_expand_tilde.return_value = 'npx C:\\Users\\test\\.claude\\mcp\\server-script'
1668+
1669+
server = {
1670+
'name': 'npx-tilde-server',
1671+
'scope': 'user',
1672+
'command': 'npx ~/.claude/mcp/server-script',
1673+
}
1674+
1675+
result = setup_environment.configure_mcp_server(server)
1676+
1677+
assert result is True
1678+
bash_cmd = mock_bash_cmd.call_args.args[0]
1679+
# Verify cmd /c wrapper with expanded path
1680+
assert 'cmd /c npx C:/Users/test/.claude/mcp/server-script' in bash_cmd
1681+
# Verify no unexpanded tilde in the command part
1682+
command_part = bash_cmd.split('-- ')[1] if '-- ' in bash_cmd else bash_cmd
1683+
assert '~' not in command_part
1684+
1685+
@patch('platform.system', return_value='Windows')
1686+
@patch('setup_environment.expand_tildes_in_command')
1687+
@patch('setup_environment.run_bash_command')
1688+
@patch('setup_environment.run_command')
1689+
@patch('setup_environment.find_command_robust')
1690+
def test_configure_mcp_server_stdio_windows_no_tilde_unchanged(
1691+
self, mock_find, mock_run_cmd, mock_bash_cmd, mock_expand_tilde, mock_system,
1692+
):
1693+
"""Verify commands without tildes are not modified (regression test).
1694+
1695+
Commands that don't contain tildes should pass through unchanged to
1696+
ensure backward compatibility.
1697+
"""
1698+
del mock_system # Unused but required for patch
1699+
mock_find.return_value = 'C:\\Users\\Test\\AppData\\Roaming\\npm\\claude.CMD'
1700+
mock_run_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1701+
mock_bash_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1702+
# Command without tilde returns unchanged
1703+
mock_expand_tilde.return_value = 'uvx mcp-server-package'
1704+
1705+
server = {
1706+
'name': 'no-tilde-server',
1707+
'scope': 'user',
1708+
'command': 'uvx mcp-server-package',
1709+
}
1710+
1711+
result = setup_environment.configure_mcp_server(server)
1712+
1713+
assert result is True
1714+
mock_expand_tilde.assert_called_once_with('uvx mcp-server-package')
1715+
bash_cmd = mock_bash_cmd.call_args.args[0]
1716+
assert 'uvx mcp-server-package' in bash_cmd
1717+
1718+
@patch('platform.system', return_value='Windows')
1719+
@patch('setup_environment.run_bash_command')
1720+
@patch('setup_environment.run_command')
1721+
@patch('setup_environment.find_command_robust')
1722+
def test_configure_mcp_server_profile_scope_unchanged(
1723+
self, mock_find, mock_run_cmd, mock_bash_cmd, mock_system,
1724+
):
1725+
"""Verify profile-scope servers return early without STDIO add operation.
1726+
1727+
Profile-scope servers are configured via --strict-mcp-config, not claude mcp add.
1728+
On Windows, removals use run_bash_command, then the function returns early.
1729+
"""
1730+
del mock_system, mock_run_cmd # Unused but required for patch
1731+
mock_find.return_value = 'C:\\Users\\Test\\AppData\\Roaming\\npm\\claude.CMD'
1732+
mock_bash_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1733+
1734+
server = {
1735+
'name': 'profile-server',
1736+
'scope': 'profile',
1737+
'command': 'python ~/.claude/mcp/script.py',
1738+
}
1739+
1740+
result = setup_environment.configure_mcp_server(server)
1741+
1742+
assert result is True
1743+
# On Windows, removals use run_bash_command (3 calls: user, local, project)
1744+
# Then profile scope returns early - no add operation
1745+
assert mock_bash_cmd.call_count == 3
1746+
# All bash calls should be removal commands, not 'mcp add'
1747+
for call in mock_bash_cmd.call_args_list:
1748+
bash_cmd = call.args[0]
1749+
assert 'mcp remove' in bash_cmd
1750+
assert 'mcp add' not in bash_cmd
1751+
1752+
@patch('platform.system', return_value='Windows')
1753+
@patch('setup_environment.expand_tildes_in_command')
1754+
@patch('setup_environment.run_bash_command')
1755+
@patch('setup_environment.run_command')
1756+
@patch('setup_environment.find_command_robust')
1757+
def test_configure_mcp_server_stdio_windows_multiple_tildes_expanded(
1758+
self, mock_find, mock_run_cmd, mock_bash_cmd, mock_expand_tilde, mock_system,
1759+
):
1760+
"""Verify commands with multiple tilde paths all get expanded.
1761+
1762+
When a command contains multiple tilde references (e.g., script path
1763+
and config path), all should be expanded.
1764+
"""
1765+
del mock_system # Unused but required for patch
1766+
mock_find.return_value = 'C:\\Users\\Test\\AppData\\Roaming\\npm\\claude.CMD'
1767+
mock_run_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1768+
mock_bash_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1769+
# Both tildes expanded
1770+
mock_expand_tilde.return_value = (
1771+
'python C:\\Users\\test\\.claude\\mcp\\script.py '
1772+
'--config C:\\Users\\test\\.config\\mcp.yaml'
1773+
)
1774+
1775+
server = {
1776+
'name': 'multi-tilde-server',
1777+
'scope': 'user',
1778+
'command': 'python ~/.claude/mcp/script.py --config ~/.config/mcp.yaml',
1779+
}
1780+
1781+
result = setup_environment.configure_mcp_server(server)
1782+
1783+
assert result is True
1784+
bash_cmd = mock_bash_cmd.call_args.args[0]
1785+
# Verify both paths expanded with forward slashes
1786+
assert 'C:/Users/test/.claude/mcp/script.py' in bash_cmd
1787+
assert 'C:/Users/test/.config/mcp.yaml' in bash_cmd
1788+
# No unexpanded tildes in command portion
1789+
command_part = bash_cmd.split('-- ')[1] if '-- ' in bash_cmd else bash_cmd
1790+
assert '~' not in command_part
1791+
1792+
@patch('platform.system', return_value='Windows')
1793+
@patch('setup_environment.expand_tildes_in_command')
1794+
@patch('setup_environment.run_bash_command')
1795+
@patch('setup_environment.run_command')
1796+
@patch('setup_environment.find_command_robust')
1797+
def test_configure_mcp_server_stdio_windows_backslash_to_forward_slash(
1798+
self, mock_find, mock_run_cmd, mock_bash_cmd, mock_expand_tilde, mock_system,
1799+
):
1800+
"""Verify backslashes from os.path.expanduser() are converted to forward slashes.
1801+
1802+
Windows os.path.expanduser() returns backslashes (C:\\Users\\...) which must
1803+
be converted to forward slashes for consistent cross-platform paths.
1804+
"""
1805+
del mock_system # Unused but required for patch
1806+
mock_find.return_value = 'C:\\Users\\Test\\AppData\\Roaming\\npm\\claude.CMD'
1807+
mock_run_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1808+
mock_bash_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1809+
# Simulate os.path.expanduser returning backslashes
1810+
mock_expand_tilde.return_value = 'python C:\\Users\\test\\.claude\\script.py'
1811+
1812+
server = {
1813+
'name': 'backslash-test-server',
1814+
'scope': 'user',
1815+
'command': 'python ~/.claude/script.py',
1816+
}
1817+
1818+
result = setup_environment.configure_mcp_server(server)
1819+
1820+
assert result is True
1821+
bash_cmd = mock_bash_cmd.call_args.args[0]
1822+
# Extract command portion after '--'
1823+
command_part = bash_cmd.split('-- ')[1] if '-- ' in bash_cmd else bash_cmd
1824+
# Verify no backslashes in command part
1825+
assert '\\' not in command_part
1826+
# Verify forward slashes are used
1827+
assert 'C:/Users/test/.claude/script.py' in bash_cmd
1828+
1829+
@patch('platform.system', return_value='Windows')
1830+
@patch('setup_environment.expand_tildes_in_command')
1831+
@patch('setup_environment.run_bash_command')
1832+
@patch('setup_environment.run_command')
1833+
@patch('setup_environment.find_command_robust')
1834+
def test_configure_mcp_server_local_scope_stdio_with_tilde(
1835+
self, mock_find, mock_run_cmd, mock_bash_cmd, mock_expand_tilde, mock_system,
1836+
):
1837+
"""Verify local-scope also gets tilde expansion on Windows.
1838+
1839+
Local-scope servers use the same STDIO code path as user-scope
1840+
and should receive the same tilde expansion treatment.
1841+
"""
1842+
del mock_system # Unused but required for patch
1843+
mock_find.return_value = 'C:\\Users\\Test\\AppData\\Roaming\\npm\\claude.CMD'
1844+
mock_run_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1845+
mock_bash_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1846+
mock_expand_tilde.return_value = 'python C:\\Users\\test\\.claude\\mcp\\local_server.py'
1847+
1848+
server = {
1849+
'name': 'local-tilde-server',
1850+
'scope': 'local',
1851+
'command': 'python ~/.claude/mcp/local_server.py',
1852+
}
1853+
1854+
result = setup_environment.configure_mcp_server(server)
1855+
1856+
assert result is True
1857+
bash_cmd = mock_bash_cmd.call_args.args[0]
1858+
# Tilde expanded and backslashes converted
1859+
assert 'C:/Users/test/.claude/mcp/local_server.py' in bash_cmd
1860+
# No unexpanded tilde in command portion
1861+
command_part = bash_cmd.split('-- ')[1] if '-- ' in bash_cmd else bash_cmd
1862+
assert '~' not in command_part
1863+
1864+
@patch('platform.system', return_value='Windows')
1865+
@patch('setup_environment.expand_tildes_in_command')
1866+
@patch('setup_environment.run_bash_command')
1867+
@patch('setup_environment.run_command')
1868+
@patch('setup_environment.find_command_robust')
1869+
def test_configure_mcp_server_combined_scope_with_tilde(
1870+
self, mock_find, mock_run_cmd, mock_bash_cmd, mock_expand_tilde, mock_system,
1871+
):
1872+
"""Verify combined scope servers with tildes work correctly.
1873+
1874+
When scope is [user, profile], the user-scope add command should
1875+
have expanded tilde, while profile servers are returned for separate handling.
1876+
"""
1877+
del mock_system # Unused but required for patch
1878+
mock_find.return_value = 'C:\\Users\\Test\\AppData\\Roaming\\npm\\claude.CMD'
1879+
mock_run_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1880+
mock_bash_cmd.return_value = subprocess.CompletedProcess([], 0, '', '')
1881+
mock_expand_tilde.return_value = 'python C:\\Users\\test\\.claude\\mcp\\combined.py'
1882+
1883+
server = {
1884+
'name': 'combined-tilde-server',
1885+
'scope': ['user', 'profile'],
1886+
'command': 'python ~/.claude/mcp/combined.py',
1887+
}
1888+
1889+
result = setup_environment.configure_mcp_server(server)
1890+
1891+
assert result is True
1892+
# expand_tildes_in_command should be called for user-scope add
1893+
mock_expand_tilde.assert_called()
1894+
# Verify user-scope bash command has expanded path
1895+
bash_cmd = mock_bash_cmd.call_args.args[0]
1896+
assert 'C:/Users/test/.claude/mcp/combined.py' in bash_cmd
1897+
1898+
16031899
class TestCreateAdditionalSettings:
16041900
"""Test additional settings creation."""
16051901

0 commit comments

Comments
 (0)