Skip to content

Commit 938c1b2

Browse files
authored
Merge pull request #242 from alex-feel/alex-feel-dev
Resolve MCP server configuration retry missing PATH export
2 parents de36f71 + 64b3cde commit 938c1b2

File tree

2 files changed

+121
-6
lines changed

2 files changed

+121
-6
lines changed

scripts/setup_environment.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,27 @@ def is_running_in_pytest() -> bool:
6666
return 'pytest' in sys.modules or 'py.test' in sys.argv[0]
6767

6868

69+
def is_debug_enabled() -> bool:
70+
"""Check if debug logging is enabled via environment variable.
71+
72+
Returns:
73+
True if CLAUDE_CODE_TOOLBOX_DEBUG is set to '1', 'true', or 'yes' (case-insensitive)
74+
"""
75+
debug_value = os.environ.get('CLAUDE_CODE_TOOLBOX_DEBUG', '').lower()
76+
return debug_value in ('1', 'true', 'yes')
77+
78+
79+
def debug_log(message: str) -> None:
80+
"""Log debug message if debug mode is enabled.
81+
82+
Args:
83+
message: Debug message to log
84+
"""
85+
if is_debug_enabled():
86+
# Use distinct prefix for easy filtering
87+
print(f' [DEBUG] {message}', file=sys.stderr)
88+
89+
6990
# Windows UAC elevation helper functions
7091
def is_admin() -> bool:
7192
"""Check if running with admin privileges on Windows.
@@ -457,9 +478,13 @@ def find_bash_windows() -> str | None:
457478
Prioritizes Git Bash locations over PATH search to avoid
458479
accidentally finding WSL's bash.exe at C:\\Windows\\System32.
459480
"""
481+
debug_log('find_bash_windows() called')
482+
460483
# Check CLAUDE_CODE_GIT_BASH_PATH env var first
461484
env_path = os.environ.get('CLAUDE_CODE_GIT_BASH_PATH')
485+
debug_log(f'CLAUDE_CODE_GIT_BASH_PATH={env_path}')
462486
if env_path and Path(env_path).exists():
487+
debug_log(f'Found via env var: {env_path}')
463488
return str(Path(env_path).resolve())
464489

465490
# Check Git Bash common locations FIRST (before PATH search)
@@ -473,20 +498,28 @@ def find_bash_windows() -> str | None:
473498
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Git\usr\bin\bash.exe'),
474499
]
475500

476-
for path in common_paths:
501+
for i, path in enumerate(common_paths):
477502
expanded = os.path.expandvars(path)
478-
if Path(expanded).exists():
503+
exists = Path(expanded).exists()
504+
debug_log(f'Common path [{i}]: {expanded} - exists={exists}')
505+
if exists:
506+
debug_log(f'Found via common path: {expanded}')
479507
return str(Path(expanded).resolve())
480508

481509
# Fall back to PATH search (may find Git Bash if installed elsewhere)
482510
bash_path = find_command('bash.exe')
511+
debug_log(f'PATH search result: {bash_path}')
483512
if bash_path:
484513
# Skip WSL bash in System32/SysWOW64
485514
bash_lower = bash_path.lower()
486-
if 'system32' not in bash_lower and 'syswow64' not in bash_lower:
515+
is_wsl = 'system32' in bash_lower or 'syswow64' in bash_lower
516+
debug_log(f'Is WSL bash: {is_wsl}')
517+
if not is_wsl:
518+
debug_log(f'Returning PATH bash: {bash_path}')
487519
return bash_path
488-
# WSL bash found - don't use it, return None instead
520+
debug_log('Skipping WSL bash')
489521

522+
debug_log('No suitable bash found, returning None')
490523
return None
491524

492525

@@ -508,23 +541,42 @@ def run_bash_command(
508541
Returns:
509542
subprocess.CompletedProcess with the result
510543
"""
544+
debug_log('run_bash_command() called')
545+
cmd_preview = command[:200] + '...' if len(command) > 200 else command
546+
debug_log(f' command: {cmd_preview}')
547+
debug_log(f' capture_output: {capture_output}')
548+
debug_log(f' login_shell: {login_shell}')
549+
511550
if sys.platform == 'win32':
512551
bash_path = find_bash_windows()
513552
else:
514553
bash_path = shutil.which('bash')
515554

555+
debug_log(f'bash_path resolved to: {bash_path}')
556+
516557
if not bash_path:
517558
error('Bash not found!')
559+
debug_log('ERROR: Returning early - bash not found')
518560
return subprocess.CompletedProcess([], 1, '', 'bash not found')
519561

520562
args = [bash_path]
521563
if login_shell:
522564
args.append('-l')
523565
args.extend(['-c', command])
524566

567+
debug_log(f'Executing: {args}')
568+
525569
try:
526-
return subprocess.run(args, capture_output=capture_output, text=True)
527-
except FileNotFoundError:
570+
result = subprocess.run(args, capture_output=capture_output, text=True)
571+
debug_log(f'Exit code: {result.returncode}')
572+
if capture_output:
573+
stdout_preview = result.stdout[:500] if result.stdout else '(empty)'
574+
stderr_preview = result.stderr[:500] if result.stderr else '(empty)'
575+
debug_log(f'stdout: {stdout_preview}')
576+
debug_log(f'stderr: {stderr_preview}')
577+
return result
578+
except FileNotFoundError as e:
579+
debug_log(f'FileNotFoundError: {e}')
528580
return subprocess.CompletedProcess(args, 1, '', f'bash not found: {bash_path}')
529581

530582

@@ -3502,6 +3554,9 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
35023554
# Windows HTTP transport - use bash for consistent cross-platform behavior
35033555
# This eliminates PowerShell's exit code quirks and CMD escaping issues
35043556
if system == 'Windows':
3557+
debug_log(f'=== MCP Server Configuration: {name} ===')
3558+
debug_log(f'claude_cmd: {claude_cmd}')
3559+
35053560
# Build explicit PATH including Node.js location
35063561
nodejs_path = r'C:\Program Files\nodejs'
35073562
current_path = os.environ.get('PATH', '')
@@ -3516,6 +3571,10 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
35163571
unix_explicit_path = convert_path_env_to_unix(windows_explicit_path)
35173572
unix_claude_cmd = convert_to_unix_path(str(claude_cmd))
35183573

3574+
debug_log(f'unix_claude_cmd: {unix_claude_cmd}')
3575+
path_preview = unix_explicit_path[:200] + '...' if len(unix_explicit_path) > 200 else unix_explicit_path
3576+
debug_log(f'unix_explicit_path: {path_preview}')
3577+
35193578
env_flags = ' '.join(f'--env "{e}"' for e in env_list) if env_list else ''
35203579
env_part = f' {env_flags}' if env_flags else ''
35213580
header_part = f' --header "{header}"' if header else ''
@@ -3526,7 +3585,12 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
35263585
f'--transport {transport}{header_part} "{url}"'
35273586
)
35283587

3588+
bash_cmd_preview = bash_cmd[:300] + '...' if len(bash_cmd) > 300 else bash_cmd
3589+
debug_log(f'First attempt bash_cmd: {bash_cmd_preview}')
35293590
result = run_bash_command(bash_cmd, capture_output=True, login_shell=True)
3591+
debug_log(f'First attempt result: returncode={result.returncode}')
3592+
if result.returncode != 0:
3593+
debug_log(f'First attempt failed! stdout={result.stdout}, stderr={result.stderr}')
35303594
else:
35313595
# On Unix, use bash with updated PATH (consistent with Windows)
35323596
parent_dir = Path(claude_cmd).parent
@@ -3591,10 +3655,23 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
35913655
# Convert Windows path to Git Bash Unix-style path
35923656
unix_claude_cmd = convert_to_unix_path(str(claude_cmd))
35933657

3658+
# Build explicit PATH including Node.js location (same as first attempt)
3659+
nodejs_path = r'C:\Program Files\nodejs'
3660+
current_path = os.environ.get('PATH', '')
3661+
if Path(nodejs_path).exists() and nodejs_path not in current_path:
3662+
windows_explicit_path = f'{nodejs_path};{current_path}'
3663+
else:
3664+
windows_explicit_path = current_path
3665+
unix_explicit_path = convert_path_env_to_unix(windows_explicit_path)
3666+
3667+
# Include PATH export in retry (same as first attempt)
35943668
bash_cmd = (
3669+
f'export PATH="{unix_explicit_path}:$PATH" && '
35953670
f'"{unix_claude_cmd}" mcp add --scope {scope} {name}{env_part_retry} '
35963671
f'--transport {transport}{header_part} "{url}"'
35973672
)
3673+
bash_cmd_preview = bash_cmd[:300] + '...' if len(bash_cmd) > 300 else bash_cmd
3674+
debug_log(f'Retry bash_cmd: {bash_cmd_preview}')
35983675
info(f'Retrying with bash command: {bash_cmd}')
35993676
result = run_bash_command(bash_cmd, capture_output=False)
36003677
else:

tests/test_setup_environment.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,44 @@ def test_header(self, capsys):
8181
assert 'Claude Code Python Environment Setup' in captured.out
8282

8383

84+
class TestDebugFunctions:
85+
"""Test debug logging functions."""
86+
87+
@pytest.mark.parametrize('env_value', ['1', 'true', 'yes', 'TRUE', 'Yes', 'TrUe'])
88+
def test_is_debug_enabled_true(self, env_value):
89+
"""Test that '1', 'true', 'yes' (case-insensitive) all return True."""
90+
with patch.dict('os.environ', {'CLAUDE_CODE_TOOLBOX_DEBUG': env_value}):
91+
assert setup_environment.is_debug_enabled() is True
92+
93+
@pytest.mark.parametrize('env_value', ['', '0', 'false', 'no', 'FALSE', 'No', 'anything'])
94+
def test_is_debug_enabled_false(self, env_value):
95+
"""Test that '', '0', 'false', 'no', and other values return False."""
96+
with patch.dict('os.environ', {'CLAUDE_CODE_TOOLBOX_DEBUG': env_value}):
97+
assert setup_environment.is_debug_enabled() is False
98+
99+
def test_is_debug_enabled_unset(self):
100+
"""Test that unset environment variable returns False."""
101+
with patch.dict('os.environ', {}, clear=True):
102+
# Remove the key if it exists
103+
os.environ.pop('CLAUDE_CODE_TOOLBOX_DEBUG', None)
104+
assert setup_environment.is_debug_enabled() is False
105+
106+
def test_debug_log_outputs_when_enabled(self, capsys):
107+
"""Test that debug_log outputs to stderr when debug is enabled."""
108+
with patch.dict('os.environ', {'CLAUDE_CODE_TOOLBOX_DEBUG': '1'}):
109+
setup_environment.debug_log('Test debug message')
110+
captured = capsys.readouterr()
111+
assert ' [DEBUG] Test debug message' in captured.err
112+
113+
def test_debug_log_silent_when_disabled(self, capsys):
114+
"""Test that debug_log produces no output when debug is disabled."""
115+
with patch.dict('os.environ', {'CLAUDE_CODE_TOOLBOX_DEBUG': '0'}):
116+
setup_environment.debug_log('Test debug message')
117+
captured = capsys.readouterr()
118+
assert captured.err == ''
119+
assert captured.out == ''
120+
121+
84122
class TestUtilityFunctions:
85123
"""Test utility functions."""
86124

0 commit comments

Comments
 (0)