1212
1313from pydantic import TypeAdapter
1414
15- from mcpm .core .schema import ServerConfig
15+ from mcpm .core .schema import ServerConfig , ProfileMetadata
1616
1717DEFAULT_GLOBAL_CONFIG_PATH = os .path .expanduser ("~/.config/mcpm/servers.json" )
18+ DEFAULT_PROFILE_METADATA_PATH = os .path .expanduser ("~/.config/mcpm/profiles_metadata.json" )
1819
1920logger = 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