code-puppy 0.0.134__py3-none-any.whl → 0.0.136__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. code_puppy/agent.py +15 -17
  2. code_puppy/agents/agent_manager.py +320 -9
  3. code_puppy/agents/base_agent.py +58 -2
  4. code_puppy/agents/runtime_manager.py +68 -42
  5. code_puppy/command_line/command_handler.py +82 -33
  6. code_puppy/command_line/mcp/__init__.py +10 -0
  7. code_puppy/command_line/mcp/add_command.py +183 -0
  8. code_puppy/command_line/mcp/base.py +35 -0
  9. code_puppy/command_line/mcp/handler.py +133 -0
  10. code_puppy/command_line/mcp/help_command.py +146 -0
  11. code_puppy/command_line/mcp/install_command.py +176 -0
  12. code_puppy/command_line/mcp/list_command.py +94 -0
  13. code_puppy/command_line/mcp/logs_command.py +126 -0
  14. code_puppy/command_line/mcp/remove_command.py +82 -0
  15. code_puppy/command_line/mcp/restart_command.py +92 -0
  16. code_puppy/command_line/mcp/search_command.py +117 -0
  17. code_puppy/command_line/mcp/start_all_command.py +126 -0
  18. code_puppy/command_line/mcp/start_command.py +98 -0
  19. code_puppy/command_line/mcp/status_command.py +185 -0
  20. code_puppy/command_line/mcp/stop_all_command.py +109 -0
  21. code_puppy/command_line/mcp/stop_command.py +79 -0
  22. code_puppy/command_line/mcp/test_command.py +107 -0
  23. code_puppy/command_line/mcp/utils.py +129 -0
  24. code_puppy/command_line/mcp/wizard_utils.py +259 -0
  25. code_puppy/command_line/model_picker_completion.py +21 -4
  26. code_puppy/command_line/prompt_toolkit_completion.py +9 -0
  27. code_puppy/main.py +23 -17
  28. code_puppy/mcp/__init__.py +42 -16
  29. code_puppy/mcp/async_lifecycle.py +51 -49
  30. code_puppy/mcp/blocking_startup.py +125 -113
  31. code_puppy/mcp/captured_stdio_server.py +63 -70
  32. code_puppy/mcp/circuit_breaker.py +63 -47
  33. code_puppy/mcp/config_wizard.py +169 -136
  34. code_puppy/mcp/dashboard.py +79 -71
  35. code_puppy/mcp/error_isolation.py +147 -100
  36. code_puppy/mcp/examples/retry_example.py +55 -42
  37. code_puppy/mcp/health_monitor.py +152 -141
  38. code_puppy/mcp/managed_server.py +100 -97
  39. code_puppy/mcp/manager.py +168 -156
  40. code_puppy/mcp/registry.py +148 -110
  41. code_puppy/mcp/retry_manager.py +63 -61
  42. code_puppy/mcp/server_registry_catalog.py +271 -225
  43. code_puppy/mcp/status_tracker.py +80 -80
  44. code_puppy/mcp/system_tools.py +47 -52
  45. code_puppy/messaging/message_queue.py +20 -13
  46. code_puppy/messaging/renderers.py +30 -15
  47. code_puppy/state_management.py +103 -0
  48. code_puppy/tui/app.py +64 -7
  49. code_puppy/tui/components/chat_view.py +3 -3
  50. code_puppy/tui/components/human_input_modal.py +12 -8
  51. code_puppy/tui/screens/__init__.py +2 -2
  52. code_puppy/tui/screens/mcp_install_wizard.py +208 -179
  53. code_puppy/tui/tests/test_agent_command.py +3 -3
  54. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
  55. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
  56. code_puppy/command_line/mcp_commands.py +0 -1789
  57. {code_puppy-0.0.134.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
  58. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
  59. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
  60. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,185 @@
1
+ """
2
+ MCP Status Command - Shows detailed status for MCP servers.
3
+ """
4
+
5
+ import logging
6
+ from datetime import datetime
7
+ from typing import List, Optional
8
+
9
+ from rich.panel import Panel
10
+
11
+ from code_puppy.mcp.managed_server import ServerState
12
+ from code_puppy.messaging import emit_info
13
+
14
+ from .base import MCPCommandBase
15
+ from .list_command import ListCommand
16
+ from .utils import (
17
+ find_server_id_by_name,
18
+ format_state_indicator,
19
+ format_uptime,
20
+ suggest_similar_servers,
21
+ )
22
+
23
+ # Configure logging
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class StatusCommand(MCPCommandBase):
28
+ """
29
+ Command handler for showing MCP server status.
30
+
31
+ Shows detailed status for a specific server or brief status for all servers.
32
+ """
33
+
34
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
35
+ """
36
+ Show detailed status for a specific server or all servers.
37
+
38
+ Args:
39
+ args: Command arguments, expects [server_name] (optional)
40
+ group_id: Optional message group ID for grouping related messages
41
+ """
42
+ if group_id is None:
43
+ group_id = self.generate_group_id()
44
+
45
+ try:
46
+ if args:
47
+ # Show detailed status for specific server
48
+ server_name = args[0]
49
+ server_id = find_server_id_by_name(self.manager, server_name)
50
+
51
+ if not server_id:
52
+ emit_info(
53
+ f"Server '{server_name}' not found", message_group=group_id
54
+ )
55
+ suggest_similar_servers(
56
+ self.manager, server_name, group_id=group_id
57
+ )
58
+ return
59
+
60
+ self._show_detailed_server_status(server_id, server_name, group_id)
61
+ else:
62
+ # Show brief status for all servers
63
+ list_command = ListCommand()
64
+ list_command.execute([], group_id=group_id)
65
+
66
+ except Exception as e:
67
+ logger.error(f"Error showing server status: {e}")
68
+ emit_info(f"Failed to get server status: {e}", message_group=group_id)
69
+
70
+ def _show_detailed_server_status(
71
+ self, server_id: str, server_name: str, group_id: Optional[str] = None
72
+ ) -> None:
73
+ """
74
+ Show comprehensive status information for a specific server.
75
+
76
+ Args:
77
+ server_id: ID of the server
78
+ server_name: Name of the server
79
+ group_id: Optional message group ID
80
+ """
81
+ if group_id is None:
82
+ group_id = self.generate_group_id()
83
+
84
+ try:
85
+ status = self.manager.get_server_status(server_id)
86
+
87
+ if not status.get("exists", True):
88
+ emit_info(
89
+ f"Server '{server_name}' not found or not accessible",
90
+ message_group=group_id,
91
+ )
92
+ return
93
+
94
+ # Create detailed status panel
95
+ status_lines = []
96
+
97
+ # Basic information
98
+ status_lines.append(f"[bold]Server:[/bold] {server_name}")
99
+ status_lines.append(f"[bold]ID:[/bold] {server_id}")
100
+ status_lines.append(
101
+ f"[bold]Type:[/bold] {status.get('type', 'unknown').upper()}"
102
+ )
103
+
104
+ # State and status
105
+ state = status.get("state", "unknown")
106
+ state_display = format_state_indicator(
107
+ ServerState(state)
108
+ if state in [s.value for s in ServerState]
109
+ else ServerState.STOPPED
110
+ )
111
+ status_lines.append(f"[bold]State:[/bold] {state_display}")
112
+
113
+ enabled = status.get("enabled", False)
114
+ status_lines.append(
115
+ f"[bold]Enabled:[/bold] {'✓ Yes' if enabled else '✗ No'}"
116
+ )
117
+
118
+ # Check async lifecycle manager status if available
119
+ try:
120
+ from code_puppy.mcp.async_lifecycle import get_lifecycle_manager
121
+
122
+ lifecycle_mgr = get_lifecycle_manager()
123
+ if lifecycle_mgr.is_running(server_id):
124
+ status_lines.append(
125
+ "[bold]Process:[/bold] [green]✓ Active (subprocess/connection running)[/green]"
126
+ )
127
+ else:
128
+ status_lines.append("[bold]Process:[/bold] [dim]Not active[/dim]")
129
+ except Exception:
130
+ pass # Lifecycle manager not available
131
+
132
+ quarantined = status.get("quarantined", False)
133
+ if quarantined:
134
+ status_lines.append("[bold]Quarantined:[/bold] [yellow]⚠ Yes[/yellow]")
135
+
136
+ # Timing information
137
+ uptime = status.get("tracker_uptime")
138
+ if uptime:
139
+ uptime_str = format_uptime(
140
+ uptime.total_seconds()
141
+ if hasattr(uptime, "total_seconds")
142
+ else uptime
143
+ )
144
+ status_lines.append(f"[bold]Uptime:[/bold] {uptime_str}")
145
+
146
+ # Error information
147
+ error_msg = status.get("error_message")
148
+ if error_msg:
149
+ status_lines.append(f"[bold]Error:[/bold] [red]{error_msg}[/red]")
150
+
151
+ # Event information
152
+ event_count = status.get("recent_events_count", 0)
153
+ status_lines.append(f"[bold]Recent Events:[/bold] {event_count}")
154
+
155
+ # Metadata
156
+ metadata = status.get("tracker_metadata", {})
157
+ if metadata:
158
+ status_lines.append(f"[bold]Metadata:[/bold] {len(metadata)} keys")
159
+
160
+ # Create and show the panel
161
+ panel_content = "\n".join(status_lines)
162
+ panel = Panel(
163
+ panel_content, title=f"🔌 {server_name} Status", border_style="cyan"
164
+ )
165
+
166
+ emit_info(panel, message_group=group_id)
167
+
168
+ # Show recent events if available
169
+ recent_events = status.get("recent_events", [])
170
+ if recent_events:
171
+ emit_info("\n📋 Recent Events:", message_group=group_id)
172
+ for event in recent_events[-5:]: # Show last 5 events
173
+ timestamp = datetime.fromisoformat(event["timestamp"])
174
+ time_str = timestamp.strftime("%H:%M:%S")
175
+ emit_info(
176
+ f" {time_str}: {event['message']}", message_group=group_id
177
+ )
178
+
179
+ except Exception as e:
180
+ logger.error(
181
+ f"Error getting detailed status for server '{server_name}': {e}"
182
+ )
183
+ emit_info(
184
+ f"[red]Error getting server status: {e}[/red]", message_group=group_id
185
+ )
@@ -0,0 +1,109 @@
1
+ """
2
+ MCP Stop All Command - Stops all running MCP servers.
3
+ """
4
+
5
+ import logging
6
+ import time
7
+ from typing import List, Optional
8
+
9
+ from code_puppy.mcp.managed_server import ServerState
10
+ from code_puppy.messaging import emit_info
11
+
12
+ from .base import MCPCommandBase
13
+
14
+ # Configure logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class StopAllCommand(MCPCommandBase):
19
+ """
20
+ Command handler for stopping all MCP servers.
21
+
22
+ Stops all running MCP servers and provides a summary of results.
23
+ """
24
+
25
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
26
+ """
27
+ Stop all running MCP servers.
28
+
29
+ Args:
30
+ args: Command arguments (unused)
31
+ group_id: Optional message group ID for grouping related messages
32
+ """
33
+ if group_id is None:
34
+ group_id = self.generate_group_id()
35
+
36
+ try:
37
+ servers = self.manager.list_servers()
38
+
39
+ if not servers:
40
+ emit_info("No servers registered", message_group=group_id)
41
+ return
42
+
43
+ stopped_count = 0
44
+ failed_count = 0
45
+
46
+ # Count running servers
47
+ running_servers = [s for s in servers if s.state == ServerState.RUNNING]
48
+
49
+ if not running_servers:
50
+ emit_info("No servers are currently running", message_group=group_id)
51
+ return
52
+
53
+ emit_info(
54
+ f"Stopping {len(running_servers)} running server(s)...",
55
+ message_group=group_id,
56
+ )
57
+
58
+ for server_info in running_servers:
59
+ server_id = server_info.id
60
+ server_name = server_info.name
61
+
62
+ # Try to stop the server
63
+ success = self.manager.stop_server_sync(server_id)
64
+
65
+ if success:
66
+ stopped_count += 1
67
+ emit_info(f" ✓ Stopped: {server_name}", message_group=group_id)
68
+ else:
69
+ failed_count += 1
70
+ emit_info(f" ✗ Failed: {server_name}", message_group=group_id)
71
+
72
+ # Summary
73
+ emit_info("", message_group=group_id)
74
+ if stopped_count > 0:
75
+ emit_info(f"Stopped {stopped_count} server(s)", message_group=group_id)
76
+ if failed_count > 0:
77
+ emit_info(
78
+ f"Failed to stop {failed_count} server(s)", message_group=group_id
79
+ )
80
+
81
+ # Reload agent if any servers were stopped
82
+ if stopped_count > 0:
83
+ # Give async tasks a moment to complete before reloading agent
84
+ try:
85
+ import asyncio
86
+
87
+ asyncio.get_running_loop() # Check if in async context
88
+ # If we're in async context, wait a bit for servers to stop
89
+ time.sleep(0.5) # Small delay to let async tasks progress
90
+ except RuntimeError:
91
+ pass # No async loop, servers will stop when needed
92
+
93
+ try:
94
+ from code_puppy.agents.runtime_manager import (
95
+ get_runtime_agent_manager,
96
+ )
97
+
98
+ manager = get_runtime_agent_manager()
99
+ manager.reload_agent()
100
+ emit_info(
101
+ "[dim]Agent reloaded with updated servers[/dim]",
102
+ message_group=group_id,
103
+ )
104
+ except Exception as e:
105
+ logger.warning(f"Could not reload agent: {e}")
106
+
107
+ except Exception as e:
108
+ logger.error(f"Error stopping all servers: {e}")
109
+ emit_info(f"Failed to stop servers: {e}", message_group=group_id)
@@ -0,0 +1,79 @@
1
+ """
2
+ MCP Stop Command - Stops a specific MCP server.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Optional
7
+
8
+ from code_puppy.messaging import emit_info
9
+
10
+ from .base import MCPCommandBase
11
+ from .utils import find_server_id_by_name, suggest_similar_servers
12
+
13
+ # Configure logging
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class StopCommand(MCPCommandBase):
18
+ """
19
+ Command handler for stopping MCP servers.
20
+
21
+ Stops a specific MCP server by name and reloads the agent.
22
+ """
23
+
24
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
25
+ """
26
+ Stop a specific MCP server.
27
+
28
+ Args:
29
+ args: Command arguments, expects [server_name]
30
+ group_id: Optional message group ID for grouping related messages
31
+ """
32
+ if group_id is None:
33
+ group_id = self.generate_group_id()
34
+
35
+ if not args:
36
+ emit_info(
37
+ "[yellow]Usage: /mcp stop <server_name>[/yellow]",
38
+ message_group=group_id,
39
+ )
40
+ return
41
+
42
+ server_name = args[0]
43
+
44
+ try:
45
+ # Find server by name
46
+ server_id = find_server_id_by_name(self.manager, server_name)
47
+ if not server_id:
48
+ emit_info(f"Server '{server_name}' not found", message_group=group_id)
49
+ suggest_similar_servers(self.manager, server_name, group_id=group_id)
50
+ return
51
+
52
+ # Stop the server (disable and stop process)
53
+ success = self.manager.stop_server_sync(server_id)
54
+
55
+ if success:
56
+ emit_info(f"✓ Stopped server: {server_name}", message_group=group_id)
57
+
58
+ # Reload the agent to remove the disabled server
59
+ try:
60
+ from code_puppy.agents.runtime_manager import (
61
+ get_runtime_agent_manager,
62
+ )
63
+
64
+ manager = get_runtime_agent_manager()
65
+ manager.reload_agent()
66
+ emit_info(
67
+ "[dim]Agent reloaded with updated servers[/dim]",
68
+ message_group=group_id,
69
+ )
70
+ except Exception as e:
71
+ logger.warning(f"Could not reload agent: {e}")
72
+ else:
73
+ emit_info(
74
+ f"✗ Failed to stop server: {server_name}", message_group=group_id
75
+ )
76
+
77
+ except Exception as e:
78
+ logger.error(f"Error stopping server '{server_name}': {e}")
79
+ emit_info(f"[red]Failed to stop server: {e}[/red]", message_group=group_id)
@@ -0,0 +1,107 @@
1
+ """
2
+ MCP Test Command - Tests connectivity to a specific MCP server.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Optional
7
+
8
+ from code_puppy.messaging import emit_info
9
+
10
+ from .base import MCPCommandBase
11
+ from .utils import find_server_id_by_name, suggest_similar_servers
12
+
13
+ # Configure logging
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class TestCommand(MCPCommandBase):
18
+ """
19
+ Command handler for testing MCP server connectivity.
20
+
21
+ Tests connectivity and basic functionality of a specific MCP server.
22
+ """
23
+
24
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
25
+ """
26
+ Test connectivity to a specific MCP server.
27
+
28
+ Args:
29
+ args: Command arguments, expects [server_name]
30
+ group_id: Optional message group ID for grouping related messages
31
+ """
32
+ if group_id is None:
33
+ group_id = self.generate_group_id()
34
+
35
+ if not args:
36
+ emit_info("Usage: /mcp test <server_name>", message_group=group_id)
37
+ return
38
+
39
+ server_name = args[0]
40
+
41
+ try:
42
+ # Find server by name
43
+ server_id = find_server_id_by_name(self.manager, server_name)
44
+ if not server_id:
45
+ emit_info(f"Server '{server_name}' not found", message_group=group_id)
46
+ suggest_similar_servers(self.manager, server_name, group_id=group_id)
47
+ return
48
+
49
+ # Get managed server
50
+ managed_server = self.manager.get_server(server_id)
51
+ if not managed_server:
52
+ emit_info(
53
+ f"Server '{server_name}' not accessible", message_group=group_id
54
+ )
55
+ return
56
+
57
+ emit_info(
58
+ f"🔍 Testing connectivity to server: {server_name}",
59
+ message_group=group_id,
60
+ )
61
+
62
+ # Basic connectivity test - try to get the pydantic server
63
+ try:
64
+ managed_server.get_pydantic_server() # Test server instantiation
65
+ emit_info(
66
+ "✓ Server instance created successfully", message_group=group_id
67
+ )
68
+
69
+ # Try to get server info if available
70
+ emit_info(
71
+ f" • Server type: {managed_server.config.type}",
72
+ message_group=group_id,
73
+ )
74
+ emit_info(
75
+ f" • Server enabled: {managed_server.is_enabled()}",
76
+ message_group=group_id,
77
+ )
78
+ emit_info(
79
+ f" • Server quarantined: {managed_server.is_quarantined()}",
80
+ message_group=group_id,
81
+ )
82
+
83
+ if not managed_server.is_enabled():
84
+ emit_info(
85
+ " • Server is disabled - enable it with '/mcp start'",
86
+ message_group=group_id,
87
+ )
88
+
89
+ if managed_server.is_quarantined():
90
+ emit_info(
91
+ " • Server is quarantined - may have recent errors",
92
+ message_group=group_id,
93
+ )
94
+
95
+ emit_info(
96
+ f"✓ Connectivity test passed for: {server_name}",
97
+ message_group=group_id,
98
+ )
99
+
100
+ except Exception as test_error:
101
+ emit_info(
102
+ f"✗ Connectivity test failed: {test_error}", message_group=group_id
103
+ )
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error testing server '{server_name}': {e}")
107
+ emit_info(f"[red]Error testing server: {e}[/red]", message_group=group_id)
@@ -0,0 +1,129 @@
1
+ """
2
+ MCP Command Utilities - Shared helper functions for MCP command handlers.
3
+
4
+ Provides common utility functions used across multiple MCP command modules.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from rich.text import Text
10
+
11
+ from code_puppy.mcp.managed_server import ServerState
12
+
13
+
14
+ def format_state_indicator(state: ServerState) -> Text:
15
+ """
16
+ Format a server state with appropriate color and icon.
17
+
18
+ Args:
19
+ state: Server state to format
20
+
21
+ Returns:
22
+ Rich Text object with colored state indicator
23
+ """
24
+ state_map = {
25
+ ServerState.RUNNING: ("✓ Run", "green"),
26
+ ServerState.STOPPED: ("✗ Stop", "red"),
27
+ ServerState.STARTING: ("↗ Start", "yellow"),
28
+ ServerState.STOPPING: ("↙ Stop", "yellow"),
29
+ ServerState.ERROR: ("⚠ Err", "red"),
30
+ ServerState.QUARANTINED: ("⏸ Quar", "yellow"),
31
+ }
32
+
33
+ display, color = state_map.get(state, ("? Unk", "dim"))
34
+ return Text(display, style=color)
35
+
36
+
37
+ def format_uptime(uptime_seconds: Optional[float]) -> str:
38
+ """
39
+ Format uptime in a human-readable format.
40
+
41
+ Args:
42
+ uptime_seconds: Uptime in seconds, or None
43
+
44
+ Returns:
45
+ Formatted uptime string
46
+ """
47
+ if uptime_seconds is None or uptime_seconds <= 0:
48
+ return "-"
49
+
50
+ # Convert to readable format
51
+ if uptime_seconds < 60:
52
+ return f"{int(uptime_seconds)}s"
53
+ elif uptime_seconds < 3600:
54
+ minutes = int(uptime_seconds // 60)
55
+ seconds = int(uptime_seconds % 60)
56
+ return f"{minutes}m {seconds}s"
57
+ else:
58
+ hours = int(uptime_seconds // 3600)
59
+ minutes = int((uptime_seconds % 3600) // 60)
60
+ return f"{hours}h {minutes}m"
61
+
62
+
63
+ def find_server_id_by_name(manager, server_name: str) -> Optional[str]:
64
+ """
65
+ Find a server ID by its name.
66
+
67
+ Args:
68
+ manager: MCP manager instance
69
+ server_name: Name of the server to find
70
+
71
+ Returns:
72
+ Server ID if found, None otherwise
73
+ """
74
+ import logging
75
+
76
+ logger = logging.getLogger(__name__)
77
+
78
+ try:
79
+ servers = manager.list_servers()
80
+ for server in servers:
81
+ if server.name.lower() == server_name.lower():
82
+ return server.id
83
+ return None
84
+ except Exception as e:
85
+ logger.error(f"Error finding server by name '{server_name}': {e}")
86
+ return None
87
+
88
+
89
+ def suggest_similar_servers(
90
+ manager, server_name: str, group_id: Optional[str] = None
91
+ ) -> None:
92
+ """
93
+ Suggest similar server names when a server is not found.
94
+
95
+ Args:
96
+ manager: MCP manager instance
97
+ server_name: The server name that was not found
98
+ group_id: Optional message group ID for grouping related messages
99
+ """
100
+ import logging
101
+
102
+ from code_puppy.messaging import emit_info
103
+
104
+ logger = logging.getLogger(__name__)
105
+
106
+ try:
107
+ servers = manager.list_servers()
108
+ if not servers:
109
+ emit_info("No servers are registered", message_group=group_id)
110
+ return
111
+
112
+ # Simple suggestion based on partial matching
113
+ suggestions = []
114
+ server_name_lower = server_name.lower()
115
+
116
+ for server in servers:
117
+ if server_name_lower in server.name.lower():
118
+ suggestions.append(server.name)
119
+
120
+ if suggestions:
121
+ emit_info(f"Did you mean: {', '.join(suggestions)}", message_group=group_id)
122
+ else:
123
+ server_names = [s.name for s in servers]
124
+ emit_info(
125
+ f"Available servers: {', '.join(server_names)}", message_group=group_id
126
+ )
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error suggesting similar servers: {e}")