Skip to content

Commit 82f3e48

Browse files
feat: improve the schema (#31)
1 parent 68ee46e commit 82f3e48

File tree

15 files changed

+269
-329
lines changed

15 files changed

+269
-329
lines changed

src/mcpm/clients/base.py

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
import platform
1010
from typing import Any, Dict, List, Optional, Union
1111

12+
from pydantic import TypeAdapter
1213
from ruamel.yaml import YAML
1314

14-
from mcpm.utils.server_config import ServerConfig
15+
from mcpm.schemas.server_config import ServerConfig, STDIOServerConfig
1516

1617
logger = logging.getLogger(__name__)
1718

@@ -28,10 +29,10 @@ class BaseClientManager(abc.ABC):
2829
client_key = "" # Client identifier (e.g., "claude-desktop")
2930
display_name = "" # Human-readable name (e.g., "Claude Desktop")
3031
download_url = "" # URL to download the client
32+
config_path: str
3133

3234
def __init__(self):
3335
"""Initialize the client manager"""
34-
self.config_path = None # To be set by subclasses
3536
self._system = platform.system()
3637

3738
@abc.abstractmethod
@@ -56,12 +57,11 @@ def get_server(self, server_name: str) -> Optional[ServerConfig]:
5657
pass
5758

5859
@abc.abstractmethod
59-
def add_server(self, server_config: Union[ServerConfig, Dict[str, Any]], name: Optional[str] = None) -> bool:
60+
def add_server(self, server_config: ServerConfig) -> bool:
6061
"""Add or update a server in the client config
6162
6263
Args:
63-
server_config: ServerConfig object or dictionary in client format
64-
name: Required server name when using dictionary format
64+
server_config: ServerConfig object
6565
6666
Returns:
6767
bool: Success or failure
@@ -140,6 +140,7 @@ class JSONClientManager(BaseClientManager):
140140
This class implements the BaseClientManager interface using JSON files
141141
for configuration storage.
142142
"""
143+
143144
configure_key_name: str = "mcpServers"
144145

145146
def __init__(self):
@@ -231,29 +232,21 @@ def get_server(self, server_name: str) -> Optional[ServerConfig]:
231232
client_config = servers[server_name]
232233
return self.from_client_format(server_name, client_config)
233234

234-
def add_server(self, server_config: Union[ServerConfig, Dict[str, Any]], name: Optional[str] = None) -> bool:
235+
def add_server(self, server_config: ServerConfig) -> bool:
235236
"""Add or update a server in the client config
236237
237238
Can accept either a ServerConfig object or a raw dictionary in client format.
238239
When using a dictionary, a name must be provided.
239240
240241
Args:
241242
server_config: ServerConfig object or dictionary in client format
242-
name: Required server name when using dictionary format
243243
244244
Returns:
245245
bool: Success or failure
246246
"""
247-
# Handle direct dictionary input
248-
if isinstance(server_config, dict):
249-
if name is None:
250-
raise ValueError("Name must be provided when using dictionary format")
251-
server_name = name
252-
client_config = server_config # Already in client format
253247
# Handle ServerConfig objects
254-
else:
255-
server_name = server_config.name
256-
client_config = self.to_client_format(server_config)
248+
server_name = server_config.name
249+
client_config = self.to_client_format(server_config)
257250

258251
# Update config directly
259252
config = self._load_config()
@@ -275,15 +268,18 @@ def to_client_format(self, server_config: ServerConfig) -> Dict[str, Any]:
275268
Dict containing client-specific configuration with core fields
276269
"""
277270
# Base result containing only essential execution information
278-
result = {
279-
"command": server_config.command,
280-
"args": server_config.args,
281-
}
282-
283-
# Add filtered environment variables if present
284-
non_empty_env = server_config.get_filtered_env_vars(os.environ)
285-
if non_empty_env:
286-
result["env"] = non_empty_env
271+
if isinstance(server_config, STDIOServerConfig):
272+
result = {
273+
"command": server_config.command,
274+
"args": server_config.args,
275+
}
276+
277+
# Add filtered environment variables if present
278+
non_empty_env = server_config.get_filtered_env_vars(os.environ)
279+
if non_empty_env:
280+
result["env"] = non_empty_env
281+
else:
282+
result = server_config.to_dict()
287283

288284
return result
289285

@@ -300,18 +296,11 @@ def from_client_format(cls, server_name: str, client_config: Dict[str, Any]) ->
300296
Returns:
301297
ServerConfig object
302298
"""
303-
# Create a dictionary that ServerConfig.from_dict can work with
304299
server_data = {
305300
"name": server_name,
306-
"command": client_config.get("command", ""),
307-
"args": client_config.get("args", []),
308301
}
309-
310-
# Add environment variables if present
311-
if "env" in client_config:
312-
server_data["env"] = client_config["env"]
313-
314-
return ServerConfig.from_dict(server_data)
302+
server_data.update(client_config)
303+
return TypeAdapter(ServerConfig).validate_python(server_data)
315304

316305
def list_servers(self) -> List[str]:
317306
"""List all MCP servers in client config
@@ -374,7 +363,6 @@ class YAMLClientManager(BaseClientManager):
374363
def __init__(self):
375364
"""Initialize the YAML client manager"""
376365
super().__init__()
377-
self.config_path = None # To be set by subclasses
378366
self.yaml_handler: YAML = YAML()
379367

380368
def _load_config(self) -> Dict[str, Any]:
@@ -454,7 +442,9 @@ def _get_all_server_names(self, config: Dict[str, Any]) -> List[str]:
454442
pass
455443

456444
@abc.abstractmethod
457-
def _add_server_to_config(self, config: Dict[str, Any], server_name: str, server_config: Dict[str, Any]) -> Dict[str, Any]:
445+
def _add_server_to_config(
446+
self, config: Dict[str, Any], server_name: str, server_config: Dict[str, Any]
447+
) -> Dict[str, Any]:
458448
"""Add or update a server in the config
459449
460450
Args:
@@ -589,11 +579,7 @@ def get_client_info(self) -> Dict[str, str]:
589579
Returns:
590580
Dict: Information about the client including display name, download URL, and config path
591581
"""
592-
return {
593-
"name": self.display_name,
594-
"download_url": self.download_url,
595-
"config_file": self.config_path
596-
}
582+
return {"name": self.display_name, "download_url": self.download_url, "config_file": self.config_path}
597583

598584
def is_client_installed(self) -> bool:
599585
"""Check if this client is installed

src/mcpm/clients/client_registry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class ClientRegistry:
3939
"continue": ContinueManager(),
4040
"goose": GooseClientManager(),
4141
"5ire": FiveireManager(),
42-
"roo-code": RooCodeManager()
42+
"roo-code": RooCodeManager(),
4343
}
4444

4545
@classmethod
@@ -139,7 +139,7 @@ def get_active_client_manager(cls) -> Optional[BaseClientManager]:
139139
return cls.get_client_manager(active_client)
140140

141141
@classmethod
142-
def get_recommended_client(cls) -> str:
142+
def get_recommended_client(cls) -> str | None:
143143
"""
144144
Get the recommended client based on installation status
145145

src/mcpm/clients/managers/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@
55
"""
66

77
from mcpm.clients.managers.claude_desktop import ClaudeDesktopManager
8+
from mcpm.clients.managers.cline import ClineManager
9+
from mcpm.clients.managers.continue_extension import ContinueManager
810
from mcpm.clients.managers.cursor import CursorManager
11+
from mcpm.clients.managers.fiveire import FiveireManager
12+
from mcpm.clients.managers.goose import GooseClientManager
913
from mcpm.clients.managers.windsurf import WindsurfManager
1014

1115
__all__ = [
1216
"ClaudeDesktopManager",
1317
"CursorManager",
1418
"WindsurfManager",
19+
"ClineManager",
20+
"ContinueManager",
21+
"FiveireManager",
22+
"GooseClientManager",
1523
]

src/mcpm/clients/managers/continue_extension.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import os
77
from typing import Any, Dict, List, Optional
88

9+
from pydantic import TypeAdapter
10+
911
from mcpm.clients.base import YAMLClientManager
10-
from mcpm.utils.server_config import ServerConfig
12+
from mcpm.schemas.server_config import ServerConfig, STDIOServerConfig
1113

1214
logger = logging.getLogger(__name__)
1315

@@ -69,7 +71,7 @@ def _get_empty_config(self) -> Dict[str, Any]:
6971
"prompts": [],
7072
"context": [],
7173
"mcpServers": [],
72-
"data": []
74+
"data": [],
7375
}
7476

7577
def _get_servers_section(self, config: Dict[str, Any]) -> List[Dict[str, Any]]:
@@ -107,9 +109,11 @@ def _get_all_server_names(self, config: Dict[str, Any]) -> List[str]:
107109
Returns:
108110
List of server names
109111
"""
110-
return [server.get("name") for server in self._get_servers_section(config) if server.get("name") is not None] # type: ignore
112+
return [server.get("name") for server in self._get_servers_section(config) if server.get("name") is not None] # type: ignore
111113

112-
def _add_server_to_config(self, config: Dict[str, Any], server_name: str, server_config: Dict[str, Any]) -> Dict[str, Any]:
114+
def _add_server_to_config(
115+
self, config: Dict[str, Any], server_name: str, server_config: Dict[str, Any]
116+
) -> Dict[str, Any]:
113117
"""Add or update a server in the config
114118
115119
Args:
@@ -168,17 +172,19 @@ def to_client_format(self, server_config: ServerConfig) -> Dict[str, Any]:
168172
Returns:
169173
Dict containing client-specific configuration
170174
"""
171-
# Base result containing essential information
172-
result = {
173-
"name": server_config.name,
174-
"command": server_config.command,
175-
"args": server_config.args,
176-
}
177-
178-
# Add filtered environment variables if present
179-
non_empty_env = server_config.get_filtered_env_vars(os.environ)
180-
if non_empty_env:
181-
result["env"] = non_empty_env
175+
# Base result containing only essential execution information
176+
if isinstance(server_config, STDIOServerConfig):
177+
result = {
178+
"command": server_config.command,
179+
"args": server_config.args,
180+
}
181+
182+
# Add filtered environment variables if present
183+
non_empty_env = server_config.get_filtered_env_vars(os.environ)
184+
if non_empty_env:
185+
result["env"] = non_empty_env
186+
else:
187+
result = server_config.to_dict()
182188

183189
return result
184190

@@ -192,15 +198,8 @@ def from_client_format(self, server_name: str, client_config: Dict[str, Any]) ->
192198
Returns:
193199
ServerConfig object
194200
"""
195-
# Create a dictionary that ServerConfig.from_dict can work with
196201
server_data = {
197202
"name": server_name,
198-
"command": client_config.get("command", ""),
199-
"args": client_config.get("args", []),
200203
}
201-
202-
# Add environment variables if present
203-
if "env" in client_config:
204-
server_data["env"] = client_config["env"]
205-
206-
return ServerConfig.from_dict(server_data)
204+
server_data.update(client_config)
205+
return TypeAdapter(ServerConfig).validate_python(server_data)

src/mcpm/clients/managers/fiveire.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import re
55
from typing import Any, Dict, List, Optional, Union
66

7+
from pydantic import TypeAdapter
8+
79
from mcpm.clients.base import JSONClientManager
8-
from mcpm.utils.server_config import ServerConfig
10+
from mcpm.schemas.server_config import ServerConfig, STDIOServerConfig
911

1012
logger = logging.getLogger(__name__)
1113

12-
class FiveireManager(JSONClientManager):
1314

15+
class FiveireManager(JSONClientManager):
1416
# Client information
1517
client_key = "5ire"
1618
display_name = "5ire"
@@ -167,24 +169,32 @@ def to_client_format(self, server_config: ServerConfig) -> Dict[str, Any]:
167169
Returns:
168170
Dict containing client-specific configuration
169171
"""
172+
173+
if isinstance(server_config, STDIOServerConfig):
174+
result = {
175+
"command": server_config.command,
176+
"args": server_config.args,
177+
}
178+
179+
# Add filtered environment variables if present
180+
non_empty_env = server_config.get_filtered_env_vars(os.environ)
181+
if non_empty_env:
182+
result["env"] = non_empty_env
183+
else:
184+
result = server_config.to_dict()
185+
170186
# Base result containing essential information
171187
key_slug = re.sub(r"[^a-zA-Z0-9]", "", server_config.name)
172188
# If the key starts with a number, prepend an key prefix
173189
if key_slug and key_slug[0].isdigit():
174190
key_slug = f"key{key_slug}"
175191

176-
result = {
177-
"name": server_config.name,
178-
"key": key_slug,
179-
"command": server_config.command,
180-
"args": server_config.args,
181-
"isActive": True
182-
}
183-
184-
# Add filtered environment variables if present
185-
non_empty_env = server_config.get_filtered_env_vars(os.environ)
186-
if non_empty_env:
187-
result["env"] = non_empty_env
192+
result.update(
193+
{
194+
"key": key_slug,
195+
"isActive": True,
196+
}
197+
)
188198

189199
return result
190200

@@ -199,18 +209,11 @@ def from_client_format(cls, server_name: str, client_config: Dict[str, Any]) ->
199209
Returns:
200210
ServerConfig object
201211
"""
202-
# Create a dictionary that ServerConfig.from_dict can work with
203212
server_data = {
204213
"name": server_name,
205-
"command": client_config.get("command", ""),
206-
"args": client_config.get("args", []),
207214
}
208-
209-
# Add environment variables if present
210-
if "env" in client_config:
211-
server_data["env"] = client_config["env"]
212-
213-
return ServerConfig.from_dict(server_data)
215+
server_data.update(client_config)
216+
return TypeAdapter(ServerConfig).validate_python(server_data)
214217

215218
def list_servers(self) -> List[str]:
216219
"""List all MCP servers in client config
@@ -307,7 +310,4 @@ def is_server_disabled(self, server_name: str) -> bool:
307310
servers = self.get_servers()
308311

309312
# Check if the server exists and is not active
310-
return (
311-
server_name in servers
312-
and servers[server_name].get("isActive", True) is False
313-
)
313+
return server_name in servers and servers[server_name].get("isActive", True) is False

0 commit comments

Comments
 (0)