Skip to content

Commit 9c07d62

Browse files
committed
Client manager refactor
1 parent 7a53292 commit 9c07d62

File tree

10 files changed

+1276
-300
lines changed

10 files changed

+1276
-300
lines changed

src/mcpm/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
server,
2020
client,
2121
inspector,
22+
add,
2223
)
2324

2425
console = Console()
@@ -103,10 +104,11 @@ def main(ctx, help_flag):
103104
# Display available commands in a table
104105
console.print("[bold]Commands:[/]")
105106
commands_table = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
107+
commands_table.add_row(" [cyan]add[/]", "Add an MCP server directly to a client.")
106108
commands_table.add_row(" [cyan]client[/]", "Manage the active MCPM client.")
107109
commands_table.add_row(" [cyan]edit[/]", "View or edit the active MCPM client's configuration file.")
108110
commands_table.add_row(" [cyan]inspector[/]", "Launch the MCPM Inspector UI to examine servers.")
109-
commands_table.add_row(" [cyan]install[/]", "Install an MCP server.")
111+
commands_table.add_row(" [cyan]install[/]", "[yellow][DEPRECATED][/] Install an MCP server (use add instead).")
110112
commands_table.add_row(" [cyan]list[/]", "List all installed MCP servers.")
111113
commands_table.add_row(" [cyan]remove[/]", "Remove an installed MCP server.")
112114
commands_table.add_row(" [cyan]search[/]", "Search available MCP servers.")
@@ -124,6 +126,7 @@ def main(ctx, help_flag):
124126
main.add_command(search.search)
125127
main.add_command(install.install)
126128
main.add_command(remove.remove)
129+
main.add_command(add.add)
127130
main.add_command(list_servers.list)
128131
main.add_command(edit.edit)
129132

src/mcpm/clients/base.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""
2+
Base client manager module that defines the interface for all client managers.
3+
"""
4+
5+
import os
6+
import json
7+
import logging
8+
from typing import Dict, Optional, Any, List
9+
10+
from mcpm.utils.server_config import ServerConfig
11+
12+
logger = logging.getLogger(__name__)
13+
14+
class BaseClientManager:
15+
"""Base class for all client managers providing a common interface"""
16+
17+
def __init__(self, config_path: str):
18+
"""Initialize with a configuration path"""
19+
self.config_path = config_path
20+
self._config = None
21+
22+
def _load_config(self) -> Dict[str, Any]:
23+
"""Load client configuration file
24+
25+
Returns:
26+
Dict containing the client configuration
27+
"""
28+
if not os.path.exists(self.config_path):
29+
logger.warning(f"Client config file not found at: {self.config_path}")
30+
return self._get_empty_config()
31+
32+
try:
33+
with open(self.config_path, 'r') as f:
34+
return json.load(f)
35+
except json.JSONDecodeError:
36+
logger.error(f"Error parsing client config file: {self.config_path}")
37+
38+
# Backup the corrupt file
39+
if os.path.exists(self.config_path):
40+
backup_path = f"{self.config_path}.bak"
41+
try:
42+
os.rename(self.config_path, backup_path)
43+
logger.info(f"Backed up corrupt config file to: {backup_path}")
44+
except Exception as e:
45+
logger.error(f"Failed to backup corrupt file: {str(e)}")
46+
47+
# Return empty config
48+
return self._get_empty_config()
49+
50+
def _save_config(self, config: Dict[str, Any]) -> bool:
51+
"""Save configuration to client config file
52+
53+
Args:
54+
config: Configuration to save
55+
56+
Returns:
57+
bool: Success or failure
58+
"""
59+
try:
60+
# Create directory if it doesn't exist
61+
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
62+
63+
with open(self.config_path, 'w') as f:
64+
json.dump(config, f, indent=2)
65+
return True
66+
except Exception as e:
67+
logger.error(f"Error saving client config: {str(e)}")
68+
return False
69+
70+
def _get_empty_config(self) -> Dict[str, Any]:
71+
"""Get an empty config structure for this client
72+
73+
Returns:
74+
Dict containing empty configuration structure
75+
"""
76+
# To be overridden by subclasses
77+
return {"mcpServers": {}}
78+
79+
def get_servers(self) -> Dict[str, Any]:
80+
"""Get all MCP servers configured for this client
81+
82+
Returns:
83+
Dict of server configurations by name
84+
"""
85+
# To be overridden by subclasses
86+
config = self._load_config()
87+
return config.get("mcpServers", {})
88+
89+
def get_server(self, server_name: str) -> Optional[Dict[str, Any]]:
90+
"""Get a specific MCP server configuration
91+
92+
Args:
93+
server_name: Name of the server to retrieve
94+
95+
Returns:
96+
Server configuration or None if not found
97+
"""
98+
servers = self.get_servers()
99+
return servers.get(server_name)
100+
101+
def _add_server_config(self, server_name: str, server_config: Dict[str, Any]) -> bool:
102+
"""Add or update an MCP server in client config using raw config dictionary
103+
104+
Note: This is an internal method that should generally not be called directly.
105+
Use add_server with a ServerConfig object instead for better type safety and validation.
106+
107+
Args:
108+
server_name: Name of the server
109+
server_config: Server configuration dictionary
110+
111+
Returns:
112+
bool: Success or failure
113+
"""
114+
# To be implemented by subclasses
115+
raise NotImplementedError("Subclasses must implement _add_server_config")
116+
117+
def add_server(self, server_config: ServerConfig) -> bool:
118+
"""Add or update a server using a ServerConfig object
119+
120+
This is the preferred method for adding servers as it ensures proper type safety
121+
and validation through the ServerConfig object.
122+
123+
Args:
124+
server_config: StandardServer configuration object
125+
126+
Returns:
127+
bool: Success or failure
128+
"""
129+
# Default implementation - can be overridden by subclasses
130+
return self._add_server_config(server_config.name, self._convert_to_client_format(server_config))
131+
132+
def _convert_to_client_format(self, server_config: ServerConfig) -> Dict[str, Any]:
133+
"""Convert ServerConfig to client-specific format
134+
135+
Args:
136+
server_config: StandardServer configuration
137+
138+
Returns:
139+
Dict containing client-specific configuration
140+
"""
141+
# To be implemented by subclasses
142+
raise NotImplementedError("Subclasses must implement _convert_to_client_format")
143+
144+
def _convert_from_client_format(self, server_name: str, client_config: Dict[str, Any]) -> ServerConfig:
145+
"""Convert client-specific format to ServerConfig
146+
147+
Args:
148+
server_name: Name of the server
149+
client_config: Client-specific configuration
150+
151+
Returns:
152+
ServerConfig object
153+
"""
154+
# To be implemented by subclasses
155+
raise NotImplementedError("Subclasses must implement _convert_from_client_format")
156+
157+
def get_server_configs(self) -> List[ServerConfig]:
158+
"""Get all MCP servers as ServerConfig objects
159+
160+
Returns:
161+
List of ServerConfig objects
162+
"""
163+
servers = self.get_servers()
164+
return [
165+
self._convert_from_client_format(name, config)
166+
for name, config in servers.items()
167+
]
168+
169+
def get_server_config(self, server_name: str) -> Optional[ServerConfig]:
170+
"""Get a specific MCP server as a ServerConfig object
171+
172+
Args:
173+
server_name: Name of the server
174+
175+
Returns:
176+
ServerConfig or None if not found
177+
"""
178+
server = self.get_server(server_name)
179+
if server:
180+
return self._convert_from_client_format(server_name, server)
181+
return None
182+
183+
def remove_server(self, server_name: str) -> bool:
184+
"""Remove an MCP server from client config
185+
186+
Args:
187+
server_name: Name of the server to remove
188+
189+
Returns:
190+
bool: Success or failure
191+
"""
192+
# To be implemented by subclasses
193+
raise NotImplementedError("Subclasses must implement remove_server")
194+
195+
def is_client_installed(self) -> bool:
196+
"""Check if this client is installed
197+
198+
Returns:
199+
bool: True if client is installed, False otherwise
200+
"""
201+
# Default implementation - can be overridden by subclasses
202+
client_dir = os.path.dirname(self.config_path)
203+
return os.path.isdir(client_dir)

src/mcpm/clients/claude_desktop.py

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
"""
44

55
import os
6-
import json
76
import logging
8-
from typing import Dict, Optional, Any
97
import platform
8+
from typing import Dict, Any
9+
10+
from mcpm.clients.base import BaseClientManager
11+
from mcpm.utils.server_config import ServerConfig
1012

1113
logger = logging.getLogger(__name__)
1214

@@ -19,51 +21,29 @@
1921
# Linux (unsupported by Claude Desktop currently, but future-proofing)
2022
CLAUDE_CONFIG_PATH = os.path.expanduser("~/.config/Claude/claude_desktop_config.json")
2123

22-
class ClaudeDesktopManager:
24+
class ClaudeDesktopManager(BaseClientManager):
2325
"""Manages Claude Desktop MCP server configurations"""
2426

2527
def __init__(self, config_path: str = CLAUDE_CONFIG_PATH):
26-
self.config_path = config_path
27-
self._config = None
28-
29-
def _load_config(self) -> Dict[str, Any]:
30-
"""Load Claude Desktop configuration file"""
31-
if not os.path.exists(self.config_path):
32-
logger.warning(f"Claude Desktop config file not found at: {self.config_path}")
33-
return {"mcpServers": {}}
34-
35-
try:
36-
with open(self.config_path, 'r') as f:
37-
return json.load(f)
38-
except json.JSONDecodeError:
39-
logger.error(f"Error parsing Claude Desktop config file: {self.config_path}")
40-
return {"mcpServers": {}}
41-
42-
def _save_config(self, config: Dict[str, Any]) -> bool:
43-
"""Save configuration to Claude Desktop config file"""
44-
try:
45-
# Create directory if it doesn't exist
46-
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
47-
48-
with open(self.config_path, 'w') as f:
49-
json.dump(config, f, indent=2)
50-
return True
51-
except Exception as e:
52-
logger.error(f"Error saving Claude Desktop config: {str(e)}")
53-
return False
28+
super().__init__(config_path)
5429

55-
def get_servers(self) -> Dict[str, Any]:
56-
"""Get all MCP servers configured in Claude Desktop"""
57-
config = self._load_config()
58-
return config.get("mcpServers", {})
59-
60-
def get_server(self, server_name: str) -> Optional[Dict[str, Any]]:
61-
"""Get a specific MCP server configuration"""
62-
servers = self.get_servers()
63-
return servers.get(server_name)
30+
def _get_empty_config(self) -> Dict[str, Any]:
31+
"""Get empty config structure for Claude Desktop"""
32+
return {"mcpServers": {}}
6433

65-
def add_server(self, server_name: str, server_config: Dict[str, Any]) -> bool:
66-
"""Add or update an MCP server in Claude Desktop config"""
34+
def _add_server_config(self, server_name: str, server_config: Dict[str, Any]) -> bool:
35+
"""Add or update an MCP server in Claude Desktop config using raw config dictionary
36+
37+
Note: This is an internal method that should generally not be called directly.
38+
Use add_server with a ServerConfig object instead for better type safety and validation.
39+
40+
Args:
41+
server_name: Name of the server
42+
server_config: Server configuration dictionary
43+
44+
Returns:
45+
bool: Success or failure
46+
"""
6747
config = self._load_config()
6848

6949
# Initialize mcpServers if it doesn't exist
@@ -74,6 +54,44 @@ def add_server(self, server_name: str, server_config: Dict[str, Any]) -> bool:
7454
config["mcpServers"][server_name] = server_config
7555

7656
return self._save_config(config)
57+
58+
def add_server(self, server_config: ServerConfig) -> bool:
59+
"""Add or update a server using a ServerConfig object
60+
61+
This is the preferred method for adding servers as it ensures proper type safety
62+
and validation through the ServerConfig object.
63+
64+
Args:
65+
server_config: ServerConfig object
66+
67+
Returns:
68+
bool: Success or failure
69+
"""
70+
client_config = self._convert_to_client_format(server_config)
71+
return self._add_server_config(server_config.name, client_config)
72+
73+
def _convert_to_client_format(self, server_config: ServerConfig) -> Dict[str, Any]:
74+
"""Convert ServerConfig to Claude Desktop format
75+
76+
Args:
77+
server_config: StandardServer configuration
78+
79+
Returns:
80+
Dict containing Claude Desktop-specific configuration
81+
"""
82+
return server_config.to_claude_desktop_format()
83+
84+
def _convert_from_client_format(self, server_name: str, client_config: Dict[str, Any]) -> ServerConfig:
85+
"""Convert Claude Desktop format to ServerConfig
86+
87+
Args:
88+
server_name: Name of the server
89+
client_config: Claude Desktop-specific configuration
90+
91+
Returns:
92+
ServerConfig object
93+
"""
94+
return ServerConfig.from_claude_desktop_format(server_name, client_config)
7795

7896
def remove_server(self, server_name: str) -> bool:
7997
"""Remove an MCP server from Claude Desktop config"""
@@ -87,9 +105,7 @@ def remove_server(self, server_name: str) -> bool:
87105
del config["mcpServers"][server_name]
88106

89107
return self._save_config(config)
90-
108+
91109
def is_claude_desktop_installed(self) -> bool:
92110
"""Check if Claude Desktop is installed"""
93-
# Check for the presence of the Claude Desktop directory
94-
claude_dir = os.path.dirname(self.config_path)
95-
return os.path.isdir(claude_dir)
111+
return self.is_client_installed()

0 commit comments

Comments
 (0)