Skip to content

Commit 7c17947

Browse files
authored
Merge pull request #228 from alex-feel/alex-feel-dev
Quote HTTP MCP server URLs in shell commands
2 parents 5a2aa56 + 4f475ca commit 7c17947

File tree

3 files changed

+83
-3
lines changed

3 files changed

+83
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
### IDE
22
.idea
33
.claude
4+
.serena
45

56
### Python template
67
# Byte-compiled / optimized / DLL files

scripts/setup_environment.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import os
1717
import platform
1818
import re
19+
import shlex
1920
import shutil
2021
import ssl
2122
import subprocess
@@ -3359,13 +3360,13 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
33593360

33603361
ps_script = f'''
33613362
$env:Path = "{explicit_path}"
3362-
& "{claude_cmd}" mcp add --scope {scope} {name}{env_part} --transport {transport} {url}
3363+
& "{claude_cmd}" mcp add --scope {scope} {name}{env_part} --transport {transport} "{url}"
33633364
exit $LASTEXITCODE
33643365
'''
33653366
if header:
33663367
ps_script = f'''
33673368
$env:Path = "{explicit_path}"
3368-
& "{claude_cmd}" mcp add --scope {scope} {name}{env_part} --transport {transport} --header "{header}" {url}
3369+
& "{claude_cmd}" mcp add --scope {scope} {name}{env_part} --transport {transport} --header "{header}" "{url}"
33693370
exit $LASTEXITCODE
33703371
'''
33713372
result = run_command(
@@ -3385,7 +3386,7 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
33853386
else:
33863387
# On Unix, spawn new bash with updated PATH
33873388
parent_dir = Path(claude_cmd).parent
3388-
bash_cmd = f'export PATH="{parent_dir}:$PATH" && {" ".join(base_cmd)}'
3389+
bash_cmd = f'export PATH="{parent_dir}:$PATH" && ' + ' '.join(shlex.quote(str(arg)) for arg in base_cmd)
33893390
result = run_command(
33903391
[
33913392
'bash',

tests/test_setup_environment.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,84 @@ def test_configure_mcp_server_stdio(self, mock_run, mock_find):
10311031
# Should call run_command 4 times: 3 for removing from all scopes, once for add
10321032
assert mock_run.call_count == 4
10331033

1034+
@patch('setup_environment.find_command_robust')
1035+
@patch('setup_environment.run_command')
1036+
def test_configure_mcp_server_http_with_ampersand_in_url(self, mock_run, mock_find):
1037+
"""Test configuring HTTP MCP server with URL containing ampersand.
1038+
1039+
This tests the fix for the bug where URLs with '&' query parameters
1040+
were incorrectly interpreted as command separators in Windows CMD/PowerShell.
1041+
"""
1042+
mock_find.return_value = 'claude'
1043+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
1044+
1045+
server = {
1046+
'name': 'supabase',
1047+
'scope': 'user',
1048+
'transport': 'http',
1049+
'url': 'https://mcp.supabase.com/mcp?project_ref=xxx&read_only=true',
1050+
}
1051+
1052+
result = setup_environment.configure_mcp_server(server)
1053+
assert result is True
1054+
# Should call run_command 4 times: 3 for removing from all scopes, once for add
1055+
assert mock_run.call_count == 4
1056+
# Check the last call (add command) contains the full URL
1057+
add_cmd_str = ' '.join(str(arg) for arg in mock_run.call_args_list[3][0][0])
1058+
assert 'mcp add' in add_cmd_str
1059+
assert 'supabase' in add_cmd_str
1060+
# The URL should be in the command (not split by &)
1061+
assert 'project_ref=xxx' in add_cmd_str
1062+
assert 'read_only=true' in add_cmd_str
1063+
1064+
@patch('setup_environment.find_command_robust')
1065+
@patch('setup_environment.run_command')
1066+
def test_configure_mcp_server_http_with_multiple_query_params(self, mock_run, mock_find):
1067+
"""Test configuring HTTP MCP server with URL containing multiple special characters."""
1068+
mock_find.return_value = 'claude'
1069+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
1070+
1071+
server = {
1072+
'name': 'test-api',
1073+
'scope': 'user',
1074+
'transport': 'http',
1075+
'url': 'https://api.example.com/v1?key=abc&token=xyz&mode=read&format=json',
1076+
}
1077+
1078+
result = setup_environment.configure_mcp_server(server)
1079+
assert result is True
1080+
# Check all query parameters are preserved in the command
1081+
add_cmd_str = ' '.join(str(arg) for arg in mock_run.call_args_list[3][0][0])
1082+
assert 'key=abc' in add_cmd_str
1083+
assert 'token=xyz' in add_cmd_str
1084+
assert 'mode=read' in add_cmd_str
1085+
assert 'format=json' in add_cmd_str
1086+
1087+
@patch('setup_environment.find_command_robust')
1088+
@patch('setup_environment.run_command')
1089+
def test_configure_mcp_server_http_with_header_and_special_url(self, mock_run, mock_find):
1090+
"""Test configuring HTTP MCP server with header and URL containing special characters."""
1091+
mock_find.return_value = 'claude'
1092+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
1093+
1094+
server = {
1095+
'name': 'auth-api',
1096+
'scope': 'user',
1097+
'transport': 'http',
1098+
'url': 'https://api.example.com?client_id=123&scope=read&redirect_uri=http://localhost',
1099+
'header': 'Authorization: Bearer token123',
1100+
}
1101+
1102+
result = setup_environment.configure_mcp_server(server)
1103+
assert result is True
1104+
# Check the command contains both header and full URL
1105+
add_cmd_str = ' '.join(str(arg) for arg in mock_run.call_args_list[3][0][0])
1106+
assert 'mcp add' in add_cmd_str
1107+
assert 'auth-api' in add_cmd_str
1108+
assert '--header' in add_cmd_str
1109+
assert 'client_id=123' in add_cmd_str
1110+
assert 'scope=read' in add_cmd_str
1111+
10341112

10351113
class TestCreateAdditionalSettings:
10361114
"""Test additional settings creation."""

0 commit comments

Comments
 (0)