Skip to content

Commit cc7dbb1

Browse files
committed
clean up client manager
1 parent b5fa302 commit cc7dbb1

File tree

7 files changed

+123
-254
lines changed

7 files changed

+123
-254
lines changed

src/mcpm/clients/base.py

Lines changed: 48 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import os
66
import json
77
import logging
8-
from typing import Dict, Optional, Any, List
8+
import platform
9+
from typing import Dict, Optional, Any, List, Union
910

1011
from mcpm.utils.server_config import ServerConfig
1112

@@ -19,24 +20,33 @@ class BaseClientManager:
1920
display_name = "" # Human-readable name (e.g., "Claude Desktop")
2021
download_url = "" # URL to download the client
2122

22-
def __init__(self, config_path: str):
23-
"""Initialize with a configuration path"""
24-
self.config_path = config_path
23+
def __init__(self):
24+
"""Initialize the client manager
25+
"""
26+
self.config_path = None # To be set by subclasses
2527
self._config = None
28+
self._system = platform.system()
2629

2730
def _load_config(self) -> Dict[str, Any]:
2831
"""Load client configuration file
2932
3033
Returns:
31-
Dict containing the client configuration
34+
Dict containing the client configuration with at least {"mcpServers": {}}
3235
"""
36+
# Create empty config with the correct structure
37+
empty_config = {"mcpServers": {}}
38+
3339
if not os.path.exists(self.config_path):
3440
logger.warning(f"Client config file not found at: {self.config_path}")
35-
return self._get_empty_config()
41+
return empty_config
3642

3743
try:
3844
with open(self.config_path, 'r') as f:
39-
return json.load(f)
45+
config = json.load(f)
46+
# Ensure mcpServers section exists
47+
if "mcpServers" not in config:
48+
config["mcpServers"] = {}
49+
return config
4050
except json.JSONDecodeError:
4151
logger.error(f"Error parsing client config file: {self.config_path}")
4252

@@ -50,7 +60,7 @@ def _load_config(self) -> Dict[str, Any]:
5060
logger.error(f"Failed to backup corrupt file: {str(e)}")
5161

5262
# Return empty config
53-
return self._get_empty_config()
63+
return empty_config
5464

5565
def _save_config(self, config: Dict[str, Any]) -> bool:
5666
"""Save configuration to client config file
@@ -72,14 +82,7 @@ def _save_config(self, config: Dict[str, Any]) -> bool:
7282
logger.error(f"Error saving client config: {str(e)}")
7383
return False
7484

75-
def _get_empty_config(self) -> Dict[str, Any]:
76-
"""Get an empty config structure for this client
77-
78-
Returns:
79-
Dict containing empty configuration structure
80-
"""
81-
# To be overridden by subclasses
82-
return {"mcpServers": {}}
85+
8386

8487
def get_servers(self) -> Dict[str, Any]:
8588
"""Get all MCP servers configured for this client
@@ -109,48 +112,39 @@ def get_server(self, server_name: str) -> Optional[ServerConfig]:
109112

110113
# Get the server config and convert to ServerConfig
111114
client_config = servers[server_name]
112-
return self._convert_from_client_format(server_name, client_config)
115+
return self.from_client_format(server_name, client_config)
113116

114-
def _add_server_config(self, server_name: str, server_config: Dict[str, Any]) -> bool:
115-
"""Add or update an MCP server in client config using raw config dictionary
117+
def add_server(self, server_config: Union[ServerConfig, Dict[str, Any]], name: Optional[str] = None) -> bool:
118+
"""Add or update a server in the client config
116119
117-
Note: This is an internal method that should generally not be called directly.
118-
Use add_server with a ServerConfig object instead for better type safety and validation.
120+
Can accept either a ServerConfig object or a raw dictionary in client format.
121+
When using a dictionary, a name must be provided.
119122
120123
Args:
121-
server_name: Name of the server
122-
server_config: Server configuration dictionary
124+
server_config: ServerConfig object or dictionary in client format
125+
name: Required server name when using dictionary format
123126
124127
Returns:
125128
bool: Success or failure
126129
"""
130+
# Handle direct dictionary input
131+
if isinstance(server_config, dict):
132+
if name is None:
133+
raise ValueError("Name must be provided when using dictionary format")
134+
server_name = name
135+
client_config = server_config # Already in client format
136+
# Handle ServerConfig objects
137+
else:
138+
server_name = server_config.name
139+
client_config = self.to_client_format(server_config)
140+
141+
# Update config directly
127142
config = self._load_config()
128-
129-
# Initialize mcpServers if it doesn't exist
130-
if "mcpServers" not in config:
131-
config["mcpServers"] = {}
132-
133-
# Add or update the server
134-
config["mcpServers"][server_name] = server_config
143+
config["mcpServers"][server_name] = client_config
135144

136145
return self._save_config(config)
137-
138-
def add_server(self, server_config: ServerConfig) -> bool:
139-
"""Add or update a server using a ServerConfig object
140-
141-
This is the preferred method for adding servers as it ensures proper type safety
142-
and validation through the ServerConfig object.
143-
144-
Args:
145-
server_config: StandardServer configuration object
146-
147-
Returns:
148-
bool: Success or failure
149-
"""
150-
# Default implementation - can be overridden by subclasses
151-
return self._add_server_config(server_config.name, self._convert_to_client_format(server_config))
152146

153-
def _convert_to_client_format(self, server_config: ServerConfig) -> Dict[str, Any]:
147+
def to_client_format(self, server_config: ServerConfig) -> Dict[str, Any]:
154148
"""Convert ServerConfig to client-specific format with common core fields
155149
156150
This base implementation provides the common core fields (command, args, env)
@@ -202,68 +196,19 @@ def from_client_format(cls, server_name: str, client_config: Dict[str, Any]) ->
202196

203197
return ServerConfig.from_dict(server_data)
204198

205-
def _convert_from_client_format(self, server_name: str, client_config: Any) -> ServerConfig:
206-
"""Convert client-specific format to ServerConfig
207-
208-
This default implementation handles the case where client_config might be a ServerConfig already
209-
or needs to be converted using from_client_format.
210-
211-
Args:
212-
server_name: Name of the server
213-
client_config: Client-specific configuration or ServerConfig object
214-
215-
Returns:
216-
ServerConfig object
217-
"""
218-
# If client_config is already a ServerConfig object, just return it
219-
if isinstance(client_config, ServerConfig):
220-
# Ensure the name is set correctly
221-
if client_config.name != server_name:
222-
client_config.name = server_name
223-
return client_config
224-
225-
# Otherwise, convert from dict format
226-
return self.from_client_format(server_name, client_config)
199+
227200

228201
def list_servers(self) -> List[str]:
229202
"""List all MCP servers in client config
230203
231204
Returns:
232205
List of server names
233206
"""
234-
config = self._load_config()
235-
236-
# Check if mcpServers exists
237-
if "mcpServers" not in config:
238-
return []
239-
240-
return list(config["mcpServers"].keys())
207+
return list(self.get_servers().keys())
241208

242-
def get_server_configs(self) -> List[ServerConfig]:
243-
"""Get all MCP servers as ServerConfig objects
244-
245-
Returns:
246-
List of ServerConfig objects
247-
"""
248-
servers = self.get_servers()
249-
return [
250-
self._convert_from_client_format(name, config)
251-
for name, config in servers.items()
252-
]
209+
253210

254-
def get_server_config(self, server_name: str) -> Optional[ServerConfig]:
255-
"""Get a specific MCP server as a ServerConfig object
256-
257-
Args:
258-
server_name: Name of the server
259-
260-
Returns:
261-
ServerConfig or None if not found
262-
"""
263-
server = self.get_server(server_name)
264-
if server:
265-
return self._convert_from_client_format(server_name, server)
266-
return None
211+
267212

268213
def remove_server(self, server_name: str) -> bool:
269214
"""Remove an MCP server from client config
@@ -274,19 +219,15 @@ def remove_server(self, server_name: str) -> bool:
274219
Returns:
275220
bool: Success or failure
276221
"""
277-
config = self._load_config()
222+
servers = self.get_servers()
278223

279-
# Check if mcpServers exists
280-
if "mcpServers" not in config:
281-
logger.warning(f"Cannot remove server {server_name}: mcpServers section doesn't exist")
282-
return False
283-
284224
# Check if the server exists
285-
if server_name not in config["mcpServers"]:
225+
if server_name not in servers:
286226
logger.warning(f"Server {server_name} not found in {self.display_name} config")
287227
return False
288228

289-
# Remove the server
229+
# Load full config and remove the server
230+
config = self._load_config()
290231
del config["mcpServers"][server_name]
291232

292233
return self._save_config(config)
@@ -312,39 +253,3 @@ def is_client_installed(self) -> bool:
312253
# Default implementation checks if the config directory exists
313254
# Can be overridden by subclasses
314255
return os.path.isdir(os.path.dirname(self.config_path))
315-
316-
def disable_server(self, server_name: str) -> bool:
317-
"""Temporarily disable (stash) a server without removing its configuration
318-
319-
Args:
320-
server_name: Name of the server to disable
321-
322-
Returns:
323-
bool: Success or failure
324-
"""
325-
# To be implemented by subclasses
326-
raise NotImplementedError("Subclasses must implement disable_server")
327-
328-
def enable_server(self, server_name: str) -> bool:
329-
"""Re-enable (pop) a previously disabled server
330-
331-
Args:
332-
server_name: Name of the server to enable
333-
334-
Returns:
335-
bool: Success or failure
336-
"""
337-
# To be implemented by subclasses
338-
raise NotImplementedError("Subclasses must implement enable_server")
339-
340-
def is_server_disabled(self, server_name: str) -> bool:
341-
"""Check if a server is currently disabled (stashed)
342-
343-
Args:
344-
server_name: Name of the server to check
345-
346-
Returns:
347-
bool: True if server is disabled, False otherwise
348-
"""
349-
# To be implemented by subclasses
350-
raise NotImplementedError("Subclasses must implement is_server_disabled")

src/mcpm/clients/claude_desktop.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,6 @@
1111

1212
logger = logging.getLogger(__name__)
1313

14-
# Claude Desktop config paths based on platform
15-
if platform.system() == "Darwin": # macOS
16-
CLAUDE_CONFIG_PATH = os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json")
17-
elif platform.system() == "Windows":
18-
CLAUDE_CONFIG_PATH = os.path.join(os.environ.get("APPDATA", ""), "Claude", "claude_desktop_config.json")
19-
else:
20-
# Linux (unsupported by Claude Desktop currently, but future-proofing)
21-
CLAUDE_CONFIG_PATH = os.path.expanduser("~/.config/Claude/claude_desktop_config.json")
22-
2314
class ClaudeDesktopManager(BaseClientManager):
2415
"""Manages Claude Desktop MCP server configurations"""
2516

@@ -28,8 +19,25 @@ class ClaudeDesktopManager(BaseClientManager):
2819
display_name = "Claude Desktop"
2920
download_url = "https://claude.ai/download"
3021

31-
def __init__(self, config_path: str = CLAUDE_CONFIG_PATH):
32-
super().__init__(config_path)
22+
def __init__(self, config_path=None):
23+
"""Initialize the Claude Desktop client manager
24+
25+
Args:
26+
config_path: Optional path to the config file. If not provided, uses default path.
27+
"""
28+
super().__init__()
29+
30+
if config_path:
31+
self.config_path = config_path
32+
else:
33+
# Set config path based on detected platform
34+
if self._system == "Darwin": # macOS
35+
self.config_path = os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json")
36+
elif self._system == "Windows":
37+
self.config_path = os.path.join(os.environ.get("APPDATA", ""), "Claude", "claude_desktop_config.json")
38+
else:
39+
# Linux (unsupported by Claude Desktop currently, but future-proofing)
40+
self.config_path = os.path.expanduser("~/.config/Claude/claude_desktop_config.json")
3341

3442
def _get_empty_config(self) -> Dict[str, Any]:
3543
"""Get empty config structure for Claude Desktop"""
@@ -38,17 +46,6 @@ def _get_empty_config(self) -> Dict[str, Any]:
3846
"disabledServers": {}
3947
}
4048

41-
# Uses base class implementation of _add_server_config
42-
43-
# Uses base class implementation of add_server
44-
45-
# Uses the base class implementation of _convert_to_client_format
46-
# which handles the core fields (command, args, env)
47-
48-
# Uses base class implementation via from_client_format
49-
50-
# Uses base class implementation of _convert_from_client_format
51-
5249
def disable_server(self, server_name: str) -> bool:
5350
"""Temporarily disable (stash) a server without removing its configuration
5451

0 commit comments

Comments
 (0)