Skip to content

Commit 37174ce

Browse files
committed
change toggle to stash/pop and more clean up
1 parent 166cdb0 commit 37174ce

File tree

13 files changed

+300
-265
lines changed

13 files changed

+300
-265
lines changed

src/mcpm/cli.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
remove,
1515
list,
1616
edit,
17-
toggle,
17+
stash,
18+
pop,
1819
server,
1920
client,
2021
inspector,
@@ -111,7 +112,8 @@ def main(ctx, help_flag):
111112
commands_table.add_row(" [cyan]remove[/]", "Remove an installed MCP server.")
112113
commands_table.add_row(" [cyan]search[/]", "Search available MCP servers.")
113114
commands_table.add_row(" [cyan]server[/]", "Manage MCP server processes.")
114-
commands_table.add_row(" [cyan]toggle[/]", "Toggle an MCP server on or off for a client.")
115+
commands_table.add_row(" [cyan]stash[/]", "Temporarily store a server configuration aside.")
116+
commands_table.add_row(" [cyan]pop[/]", "Restore a previously stashed server configuration.")
115117
console.print(commands_table)
116118

117119

@@ -127,7 +129,8 @@ def main(ctx, help_flag):
127129
main.add_command(list.list)
128130
main.add_command(edit.edit)
129131

130-
main.add_command(toggle.toggle)
132+
main.add_command(stash.stash)
133+
main.add_command(pop.pop)
131134
main.add_command(server.server)
132135
main.add_command(client.client)
133136
main.add_command(inspector.inspector, name="inspector")

src/mcpm/clients/base.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,39 @@ def is_client_installed(self) -> bool:
201201
# Default implementation - can be overridden by subclasses
202202
client_dir = os.path.dirname(self.config_path)
203203
return os.path.isdir(client_dir)
204+
205+
def disable_server(self, server_name: str) -> bool:
206+
"""Temporarily disable (stash) a server without removing its configuration
207+
208+
Args:
209+
server_name: Name of the server to disable
210+
211+
Returns:
212+
bool: Success or failure
213+
"""
214+
# To be implemented by subclasses
215+
raise NotImplementedError("Subclasses must implement disable_server")
216+
217+
def enable_server(self, server_name: str) -> bool:
218+
"""Re-enable (pop) a previously disabled server
219+
220+
Args:
221+
server_name: Name of the server to enable
222+
223+
Returns:
224+
bool: Success or failure
225+
"""
226+
# To be implemented by subclasses
227+
raise NotImplementedError("Subclasses must implement enable_server")
228+
229+
def is_server_disabled(self, server_name: str) -> bool:
230+
"""Check if a server is currently disabled (stashed)
231+
232+
Args:
233+
server_name: Name of the server to check
234+
235+
Returns:
236+
bool: True if server is disabled, False otherwise
237+
"""
238+
# To be implemented by subclasses
239+
raise NotImplementedError("Subclasses must implement is_server_disabled")

src/mcpm/clients/claude_desktop.py

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ def __init__(self, config_path: str = CLAUDE_CONFIG_PATH):
2929

3030
def _get_empty_config(self) -> Dict[str, Any]:
3131
"""Get empty config structure for Claude Desktop"""
32-
return {"mcpServers": {}}
32+
return {
33+
"mcpServers": {},
34+
"disabledServers": {}
35+
}
3336

3437
def _add_server_config(self, server_name: str, server_config: Dict[str, Any]) -> bool:
3538
"""Add or update an MCP server in Claude Desktop config using raw config dictionary
@@ -92,7 +95,7 @@ def _convert_to_client_format(self, server_config: ServerConfig) -> Dict[str, An
9295

9396
# Add additional metadata fields for display in Claude Desktop
9497
# Fields that are None will be automatically excluded by JSON serialization
95-
for field in ["name", "display_name", "description", "version", "status", "path", "install_date"]:
98+
for field in ["name", "display_name", "description", "installation"]:
9699
value = getattr(server_config, field, None)
97100
if value is not None:
98101
result[field] = value
@@ -122,8 +125,7 @@ def from_claude_desktop_format(cls, server_name: str, client_config: Dict[str, A
122125
server_data["env_vars"] = client_config["env"]
123126

124127
# Add additional metadata fields if present
125-
for field in ["display_name", "description", "version", "status", "path", "install_date",
126-
"package", "installation_method", "installation_type"]:
128+
for field in ["display_name", "description", "installation"]:
127129
if field in client_config:
128130
server_data[field] = client_config[field]
129131

@@ -141,6 +143,74 @@ def _convert_from_client_format(self, server_name: str, client_config: Dict[str,
141143
"""
142144
return self.from_claude_desktop_format(server_name, client_config)
143145

146+
def disable_server(self, server_name: str) -> bool:
147+
"""Temporarily disable (stash) a server without removing its configuration
148+
149+
Args:
150+
server_name: Name of the server to disable
151+
152+
Returns:
153+
bool: Success or failure
154+
"""
155+
config = self._load_config()
156+
157+
# Check if the server exists in active servers
158+
if "mcpServers" not in config or server_name not in config["mcpServers"]:
159+
logger.warning(f"Server '{server_name}' not found in active servers")
160+
return False
161+
162+
# Initialize disabledServers if it doesn't exist
163+
if "disabledServers" not in config:
164+
config["disabledServers"] = {}
165+
166+
# Store the server config in disabled servers
167+
config["disabledServers"][server_name] = config["mcpServers"][server_name]
168+
169+
# Remove from active servers
170+
del config["mcpServers"][server_name]
171+
172+
return self._save_config(config)
173+
174+
def enable_server(self, server_name: str) -> bool:
175+
"""Re-enable (pop) a previously disabled server
176+
177+
Args:
178+
server_name: Name of the server to enable
179+
180+
Returns:
181+
bool: Success or failure
182+
"""
183+
config = self._load_config()
184+
185+
# Check if the server exists in disabled servers
186+
if "disabledServers" not in config or server_name not in config["disabledServers"]:
187+
logger.warning(f"Server '{server_name}' not found in disabled servers")
188+
return False
189+
190+
# Initialize mcpServers if it doesn't exist
191+
if "mcpServers" not in config:
192+
config["mcpServers"] = {}
193+
194+
# Move the server config from disabled to active
195+
config["mcpServers"][server_name] = config["disabledServers"][server_name]
196+
197+
# Remove from disabled servers
198+
del config["disabledServers"][server_name]
199+
200+
return self._save_config(config)
201+
202+
def is_server_disabled(self, server_name: str) -> bool:
203+
"""Check if a server is currently disabled (stashed)
204+
205+
Args:
206+
server_name: Name of the server to check
207+
208+
Returns:
209+
bool: True if server is disabled, False otherwise
210+
"""
211+
config = self._load_config()
212+
return "disabledServers" in config and server_name in config["disabledServers"]
213+
144214
def remove_server(self, server_name: str) -> bool:
145215
"""Remove an MCP server from Claude Desktop config
146216

src/mcpm/clients/windsurf.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,26 @@ def from_windsurf_format(cls, server_name: str, client_config: Dict[str, Any]) -
123123
server_data["env_vars"] = client_config["env"]
124124

125125
# Add additional metadata fields if present
126-
for field in ["display_name", "description", "version", "status", "path", "install_date",
127-
"package", "installation_method", "installation_type"]:
126+
for field in ["display_name", "description", "installation"]:
128127
if field in client_config:
129128
server_data[field] = client_config[field]
129+
130+
# For backward compatibility, if path, version, etc. are in the config
131+
# but not in ServerConfig anymore, store the installation info
132+
if "installation" not in server_data:
133+
install_details = []
134+
# Check for explicit installation fields
135+
if "installation_method" in client_config:
136+
install_details.append(client_config["installation_method"])
137+
if "installation_type" in client_config:
138+
install_details.append(client_config["installation_type"])
139+
140+
# If no explicit installation info but has legacy fields,
141+
# create a generic installation value
142+
if not install_details and ("path" in client_config or "version" in client_config):
143+
server_data["installation"] = "legacy:import"
144+
elif install_details:
145+
server_data["installation"] = ":".join(install_details)
130146

131147
return ServerConfig.from_dict(server_data)
132148

src/mcpm/commands/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
"edit",
99
"inspector",
1010
"list",
11+
"pop",
1112
"remove",
1213
"search",
1314
"server",
14-
"toggle"
15+
"stash"
1516
]
1617

1718
# All command modules
@@ -20,7 +21,8 @@
2021
from . import edit
2122
from . import inspector
2223
from . import list
24+
from . import pop
2325
from . import remove
2426
from . import search
2527
from . import server
26-
from . import toggle
28+
from . import stash

src/mcpm/commands/add.py

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import os
66
import json
7-
from datetime import datetime
87

98
import click
109
from rich.console import Console
@@ -94,8 +93,7 @@ def add(server_name, force=False):
9493

9594
# If no installation information is available, create minimal default values
9695
# This allows us to add the server config without full installation details
97-
method_key = "default"
98-
install_type = "manual"
96+
installation_method = "manual" # Single consolidated concept
9997
install_command = "echo"
10098
install_args = [f"Server {server_name} added to configuration"]
10199
package_name = None
@@ -106,52 +104,56 @@ def add(server_name, force=False):
106104
selected_method = None # Initialize selected_method to None to avoid UnboundLocalError
107105
if installations:
108106
# Find recommended installation method or default to the first one
109-
method_key = "default"
107+
method_id = "default" # ID of the method in the config
110108

111109
# First check for a recommended method
112110
for key, method in installations.items():
113111
if method.get("recommended", False):
114112
selected_method = method
115-
method_key = key
113+
method_id = key
116114
break
117115

118116
# If no recommended method found, use the first one
119117
if not selected_method and installations:
120-
method_key = next(iter(installations))
121-
selected_method = installations[method_key]
118+
method_id = next(iter(installations))
119+
selected_method = installations[method_id]
122120

123121
# If multiple methods are available and not forced, offer selection
124122
if len(installations) > 1 and not force:
125123
console.print("\n[bold]Available installation methods:[/]")
126124
methods_list = []
127125

128126
for i, (key, method) in enumerate(installations.items(), 1):
129-
install_type = method.get("type", "unknown")
130-
description = method.get("description", f"{install_type} installation")
127+
method_type = method.get("type", "unknown")
128+
description = method.get("description", f"{method_type} installation")
131129
recommended = " [green](recommended)[/]" if method.get("recommended", False) else ""
132130

133131
console.print(f" {i}. [cyan]{key}[/]: {description}{recommended}")
134132
methods_list.append(key)
135133

136134
# Ask user to select a method
137135
try:
138-
selection = click.prompt("\nSelect installation method", type=int, default=methods_list.index(method_key) + 1)
136+
selection = click.prompt("\nSelect installation method", type=int, default=methods_list.index(method_id) + 1)
139137
if 1 <= selection <= len(methods_list):
140-
method_key = methods_list[selection - 1]
141-
selected_method = installations[method_key]
138+
method_id = methods_list[selection - 1]
139+
selected_method = installations[method_id]
142140
except (ValueError, click.Abort):
143141
console.print("[yellow]Using default installation method.[/]")
144142

145143
# Extract installation details
146144
if selected_method:
147-
install_type = selected_method.get("type", install_type)
145+
# Use the method's type as the installation method if available, otherwise use the key
146+
installation_method = selected_method.get("type")
147+
if not installation_method or installation_method == "unknown":
148+
installation_method = method_id
149+
148150
install_command = selected_method.get("command", install_command)
149151
install_args = selected_method.get("args", install_args)
150152
package_name = selected_method.get("package", package_name)
151153
env_vars = selected_method.get("env", env_vars)
152154
required_args = server_metadata.get("required_args", required_args)
153155

154-
console.print(f"\n[green]Using {install_type} installation method: [bold]{method_key}[/][/]")
156+
console.print(f"\n[green]Using [bold]{installation_method}[/] installation method[/]")
155157

156158
# Configure the server
157159
with Progress(
@@ -311,18 +313,13 @@ def add(server_name, force=False):
311313
# Create server configuration using ServerConfig
312314
server_config = ServerConfig(
313315
name=server_name,
314-
path=server_dir,
315316
display_name=display_name,
316317
description=description,
317-
version=version,
318-
status="stopped",
319318
command=mcp_command, # Use the actual MCP server command
320319
args=mcp_args, # Use the actual MCP server arguments
321320
env_vars=processed_env,
322-
install_date=datetime.now().strftime("%Y-%m-%d"),
323-
package=package_name,
324-
installation_method=method_key,
325-
installation_type=install_type
321+
# Use the simplified installation method
322+
installation = installation_method
326323
)
327324

328325
# Add the server to the client configuration

src/mcpm/commands/pop.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
Pop command for MCPM - restores previously stashed server configuration
3+
"""
4+
5+
import click
6+
from rich.console import Console
7+
8+
from mcpm.utils.client_manager import get_active_client_info
9+
10+
console = Console()
11+
12+
@click.command()
13+
@click.argument("server_name")
14+
def pop(server_name):
15+
"""Restore a previously stashed server configuration.
16+
17+
This command re-enables a previously stashed (disabled) server,
18+
restoring it to active status.
19+
20+
Examples:
21+
mcpm pop memory
22+
"""
23+
# Get the active client manager and related information
24+
client_manager, client_name, client_id = get_active_client_info()
25+
26+
# Check if client is supported
27+
if client_manager is None:
28+
console.print("[bold red]Error:[/] Unsupported active client")
29+
console.print("Please switch to a supported client using 'mcpm client <client-name>'")
30+
return
31+
32+
# Check if the server exists in the stashed configurations
33+
if not client_manager.is_server_disabled(server_name):
34+
console.print(f"[bold red]Error:[/] Server '{server_name}' not found in stashed configurations.")
35+
return
36+
37+
# Pop (re-enable) the server
38+
success = client_manager.enable_server(server_name)
39+
40+
if success:
41+
console.print(f"[bold green]Restored[/] MCP server '{server_name}' for {client_name}")
42+
console.print("Remember to restart the client for changes to take effect.")
43+
else:
44+
console.print(f"[bold red]Failed to restore[/] '{server_name}' for {client_name}.")

0 commit comments

Comments
 (0)