Skip to content

Commit 6835f9a

Browse files
authored
Merge pull request #277 from alex-feel/alex-feel-dev
Ensure profile MCP servers removed from all scopes before configuration
2 parents 13a5d41 + 44cb5ba commit 6835f9a

File tree

2 files changed

+99
-11
lines changed

2 files changed

+99
-11
lines changed

scripts/setup_environment.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3864,12 +3864,6 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
38643864
"""Configure a single MCP server."""
38653865
name = server.get('name')
38663866
scope = server.get('scope', 'user')
3867-
3868-
# Skip profile-scoped servers - they are configured via create_mcp_config_file()
3869-
if scope == 'profile':
3870-
info(f'MCP server {name} has scope: profile (will be configured via --strict-mcp-config)')
3871-
return True # Not an error, just handled elsewhere
3872-
38733867
transport = server.get('transport')
38743868
url = server.get('url')
38753869
command = server.get('command')
@@ -3925,6 +3919,11 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
39253919
else:
39263920
info(f'MCP server {name} was not found in any scope')
39273921

3922+
# Profile-scoped servers are configured via create_mcp_config_file(), not claude mcp add
3923+
if scope == 'profile':
3924+
info(f'MCP server {name} has scope: profile (will be configured via --strict-mcp-config)')
3925+
return True
3926+
39283927
# Build the base command
39293928
base_cmd = [str(claude_cmd), 'mcp', 'add']
39303929

@@ -4127,6 +4126,14 @@ def configure_all_mcp_servers(
41274126
if profile_servers and profile_mcp_config_path:
41284127
info(f'Creating profile MCP config with {len(profile_servers)} server(s)...')
41294128
create_mcp_config_file(profile_servers, profile_mcp_config_path)
4129+
elif profile_mcp_config_path and profile_mcp_config_path.exists():
4130+
# Remove stale profile MCP config file when no profile servers are configured
4131+
info(f'Removing stale profile MCP config: {profile_mcp_config_path.name}')
4132+
try:
4133+
profile_mcp_config_path.unlink()
4134+
success(f'Removed stale profile MCP config: {profile_mcp_config_path.name}')
4135+
except OSError as e:
4136+
warning(f'Failed to remove stale profile MCP config: {e}')
41304137

41314138
return True, profile_servers
41324139

tests/test_setup_environment_additional.py

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2655,12 +2655,12 @@ def test_create_mcp_config_file_permission_error(self) -> None:
26552655

26562656
@patch('platform.system', return_value='Linux')
26572657
@patch('setup_environment.find_command_robust', return_value='claude')
2658-
def test_configure_mcp_server_profile_scope_skipped(
2658+
def test_configure_mcp_server_profile_scope_removes_from_all_scopes(
26592659
self,
26602660
mock_find: MagicMock,
26612661
_mock_system: MagicMock,
26622662
) -> None:
2663-
"""Test that profile-scoped servers are skipped by configure_mcp_server."""
2663+
"""Test that profile-scoped servers are removed from all scopes but not added via claude mcp add."""
26642664
del mock_find # Unused but required for patch
26652665
del _mock_system # Unused but required for patch
26662666

@@ -2670,13 +2670,29 @@ def test_configure_mcp_server_profile_scope_skipped(
26702670
'scope': 'profile',
26712671
}
26722672

2673-
# Should return True (success) but not actually configure
26742673
with patch('setup_environment.run_command') as mock_run:
2674+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
26752675
result = setup_environment.configure_mcp_server(server)
26762676

26772677
assert result is True
2678-
# run_command should NOT be called for profile-scoped servers
2679-
mock_run.assert_not_called()
2678+
# run_command should be called 3 times (once per scope) for removal
2679+
assert mock_run.call_count == 3
2680+
2681+
# Verify removal was attempted from all scopes
2682+
calls = mock_run.call_args_list
2683+
scopes_removed = set()
2684+
for call in calls:
2685+
args = call[0][0] # First positional argument is the command list
2686+
# Verify it's a removal command
2687+
assert 'mcp' in args
2688+
assert 'remove' in args
2689+
assert 'profile-server' in args
2690+
# Extract scope
2691+
scope_idx = args.index('--scope') + 1
2692+
scopes_removed.add(args[scope_idx])
2693+
2694+
# Verify all three scopes were attempted
2695+
assert scopes_removed == {'user', 'local', 'project'}
26802696

26812697
@patch('platform.system', return_value='Linux')
26822698
@patch('setup_environment.find_command_robust', return_value='claude')
@@ -2840,3 +2856,68 @@ def test_configure_all_mcp_servers_mixed_scopes(
28402856
assert 'profile-2' in config['mcpServers']
28412857
assert 'default-scope' not in config['mcpServers']
28422858
assert 'explicit-user' not in config['mcpServers']
2859+
2860+
def test_stale_mcp_config_file_removed_when_no_profile_servers(self) -> None:
2861+
"""Test that stale MCP config file is removed when no profile servers exist."""
2862+
with tempfile.TemporaryDirectory() as tmpdir:
2863+
config_path = Path(tmpdir) / 'test-mcp.json'
2864+
# Create a stale MCP config file
2865+
config_path.write_text('{"mcpServers": {"stale-server": {}}}')
2866+
assert config_path.exists()
2867+
2868+
# Configure with NO profile servers (only user-scoped)
2869+
servers = [
2870+
{'name': 'global-server', 'transport': 'http', 'url': 'http://a.com', 'scope': 'user'},
2871+
]
2872+
2873+
with patch('setup_environment.configure_mcp_server', return_value=True):
2874+
success, profile_servers = setup_environment.configure_all_mcp_servers(
2875+
servers, config_path,
2876+
)
2877+
2878+
# Verify stale file was removed
2879+
assert success is True
2880+
assert len(profile_servers) == 0
2881+
assert not config_path.exists()
2882+
2883+
def test_mcp_config_file_not_touched_when_path_none(self) -> None:
2884+
"""Test that no file operations occur when profile_mcp_config_path is None."""
2885+
servers = [
2886+
{'name': 'server', 'transport': 'http', 'url': 'http://a.com', 'scope': 'user'},
2887+
]
2888+
2889+
with patch('setup_environment.configure_mcp_server', return_value=True):
2890+
success, profile_servers = setup_environment.configure_all_mcp_servers(
2891+
servers, None, # No profile config path
2892+
)
2893+
2894+
assert success is True
2895+
assert len(profile_servers) == 0
2896+
# No assertion about file - just verify no crash
2897+
2898+
def test_stale_mcp_config_removal_failure_logged_as_warning(self) -> None:
2899+
"""Test that stale MCP config removal failure is logged as warning, not error."""
2900+
with tempfile.TemporaryDirectory() as tmpdir:
2901+
config_path = Path(tmpdir) / 'test-mcp.json'
2902+
# Create a stale MCP config file
2903+
config_path.write_text('{"mcpServers": {"stale-server": {}}}')
2904+
2905+
servers = [
2906+
{'name': 'server', 'transport': 'http', 'url': 'http://a.com', 'scope': 'user'},
2907+
]
2908+
2909+
with (
2910+
patch('setup_environment.configure_mcp_server', return_value=True),
2911+
patch.object(Path, 'unlink', side_effect=OSError('Permission denied')),
2912+
patch('setup_environment.warning') as mock_warning,
2913+
):
2914+
success, profile_servers = setup_environment.configure_all_mcp_servers(
2915+
servers, config_path,
2916+
)
2917+
2918+
# Should still return success (non-fatal error)
2919+
assert success is True
2920+
assert len(profile_servers) == 0
2921+
# Warning should have been logged
2922+
mock_warning.assert_called_once()
2923+
assert 'Permission denied' in str(mock_warning.call_args)

0 commit comments

Comments
 (0)