Skip to content

Commit 6c8d6f6

Browse files
committed
fix profile implementation
1 parent d1aa74d commit 6c8d6f6

File tree

8 files changed

+1008
-305
lines changed

8 files changed

+1008
-305
lines changed

src/mcpm/commands/profile/edit.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,9 @@ def edit_profile(profile_name, name, servers):
160160
# Create new profile with selected servers
161161
profile_config_manager.new_profile(new_name)
162162

163-
# Add selected servers to new profile
163+
# Add selected servers to new profile (using efficient tagging)
164164
for server_name in selected_servers:
165-
server_config = all_servers[server_name]
166-
profile_config_manager.set_profile(new_name, server_config)
165+
profile_config_manager.add_server_to_profile(new_name, server_name)
167166

168167
# Delete old profile
169168
profile_config_manager.delete_profile(profile_name)
@@ -174,10 +173,9 @@ def edit_profile(profile_name, name, servers):
174173
# Clear current servers
175174
profile_config_manager.clear_profile(profile_name)
176175

177-
# Add selected servers
176+
# Add selected servers (using efficient tagging)
178177
for server_name in selected_servers:
179-
server_config = all_servers[server_name]
180-
profile_config_manager.set_profile(profile_name, server_config)
178+
profile_config_manager.add_server_to_profile(profile_name, server_name)
181179

182180
console.print(f"[green]✅ Profile '[cyan]{profile_name}[/]' updated[/]")
183181

src/mcpm/commands/target_operations/common.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,11 @@ def global_remove_server(server_name: str) -> bool:
6060
console.print(f"[bold red]Error:[/] Server '{server_name}' not found in global configuration.")
6161
return False
6262

63-
# Remove from global config
63+
# Remove from global config (this automatically removes all profile tags)
6464
success = global_config_manager.remove_server(server_name)
6565

66-
if success:
67-
# Also remove from all profiles (clean up tags)
68-
profile_manager = ProfileConfigManager()
69-
profiles = profile_manager.list_profiles()
70-
71-
for profile_name, profile_servers in profiles.items():
72-
# Remove the server from this profile if it exists
73-
updated_servers = [s for s in profile_servers if s.name != server_name]
74-
if len(updated_servers) != len(profile_servers):
75-
# Server was found and removed from this profile
76-
profile_manager._profiles[profile_name] = updated_servers
77-
78-
# Save the updated profiles
79-
profile_manager._save_profiles()
66+
# No need for additional profile cleanup since virtual profiles
67+
# are managed automatically through profile tags on servers
8068

8169
return success
8270

src/mcpm/core/schema.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,25 @@
55

66
class BaseServerConfig(BaseModel):
77
name: str
8+
profile_tags: List[str] = []
89

910
def to_dict(self) -> Dict[str, Any]:
1011
return self.model_dump()
1112

13+
def add_profile_tag(self, tag: str) -> None:
14+
"""Add a profile tag to this server if not already present."""
15+
if tag not in self.profile_tags:
16+
self.profile_tags.append(tag)
17+
18+
def remove_profile_tag(self, tag: str) -> None:
19+
"""Remove a profile tag from this server if present."""
20+
if tag in self.profile_tags:
21+
self.profile_tags.remove(tag)
22+
23+
def has_profile_tag(self, tag: str) -> bool:
24+
"""Check if this server has a specific profile tag."""
25+
return tag in self.profile_tags
26+
1227

1328
class STDIOServerConfig(BaseServerConfig):
1429
command: str
@@ -82,7 +97,9 @@ class CustomServerConfig(BaseServerConfig):
8297
ServerConfig = Union[STDIOServerConfig, RemoteServerConfig, CustomServerConfig]
8398

8499

85-
class Profile(BaseModel):
100+
# Profile metadata - servers are now associated via virtual tags
101+
class ProfileMetadata(BaseModel):
86102
name: str
87-
api_key: Optional[str]
88-
servers: list[ServerConfig]
103+
api_key: Optional[str] = None
104+
description: Optional[str] = None
105+
# Additional metadata can be added here (sharing settings, etc.)

src/mcpm/global_config.py

Lines changed: 238 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212

1313
from pydantic import TypeAdapter
1414

15-
from mcpm.core.schema import ServerConfig
15+
from mcpm.core.schema import ServerConfig, ProfileMetadata
1616

1717
DEFAULT_GLOBAL_CONFIG_PATH = os.path.expanduser("~/.config/mcpm/servers.json")
18+
DEFAULT_PROFILE_METADATA_PATH = os.path.expanduser("~/.config/mcpm/profiles_metadata.json")
1819

1920
logger = logging.getLogger(__name__)
2021

@@ -26,10 +27,13 @@ class GlobalConfigManager:
2627
Profiles organize servers via tagging, but servers exist independently.
2728
"""
2829

29-
def __init__(self, config_path: str = DEFAULT_GLOBAL_CONFIG_PATH):
30+
def __init__(self, config_path: str = DEFAULT_GLOBAL_CONFIG_PATH,
31+
metadata_path: str = DEFAULT_PROFILE_METADATA_PATH):
3032
self.config_path = os.path.expanduser(config_path)
33+
self.metadata_path = os.path.expanduser(metadata_path)
3134
self.config_dir = os.path.dirname(self.config_path)
3235
self._servers: Dict[str, ServerConfig] = self._load_servers()
36+
self._profile_metadata: Dict[str, ProfileMetadata] = self._load_profile_metadata()
3337
self._ensure_dirs()
3438

3539
def _ensure_dirs(self) -> None:
@@ -66,6 +70,36 @@ def _save_servers(self) -> None:
6670
with open(self.config_path, "w", encoding="utf-8") as f:
6771
json.dump(servers_data, f, indent=2)
6872

73+
def _load_profile_metadata(self) -> Dict[str, ProfileMetadata]:
74+
"""Load profile metadata from the metadata configuration file."""
75+
if not os.path.exists(self.metadata_path):
76+
return {}
77+
78+
try:
79+
with open(self.metadata_path, "r", encoding="utf-8") as f:
80+
metadata_data = json.load(f) or {}
81+
except json.JSONDecodeError as e:
82+
logger.error(f"Error loading profile metadata from {self.metadata_path}: {e}")
83+
return {}
84+
85+
metadata = {}
86+
for name, meta_data in metadata_data.items():
87+
try:
88+
metadata[name] = ProfileMetadata.model_validate(meta_data)
89+
except Exception as e:
90+
logger.error(f"Error loading profile metadata {name}: {e}")
91+
continue
92+
93+
return metadata
94+
95+
def _save_profile_metadata(self) -> None:
96+
"""Save profile metadata to the metadata configuration file."""
97+
self._ensure_dirs()
98+
metadata_data = {name: meta.model_dump() for name, meta in self._profile_metadata.items()}
99+
100+
with open(self.metadata_path, "w", encoding="utf-8") as f:
101+
json.dump(metadata_data, f, indent=2)
102+
69103
def add_server(self, server_config: ServerConfig, force: bool = False) -> bool:
70104
"""Add a server to the global configuration.
71105
@@ -146,4 +180,205 @@ def update_server(self, server_config: ServerConfig) -> bool:
146180

147181
self._servers[server_config.name] = server_config
148182
self._save_servers()
149-
return True
183+
return True
184+
185+
# Virtual Profile Methods
186+
def get_servers_by_profile_tag(self, profile_tag: str) -> Dict[str, ServerConfig]:
187+
"""Get all servers that have a specific profile tag.
188+
189+
Args:
190+
profile_tag: The profile tag to filter by
191+
192+
Returns:
193+
Dict mapping server names to configurations that have the tag
194+
"""
195+
return {
196+
name: config for name, config in self._servers.items()
197+
if config.has_profile_tag(profile_tag)
198+
}
199+
200+
def add_profile_tag_to_server(self, server_name: str, profile_tag: str) -> bool:
201+
"""Add a profile tag to a specific server.
202+
203+
Args:
204+
server_name: Name of the server
205+
profile_tag: Profile tag to add
206+
207+
Returns:
208+
bool: Success or failure
209+
"""
210+
if server_name not in self._servers:
211+
logger.warning(f"Server '{server_name}' not found")
212+
return False
213+
214+
self._servers[server_name].add_profile_tag(profile_tag)
215+
self._save_servers()
216+
return True
217+
218+
def remove_profile_tag_from_server(self, server_name: str, profile_tag: str) -> bool:
219+
"""Remove a profile tag from a specific server.
220+
221+
Args:
222+
server_name: Name of the server
223+
profile_tag: Profile tag to remove
224+
225+
Returns:
226+
bool: Success or failure
227+
"""
228+
if server_name not in self._servers:
229+
logger.warning(f"Server '{server_name}' not found")
230+
return False
231+
232+
self._servers[server_name].remove_profile_tag(profile_tag)
233+
self._save_servers()
234+
return True
235+
236+
def get_all_profile_tags(self) -> List[str]:
237+
"""Get all unique profile tags across all servers.
238+
239+
Returns:
240+
List of unique profile tag names
241+
"""
242+
all_tags = set()
243+
for config in self._servers.values():
244+
all_tags.update(config.profile_tags)
245+
return sorted(list(all_tags))
246+
247+
def get_virtual_profiles(self) -> Dict[str, List[str]]:
248+
"""Get all virtual profiles and their associated server names.
249+
250+
Returns:
251+
Dict mapping profile names to lists of server names
252+
"""
253+
profiles = {}
254+
for server_name, config in self._servers.items():
255+
for tag in config.profile_tags:
256+
if tag not in profiles:
257+
profiles[tag] = []
258+
profiles[tag].append(server_name)
259+
return profiles
260+
261+
def delete_virtual_profile(self, profile_tag: str) -> int:
262+
"""Delete a virtual profile by removing the tag from all servers.
263+
264+
Args:
265+
profile_tag: Profile tag to remove from all servers
266+
267+
Returns:
268+
int: Number of servers that had the tag removed
269+
"""
270+
count = 0
271+
for config in self._servers.values():
272+
if config.has_profile_tag(profile_tag):
273+
config.remove_profile_tag(profile_tag)
274+
count += 1
275+
276+
if count > 0:
277+
self._save_servers()
278+
279+
return count
280+
281+
def virtual_profile_exists(self, profile_tag: str) -> bool:
282+
"""Check if a virtual profile exists (has any servers with the tag).
283+
284+
Args:
285+
profile_tag: Profile tag to check
286+
287+
Returns:
288+
bool: True if any server has this tag
289+
"""
290+
return any(config.has_profile_tag(profile_tag) for config in self._servers.values())
291+
292+
# Profile Metadata Methods
293+
def create_profile_metadata(self, name: str, api_key: Optional[str] = None,
294+
description: Optional[str] = None) -> bool:
295+
"""Create profile metadata.
296+
297+
Args:
298+
name: Profile name
299+
api_key: Optional API key for sharing
300+
description: Optional profile description
301+
302+
Returns:
303+
bool: Success or failure
304+
"""
305+
if name in self._profile_metadata:
306+
logger.warning(f"Profile metadata '{name}' already exists")
307+
return False
308+
309+
self._profile_metadata[name] = ProfileMetadata(
310+
name=name, api_key=api_key, description=description
311+
)
312+
self._save_profile_metadata()
313+
return True
314+
315+
def get_profile_metadata(self, name: str) -> Optional[ProfileMetadata]:
316+
"""Get profile metadata by name.
317+
318+
Args:
319+
name: Profile name
320+
321+
Returns:
322+
ProfileMetadata or None if not found
323+
"""
324+
return self._profile_metadata.get(name)
325+
326+
def update_profile_metadata(self, metadata: ProfileMetadata) -> bool:
327+
"""Update profile metadata.
328+
329+
Args:
330+
metadata: Updated profile metadata
331+
332+
Returns:
333+
bool: Success or failure
334+
"""
335+
self._profile_metadata[metadata.name] = metadata
336+
self._save_profile_metadata()
337+
return True
338+
339+
def delete_profile_metadata(self, name: str) -> bool:
340+
"""Delete profile metadata.
341+
342+
Args:
343+
name: Profile name
344+
345+
Returns:
346+
bool: Success or failure
347+
"""
348+
if name not in self._profile_metadata:
349+
logger.warning(f"Profile metadata '{name}' not found")
350+
return False
351+
352+
del self._profile_metadata[name]
353+
self._save_profile_metadata()
354+
return True
355+
356+
def list_profile_metadata(self) -> Dict[str, ProfileMetadata]:
357+
"""Get all profile metadata.
358+
359+
Returns:
360+
Dict mapping profile names to metadata
361+
"""
362+
return self._profile_metadata.copy()
363+
364+
def get_complete_profile(self, name: str) -> Optional[Dict[str, any]]:
365+
"""Get complete profile information including metadata and servers.
366+
367+
Args:
368+
name: Profile name
369+
370+
Returns:
371+
Dict with metadata and servers, or None if profile doesn't exist
372+
"""
373+
# Check if profile exists either as metadata or virtual profile
374+
metadata = self.get_profile_metadata(name)
375+
servers = self.get_servers_by_profile_tag(name)
376+
377+
if not metadata and not servers:
378+
return None
379+
380+
return {
381+
"name": name,
382+
"metadata": metadata.model_dump() if metadata else {"name": name},
383+
"servers": [config.model_dump() for config in servers.values()]
384+
}

0 commit comments

Comments
 (0)