Skip to content

Commit 2ff47a4

Browse files
authored
Add mcpm add commend
Add server
2 parents 7a53292 + e7906cd commit 2ff47a4

File tree

10 files changed

+1447
-468
lines changed

10 files changed

+1447
-468
lines changed

src/mcpm/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
from mcpm import __version__
1212
from mcpm.commands import (
1313
search,
14-
install,
1514
remove,
1615
list_servers,
1716
edit,
1817
toggle,
1918
server,
2019
client,
2120
inspector,
21+
add,
2222
)
2323

2424
console = Console()
@@ -103,10 +103,10 @@ def main(ctx, help_flag):
103103
# Display available commands in a table
104104
console.print("[bold]Commands:[/]")
105105
commands_table = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
106+
commands_table.add_row(" [cyan]add[/]", "Add an MCP server directly to a client.")
106107
commands_table.add_row(" [cyan]client[/]", "Manage the active MCPM client.")
107108
commands_table.add_row(" [cyan]edit[/]", "View or edit the active MCPM client's configuration file.")
108109
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.")
110110
commands_table.add_row(" [cyan]list[/]", "List all installed MCP servers.")
111111
commands_table.add_row(" [cyan]remove[/]", "Remove an installed MCP server.")
112112
commands_table.add_row(" [cyan]search[/]", "Search available MCP servers.")
@@ -122,8 +122,8 @@ def main(ctx, help_flag):
122122

123123
# Register commands
124124
main.add_command(search.search)
125-
main.add_command(install.install)
126125
main.add_command(remove.remove)
126+
main.add_command(add.add)
127127
main.add_command(list_servers.list)
128128
main.add_command(edit.edit)
129129

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)

0 commit comments

Comments
 (0)