code-puppy 0.0.127__py3-none-any.whl → 0.0.128__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.
- code_puppy/__init__.py +1 -0
- code_puppy/agent.py +65 -69
- code_puppy/agents/agent_code_puppy.py +0 -3
- code_puppy/agents/runtime_manager.py +212 -0
- code_puppy/command_line/command_handler.py +56 -25
- code_puppy/command_line/mcp_commands.py +1298 -0
- code_puppy/command_line/meta_command_handler.py +3 -2
- code_puppy/command_line/model_picker_completion.py +21 -8
- code_puppy/main.py +52 -157
- code_puppy/mcp/__init__.py +23 -0
- code_puppy/mcp/async_lifecycle.py +237 -0
- code_puppy/mcp/circuit_breaker.py +218 -0
- code_puppy/mcp/config_wizard.py +437 -0
- code_puppy/mcp/dashboard.py +291 -0
- code_puppy/mcp/error_isolation.py +360 -0
- code_puppy/mcp/examples/retry_example.py +208 -0
- code_puppy/mcp/health_monitor.py +549 -0
- code_puppy/mcp/managed_server.py +346 -0
- code_puppy/mcp/manager.py +701 -0
- code_puppy/mcp/registry.py +412 -0
- code_puppy/mcp/retry_manager.py +321 -0
- code_puppy/mcp/server_registry_catalog.py +751 -0
- code_puppy/mcp/status_tracker.py +355 -0
- code_puppy/messaging/spinner/textual_spinner.py +6 -2
- code_puppy/model_factory.py +19 -4
- code_puppy/models.json +8 -6
- code_puppy/tui/app.py +19 -27
- code_puppy/tui/tests/test_agent_command.py +22 -15
- {code_puppy-0.0.127.data → code_puppy-0.0.128.data}/data/code_puppy/models.json +8 -6
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.128.dist-info}/METADATA +2 -3
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.128.dist-info}/RECORD +34 -18
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.128.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.128.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.128.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Command Handler - Command line interface for managing MCP servers.
|
|
3
|
+
|
|
4
|
+
This module provides the MCPCommandHandler class that implements the /mcp command
|
|
5
|
+
interface for managing MCP servers at runtime. It provides commands for listing,
|
|
6
|
+
starting, stopping, configuring, and monitoring MCP servers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import shlex
|
|
11
|
+
from typing import List, Optional, Dict, Any
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.columns import Columns
|
|
19
|
+
|
|
20
|
+
from code_puppy.mcp.manager import get_mcp_manager, ServerInfo
|
|
21
|
+
from code_puppy.mcp.managed_server import ServerConfig, ServerState
|
|
22
|
+
from code_puppy.messaging import emit_info, emit_system_message
|
|
23
|
+
|
|
24
|
+
# Configure logging
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MCPCommandHandler:
|
|
29
|
+
"""
|
|
30
|
+
Command handler for MCP server management operations.
|
|
31
|
+
|
|
32
|
+
Provides the /mcp command interface that allows users to manage MCP servers
|
|
33
|
+
at runtime through commands like list, start, stop, restart, status, etc.
|
|
34
|
+
Uses Rich library for formatted output with tables, colors, and status indicators.
|
|
35
|
+
|
|
36
|
+
Example usage:
|
|
37
|
+
handler = MCPCommandHandler()
|
|
38
|
+
handler.handle_mcp_command("/mcp list")
|
|
39
|
+
handler.handle_mcp_command("/mcp start filesystem")
|
|
40
|
+
handler.handle_mcp_command("/mcp status filesystem")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
"""Initialize the MCP command handler."""
|
|
45
|
+
self.console = Console()
|
|
46
|
+
self.manager = get_mcp_manager()
|
|
47
|
+
logger.info("MCPCommandHandler initialized")
|
|
48
|
+
|
|
49
|
+
def handle_mcp_command(self, command: str) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Handle MCP commands and route to appropriate handler.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
command: The full command string (e.g., "/mcp list", "/mcp start server")
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if command was handled successfully, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
import uuid
|
|
60
|
+
# Generate a group ID for this entire MCP command session
|
|
61
|
+
group_id = str(uuid.uuid4())
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# Remove /mcp prefix and parse arguments
|
|
65
|
+
command = command.strip()
|
|
66
|
+
if not command.startswith("/mcp"):
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Remove the /mcp prefix
|
|
70
|
+
args_str = command[4:].strip()
|
|
71
|
+
|
|
72
|
+
# If no subcommand, show status dashboard
|
|
73
|
+
if not args_str:
|
|
74
|
+
self.cmd_list([], group_id=group_id)
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
# Parse arguments using shlex for proper handling of quoted strings
|
|
78
|
+
try:
|
|
79
|
+
args = shlex.split(args_str)
|
|
80
|
+
except ValueError as e:
|
|
81
|
+
emit_info(f"[red]Invalid command syntax: {e}[/red]", message_group=group_id)
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
if not args:
|
|
85
|
+
self.cmd_list([], group_id=group_id)
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
subcommand = args[0].lower()
|
|
89
|
+
sub_args = args[1:] if len(args) > 1 else []
|
|
90
|
+
|
|
91
|
+
# Route to appropriate command handler
|
|
92
|
+
command_map = {
|
|
93
|
+
'list': self.cmd_list,
|
|
94
|
+
'start': self.cmd_start,
|
|
95
|
+
'start-all': self.cmd_start_all,
|
|
96
|
+
'stop': self.cmd_stop,
|
|
97
|
+
'stop-all': self.cmd_stop_all,
|
|
98
|
+
'restart': self.cmd_restart,
|
|
99
|
+
'status': self.cmd_status,
|
|
100
|
+
'test': self.cmd_test,
|
|
101
|
+
'add': self.cmd_add,
|
|
102
|
+
'remove': self.cmd_remove,
|
|
103
|
+
'logs': self.cmd_logs,
|
|
104
|
+
'search': self.cmd_search,
|
|
105
|
+
'install': self.cmd_install,
|
|
106
|
+
'help': self.cmd_help,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
handler = command_map.get(subcommand)
|
|
110
|
+
if handler:
|
|
111
|
+
handler(sub_args)
|
|
112
|
+
return True
|
|
113
|
+
else:
|
|
114
|
+
emit_info(f"[yellow]Unknown MCP subcommand: {subcommand}[/yellow]", message_group=group_id)
|
|
115
|
+
emit_info("Type '/mcp help' for available commands", message_group=group_id)
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Error handling MCP command '{command}': {e}")
|
|
120
|
+
emit_info(f"Error executing MCP command: {e}", message_group=group_id)
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def cmd_list(self, args: List[str], group_id: str = None) -> None:
|
|
124
|
+
"""
|
|
125
|
+
List all registered MCP servers in a formatted table.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
args: Command arguments (unused for list command)
|
|
129
|
+
group_id: Optional message group ID for grouping related messages
|
|
130
|
+
"""
|
|
131
|
+
if group_id is None:
|
|
132
|
+
import uuid
|
|
133
|
+
group_id = str(uuid.uuid4())
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
servers = self.manager.list_servers()
|
|
137
|
+
|
|
138
|
+
if not servers:
|
|
139
|
+
emit_info("No MCP servers registered", message_group=group_id)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# Create table for server list
|
|
143
|
+
table = Table(title="🔌 MCP Server Status Dashboard")
|
|
144
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
145
|
+
table.add_column("Type", style="dim", no_wrap=True)
|
|
146
|
+
table.add_column("State", justify="center")
|
|
147
|
+
table.add_column("Enabled", justify="center")
|
|
148
|
+
table.add_column("Uptime", style="dim")
|
|
149
|
+
table.add_column("Status", style="dim")
|
|
150
|
+
|
|
151
|
+
for server in servers:
|
|
152
|
+
# Format state with appropriate color and icon
|
|
153
|
+
state_display = self._format_state_indicator(server.state)
|
|
154
|
+
|
|
155
|
+
# Format enabled status
|
|
156
|
+
enabled_display = "✓" if server.enabled else "✗"
|
|
157
|
+
enabled_style = "green" if server.enabled else "red"
|
|
158
|
+
|
|
159
|
+
# Format uptime
|
|
160
|
+
uptime_display = self._format_uptime(server.uptime_seconds)
|
|
161
|
+
|
|
162
|
+
# Format status message
|
|
163
|
+
status_display = server.error_message or "OK"
|
|
164
|
+
if server.quarantined:
|
|
165
|
+
status_display = "Quarantined"
|
|
166
|
+
|
|
167
|
+
table.add_row(
|
|
168
|
+
server.name,
|
|
169
|
+
server.type.upper(),
|
|
170
|
+
state_display,
|
|
171
|
+
Text(enabled_display, style=enabled_style),
|
|
172
|
+
uptime_display,
|
|
173
|
+
status_display
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
emit_info(table, message_group=group_id)
|
|
177
|
+
|
|
178
|
+
# Show summary
|
|
179
|
+
total = len(servers)
|
|
180
|
+
running = sum(1 for s in servers if s.state == ServerState.RUNNING and s.enabled)
|
|
181
|
+
emit_info(f"\n📊 Summary: {running}/{total} servers running", message_group=group_id)
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.error(f"Error listing MCP servers: {e}")
|
|
185
|
+
emit_info(f"Failed to list servers: {e}", message_group=group_id)
|
|
186
|
+
|
|
187
|
+
def cmd_start(self, args: List[str]) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Start a specific MCP server.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
args: Command arguments, expects [server_name]
|
|
193
|
+
"""
|
|
194
|
+
import uuid
|
|
195
|
+
group_id = str(uuid.uuid4())
|
|
196
|
+
|
|
197
|
+
if not args:
|
|
198
|
+
emit_info("[yellow]Usage: /mcp start <server_name>[/yellow]", message_group=group_id)
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
server_name = args[0]
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
# Find server by name
|
|
205
|
+
server_id = self._find_server_id_by_name(server_name)
|
|
206
|
+
if not server_id:
|
|
207
|
+
emit_info(f"[red]Server '{server_name}' not found[/red]", message_group=group_id)
|
|
208
|
+
self._suggest_similar_servers(server_name, group_id=group_id)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Start the server (enable and start process)
|
|
212
|
+
success = self.manager.start_server_sync(server_id)
|
|
213
|
+
|
|
214
|
+
if success:
|
|
215
|
+
# This and subsequent messages will auto-group with the first message
|
|
216
|
+
emit_info(f"[green]✓ Started server: {server_name}[/green]", message_group=group_id)
|
|
217
|
+
|
|
218
|
+
# Give async tasks a moment to complete
|
|
219
|
+
import asyncio
|
|
220
|
+
try:
|
|
221
|
+
loop = asyncio.get_running_loop()
|
|
222
|
+
# If we're in async context, wait a bit for server to start
|
|
223
|
+
import time
|
|
224
|
+
time.sleep(0.5) # Small delay to let async tasks progress
|
|
225
|
+
except RuntimeError:
|
|
226
|
+
pass # No async loop, server will start when agent uses it
|
|
227
|
+
|
|
228
|
+
# Reload the agent to pick up the newly enabled server
|
|
229
|
+
try:
|
|
230
|
+
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
231
|
+
manager = get_runtime_agent_manager()
|
|
232
|
+
manager.reload_agent()
|
|
233
|
+
emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.warning(f"Could not reload agent: {e}")
|
|
236
|
+
else:
|
|
237
|
+
emit_info(f"[red]✗ Failed to start server: {server_name}[/red]", message_group=group_id)
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Error starting server '{server_name}': {e}")
|
|
241
|
+
emit_info(f"[red]Failed to start server: {e}[/red]", message_group=group_id)
|
|
242
|
+
|
|
243
|
+
def cmd_start_all(self, args: List[str]) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Start all registered MCP servers.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
args: Command arguments (unused)
|
|
249
|
+
"""
|
|
250
|
+
import uuid
|
|
251
|
+
group_id = str(uuid.uuid4())
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
servers = self.manager.list_servers()
|
|
255
|
+
|
|
256
|
+
if not servers:
|
|
257
|
+
emit_info("[yellow]No servers registered[/yellow]", message_group=group_id)
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
started_count = 0
|
|
261
|
+
failed_count = 0
|
|
262
|
+
already_running = 0
|
|
263
|
+
|
|
264
|
+
emit_info(f"Starting {len(servers)} servers...", message_group=group_id)
|
|
265
|
+
|
|
266
|
+
for server_info in servers:
|
|
267
|
+
server_id = server_info.id
|
|
268
|
+
server_name = server_info.name
|
|
269
|
+
|
|
270
|
+
# Skip if already running
|
|
271
|
+
if server_info.state == ServerState.RUNNING:
|
|
272
|
+
already_running += 1
|
|
273
|
+
emit_info(f" • {server_name}: already running", message_group=group_id)
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Try to start the server
|
|
277
|
+
success = self.manager.start_server_sync(server_id)
|
|
278
|
+
|
|
279
|
+
if success:
|
|
280
|
+
started_count += 1
|
|
281
|
+
emit_info(f" [green]✓ Started: {server_name}[/green]", message_group=group_id)
|
|
282
|
+
else:
|
|
283
|
+
failed_count += 1
|
|
284
|
+
emit_info(f" [red]✗ Failed: {server_name}[/red]", message_group=group_id)
|
|
285
|
+
|
|
286
|
+
# Summary
|
|
287
|
+
emit_info("", message_group=group_id)
|
|
288
|
+
if started_count > 0:
|
|
289
|
+
emit_info(f"[green]Started {started_count} server(s)[/green]", message_group=group_id)
|
|
290
|
+
if already_running > 0:
|
|
291
|
+
emit_info(f"{already_running} server(s) already running", message_group=group_id)
|
|
292
|
+
if failed_count > 0:
|
|
293
|
+
emit_info(f"[yellow]Failed to start {failed_count} server(s)[/yellow]", message_group=group_id)
|
|
294
|
+
|
|
295
|
+
# Reload agent if any servers were started
|
|
296
|
+
if started_count > 0:
|
|
297
|
+
# Give async tasks a moment to complete before reloading agent
|
|
298
|
+
import asyncio
|
|
299
|
+
try:
|
|
300
|
+
loop = asyncio.get_running_loop()
|
|
301
|
+
# If we're in async context, wait a bit for servers to start
|
|
302
|
+
import time
|
|
303
|
+
time.sleep(0.5) # Small delay to let async tasks progress
|
|
304
|
+
except RuntimeError:
|
|
305
|
+
pass # No async loop, servers will start when agent uses them
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
309
|
+
manager = get_runtime_agent_manager()
|
|
310
|
+
manager.reload_agent()
|
|
311
|
+
emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.warning(f"Could not reload agent: {e}")
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.error(f"Error starting all servers: {e}")
|
|
317
|
+
emit_info(f"[red]Failed to start servers: {e}[/red]", message_group=group_id)
|
|
318
|
+
|
|
319
|
+
def cmd_stop(self, args: List[str]) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Stop a specific MCP server.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
args: Command arguments, expects [server_name]
|
|
325
|
+
"""
|
|
326
|
+
import uuid
|
|
327
|
+
group_id = str(uuid.uuid4())
|
|
328
|
+
|
|
329
|
+
if not args:
|
|
330
|
+
emit_info("[yellow]Usage: /mcp stop <server_name>[/yellow]", message_group=group_id)
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
server_name = args[0]
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
# Find server by name
|
|
337
|
+
server_id = self._find_server_id_by_name(server_name)
|
|
338
|
+
if not server_id:
|
|
339
|
+
emit_info(f"Server '{server_name}' not found", message_group=group_id)
|
|
340
|
+
self._suggest_similar_servers(server_name, group_id=group_id)
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
# Stop the server (disable and stop process)
|
|
344
|
+
success = self.manager.stop_server_sync(server_id)
|
|
345
|
+
|
|
346
|
+
if success:
|
|
347
|
+
emit_info(f"✓ Stopped server: {server_name}", message_group=group_id)
|
|
348
|
+
|
|
349
|
+
# Reload the agent to remove the disabled server
|
|
350
|
+
try:
|
|
351
|
+
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
352
|
+
manager = get_runtime_agent_manager()
|
|
353
|
+
manager.reload_agent()
|
|
354
|
+
emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.warning(f"Could not reload agent: {e}")
|
|
357
|
+
else:
|
|
358
|
+
emit_info(f"✗ Failed to stop server: {server_name}", message_group=group_id)
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(f"Error stopping server '{server_name}': {e}")
|
|
362
|
+
emit_info(f"Failed to stop server: {e}", message_group=group_id)
|
|
363
|
+
|
|
364
|
+
def cmd_stop_all(self, args: List[str]) -> None:
|
|
365
|
+
"""
|
|
366
|
+
Stop all running MCP servers.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
args: [group_id] - optional group ID for message grouping
|
|
370
|
+
"""
|
|
371
|
+
group_id = args[0] if args else None
|
|
372
|
+
if group_id is None:
|
|
373
|
+
import uuid
|
|
374
|
+
group_id = str(uuid.uuid4())
|
|
375
|
+
try:
|
|
376
|
+
servers = self.manager.list_servers()
|
|
377
|
+
|
|
378
|
+
if not servers:
|
|
379
|
+
emit_info("No servers registered", message_group=group_id)
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
stopped_count = 0
|
|
383
|
+
failed_count = 0
|
|
384
|
+
already_stopped = 0
|
|
385
|
+
|
|
386
|
+
# Count running servers
|
|
387
|
+
running_servers = [s for s in servers if s.state == ServerState.RUNNING]
|
|
388
|
+
|
|
389
|
+
if not running_servers:
|
|
390
|
+
emit_info("No servers are currently running", message_group=group_id)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
emit_info(f"Stopping {len(running_servers)} running server(s)...", message_group=group_id)
|
|
394
|
+
|
|
395
|
+
for server_info in running_servers:
|
|
396
|
+
server_id = server_info.id
|
|
397
|
+
server_name = server_info.name
|
|
398
|
+
|
|
399
|
+
# Try to stop the server
|
|
400
|
+
success = self.manager.stop_server_sync(server_id)
|
|
401
|
+
|
|
402
|
+
if success:
|
|
403
|
+
stopped_count += 1
|
|
404
|
+
emit_info(f" ✓ Stopped: {server_name}", message_group=group_id)
|
|
405
|
+
else:
|
|
406
|
+
failed_count += 1
|
|
407
|
+
emit_info(f" ✗ Failed: {server_name}", message_group=group_id)
|
|
408
|
+
|
|
409
|
+
# Summary
|
|
410
|
+
emit_info("", message_group=group_id)
|
|
411
|
+
if stopped_count > 0:
|
|
412
|
+
emit_info(f"Stopped {stopped_count} server(s)", message_group=group_id)
|
|
413
|
+
if failed_count > 0:
|
|
414
|
+
emit_info(f"Failed to stop {failed_count} server(s)", message_group=group_id)
|
|
415
|
+
|
|
416
|
+
# Reload agent if any servers were stopped
|
|
417
|
+
if stopped_count > 0:
|
|
418
|
+
# Give async tasks a moment to complete before reloading agent
|
|
419
|
+
import asyncio
|
|
420
|
+
try:
|
|
421
|
+
loop = asyncio.get_running_loop()
|
|
422
|
+
# If we're in async context, wait a bit for servers to stop
|
|
423
|
+
import time
|
|
424
|
+
time.sleep(0.5) # Small delay to let async tasks progress
|
|
425
|
+
except RuntimeError:
|
|
426
|
+
pass # No async loop, servers will stop when needed
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
430
|
+
manager = get_runtime_agent_manager()
|
|
431
|
+
manager.reload_agent()
|
|
432
|
+
emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
|
|
433
|
+
except Exception as e:
|
|
434
|
+
logger.warning(f"Could not reload agent: {e}")
|
|
435
|
+
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.error(f"Error stopping all servers: {e}")
|
|
438
|
+
emit_info(f"Failed to stop servers: {e}", message_group=group_id)
|
|
439
|
+
|
|
440
|
+
def cmd_restart(self, args: List[str]) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Restart a specific MCP server.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
args: Command arguments, expects [server_name]
|
|
446
|
+
"""
|
|
447
|
+
import uuid
|
|
448
|
+
group_id = str(uuid.uuid4())
|
|
449
|
+
|
|
450
|
+
if not args:
|
|
451
|
+
emit_info("Usage: /mcp restart <server_name>", message_group=group_id)
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
server_name = args[0]
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
# Find server by name
|
|
458
|
+
server_id = self._find_server_id_by_name(server_name)
|
|
459
|
+
if not server_id:
|
|
460
|
+
emit_info(f"Server '{server_name}' not found", message_group=group_id)
|
|
461
|
+
self._suggest_similar_servers(server_name)
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
# Stop the server first
|
|
465
|
+
emit_info(f"Stopping server: {server_name}", message_group=group_id)
|
|
466
|
+
self.manager.stop_server_sync(server_id)
|
|
467
|
+
|
|
468
|
+
# Then reload and start it
|
|
469
|
+
emit_info(f"Reloading configuration...", message_group=group_id)
|
|
470
|
+
reload_success = self.manager.reload_server(server_id)
|
|
471
|
+
|
|
472
|
+
if reload_success:
|
|
473
|
+
emit_info(f"Starting server: {server_name}", message_group=group_id)
|
|
474
|
+
start_success = self.manager.start_server_sync(server_id)
|
|
475
|
+
|
|
476
|
+
if start_success:
|
|
477
|
+
emit_info(f"✓ Restarted server: {server_name}", message_group=group_id)
|
|
478
|
+
|
|
479
|
+
# Reload the agent to pick up the server changes
|
|
480
|
+
try:
|
|
481
|
+
from code_puppy.agent import get_code_generation_agent
|
|
482
|
+
get_code_generation_agent(force_reload=True)
|
|
483
|
+
emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.warning(f"Could not reload agent: {e}")
|
|
486
|
+
else:
|
|
487
|
+
emit_info(f"✗ Failed to start server after reload: {server_name}", message_group=group_id)
|
|
488
|
+
else:
|
|
489
|
+
emit_info(f"✗ Failed to reload server configuration: {server_name}", message_group=group_id)
|
|
490
|
+
|
|
491
|
+
except Exception as e:
|
|
492
|
+
logger.error(f"Error restarting server '{server_name}': {e}")
|
|
493
|
+
emit_info(f"Failed to restart server: {e}", message_group=group_id)
|
|
494
|
+
|
|
495
|
+
def cmd_status(self, args: List[str]) -> None:
|
|
496
|
+
"""
|
|
497
|
+
Show detailed status for a specific server or all servers.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
args: Command arguments, expects [server_name] (optional)
|
|
501
|
+
"""
|
|
502
|
+
import uuid
|
|
503
|
+
group_id = str(uuid.uuid4())
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
if args:
|
|
507
|
+
# Show detailed status for specific server
|
|
508
|
+
server_name = args[0]
|
|
509
|
+
server_id = self._find_server_id_by_name(server_name)
|
|
510
|
+
|
|
511
|
+
if not server_id:
|
|
512
|
+
emit_info(f"Server '{server_name}' not found", message_group=group_id)
|
|
513
|
+
self._suggest_similar_servers(server_name)
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
self._show_detailed_server_status(server_id, server_name, group_id)
|
|
517
|
+
else:
|
|
518
|
+
# Show brief status for all servers
|
|
519
|
+
self.cmd_list([])
|
|
520
|
+
|
|
521
|
+
except Exception as e:
|
|
522
|
+
logger.error(f"Error showing server status: {e}")
|
|
523
|
+
emit_info(f"Failed to get server status: {e}", message_group=group_id)
|
|
524
|
+
|
|
525
|
+
def cmd_test(self, args: List[str]) -> None:
|
|
526
|
+
"""
|
|
527
|
+
Test connectivity to a specific MCP server.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
args: Command arguments, expects [server_name]
|
|
531
|
+
"""
|
|
532
|
+
import uuid
|
|
533
|
+
group_id = str(uuid.uuid4())
|
|
534
|
+
|
|
535
|
+
if not args:
|
|
536
|
+
emit_info("Usage: /mcp test <server_name>", message_group=group_id)
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
server_name = args[0]
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
# Find server by name
|
|
543
|
+
server_id = self._find_server_id_by_name(server_name)
|
|
544
|
+
if not server_id:
|
|
545
|
+
emit_info(f"Server '{server_name}' not found", message_group=group_id)
|
|
546
|
+
self._suggest_similar_servers(server_name)
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
# Get managed server
|
|
550
|
+
managed_server = self.manager.get_server(server_id)
|
|
551
|
+
if not managed_server:
|
|
552
|
+
emit_info(f"Server '{server_name}' not accessible", message_group=group_id)
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
emit_info(f"🔍 Testing connectivity to server: {server_name}", message_group=group_id)
|
|
556
|
+
|
|
557
|
+
# Basic connectivity test - try to get the pydantic server
|
|
558
|
+
try:
|
|
559
|
+
pydantic_server = managed_server.get_pydantic_server()
|
|
560
|
+
emit_info(f"✓ Server instance created successfully", message_group=group_id)
|
|
561
|
+
|
|
562
|
+
# Try to get server info if available
|
|
563
|
+
emit_info(f" • Server type: {managed_server.config.type}", message_group=group_id)
|
|
564
|
+
emit_info(f" • Server enabled: {managed_server.is_enabled()}", message_group=group_id)
|
|
565
|
+
emit_info(f" • Server quarantined: {managed_server.is_quarantined()}", message_group=group_id)
|
|
566
|
+
|
|
567
|
+
if not managed_server.is_enabled():
|
|
568
|
+
emit_info(" • Server is disabled - enable it with '/mcp start'", message_group=group_id)
|
|
569
|
+
|
|
570
|
+
if managed_server.is_quarantined():
|
|
571
|
+
emit_info(" • Server is quarantined - may have recent errors", message_group=group_id)
|
|
572
|
+
|
|
573
|
+
emit_info(f"✓ Connectivity test passed for: {server_name}", message_group=group_id)
|
|
574
|
+
|
|
575
|
+
except Exception as test_error:
|
|
576
|
+
emit_info(f"✗ Connectivity test failed: {test_error}", message_group=group_id)
|
|
577
|
+
|
|
578
|
+
except Exception as e:
|
|
579
|
+
logger.error(f"Error testing server '{server_name}': {e}")
|
|
580
|
+
emit_info(f"Failed to test server: {e}", message_group=group_id)
|
|
581
|
+
|
|
582
|
+
def cmd_add(self, args: List[str]) -> None:
|
|
583
|
+
"""
|
|
584
|
+
Add a new MCP server from JSON configuration or launch wizard.
|
|
585
|
+
|
|
586
|
+
Usage:
|
|
587
|
+
/mcp add - Launch interactive wizard
|
|
588
|
+
/mcp add <json> - Add server from JSON config
|
|
589
|
+
|
|
590
|
+
Example JSON:
|
|
591
|
+
/mcp add {"name": "test", "type": "stdio", "command": "echo", "args": ["hello"]}
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
args: Command arguments - JSON config or empty for wizard
|
|
595
|
+
"""
|
|
596
|
+
import uuid
|
|
597
|
+
group_id = str(uuid.uuid4())
|
|
598
|
+
|
|
599
|
+
try:
|
|
600
|
+
if args:
|
|
601
|
+
# Parse JSON from arguments
|
|
602
|
+
import json
|
|
603
|
+
json_str = ' '.join(args)
|
|
604
|
+
|
|
605
|
+
try:
|
|
606
|
+
config_dict = json.loads(json_str)
|
|
607
|
+
except json.JSONDecodeError as e:
|
|
608
|
+
emit_info(f"Invalid JSON: {e}", message_group=group_id)
|
|
609
|
+
emit_info("Usage: /mcp add <json> or /mcp add (for wizard)", message_group=group_id)
|
|
610
|
+
emit_info('Example: /mcp add {"name": "test", "type": "stdio", "command": "echo"}', message_group=group_id)
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
# Validate required fields
|
|
614
|
+
if 'name' not in config_dict:
|
|
615
|
+
emit_info("Missing required field: 'name'", message_group=group_id)
|
|
616
|
+
return
|
|
617
|
+
if 'type' not in config_dict:
|
|
618
|
+
emit_info("Missing required field: 'type'", message_group=group_id)
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
# Create ServerConfig
|
|
622
|
+
from code_puppy.mcp import ServerConfig
|
|
623
|
+
|
|
624
|
+
name = config_dict.pop('name')
|
|
625
|
+
server_type = config_dict.pop('type')
|
|
626
|
+
enabled = config_dict.pop('enabled', True)
|
|
627
|
+
|
|
628
|
+
# Everything else goes into config
|
|
629
|
+
server_config = ServerConfig(
|
|
630
|
+
id=f"{name}_{hash(name)}",
|
|
631
|
+
name=name,
|
|
632
|
+
type=server_type,
|
|
633
|
+
enabled=enabled,
|
|
634
|
+
config=config_dict # Remaining fields are server-specific config
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# Register the server
|
|
638
|
+
server_id = self.manager.register_server(server_config)
|
|
639
|
+
|
|
640
|
+
if server_id:
|
|
641
|
+
emit_info(f"✅ Added server '{name}' (ID: {server_id})", message_group=group_id)
|
|
642
|
+
|
|
643
|
+
# Save to mcp_servers.json for persistence
|
|
644
|
+
from code_puppy.config import MCP_SERVERS_FILE
|
|
645
|
+
import os
|
|
646
|
+
|
|
647
|
+
# Load existing configs
|
|
648
|
+
if os.path.exists(MCP_SERVERS_FILE):
|
|
649
|
+
with open(MCP_SERVERS_FILE, 'r') as f:
|
|
650
|
+
data = json.load(f)
|
|
651
|
+
servers = data.get("mcp_servers", {})
|
|
652
|
+
else:
|
|
653
|
+
servers = {}
|
|
654
|
+
data = {"mcp_servers": servers}
|
|
655
|
+
|
|
656
|
+
# Add new server
|
|
657
|
+
servers[name] = config_dict
|
|
658
|
+
servers[name]['type'] = server_type
|
|
659
|
+
|
|
660
|
+
# Save back
|
|
661
|
+
os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
|
|
662
|
+
with open(MCP_SERVERS_FILE, 'w') as f:
|
|
663
|
+
json.dump(data, f, indent=2)
|
|
664
|
+
|
|
665
|
+
# Reload MCP servers
|
|
666
|
+
from code_puppy.agent import reload_mcp_servers
|
|
667
|
+
reload_mcp_servers()
|
|
668
|
+
|
|
669
|
+
emit_info("Use '/mcp list' to see all servers", message_group=group_id)
|
|
670
|
+
else:
|
|
671
|
+
emit_info(f"Failed to add server '{name}'", message_group=group_id)
|
|
672
|
+
|
|
673
|
+
else:
|
|
674
|
+
# No arguments - launch interactive wizard
|
|
675
|
+
from code_puppy.mcp.config_wizard import run_add_wizard
|
|
676
|
+
|
|
677
|
+
success = run_add_wizard()
|
|
678
|
+
|
|
679
|
+
if success:
|
|
680
|
+
# Reload the agent to pick up new server
|
|
681
|
+
from code_puppy.agent import reload_mcp_servers
|
|
682
|
+
reload_mcp_servers()
|
|
683
|
+
|
|
684
|
+
except ImportError as e:
|
|
685
|
+
logger.error(f"Failed to import: {e}")
|
|
686
|
+
emit_info("Required module not available", message_group=group_id)
|
|
687
|
+
except Exception as e:
|
|
688
|
+
logger.error(f"Error adding server: {e}")
|
|
689
|
+
emit_info(f"Failed to add server: {e}", message_group=group_id)
|
|
690
|
+
|
|
691
|
+
def cmd_remove(self, args: List[str]) -> None:
|
|
692
|
+
"""
|
|
693
|
+
Remove an MCP server.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
args: Command arguments, expects [server_name]
|
|
697
|
+
"""
|
|
698
|
+
import uuid
|
|
699
|
+
group_id = str(uuid.uuid4())
|
|
700
|
+
|
|
701
|
+
if not args:
|
|
702
|
+
emit_info("Usage: /mcp remove <server_name>", message_group=group_id)
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
server_name = args[0]
|
|
706
|
+
|
|
707
|
+
try:
|
|
708
|
+
# Find server by name
|
|
709
|
+
server_id = self._find_server_id_by_name(server_name)
|
|
710
|
+
if not server_id:
|
|
711
|
+
emit_info(f"Server '{server_name}' not found", message_group=group_id)
|
|
712
|
+
self._suggest_similar_servers(server_name)
|
|
713
|
+
return
|
|
714
|
+
|
|
715
|
+
# Actually remove the server
|
|
716
|
+
success = self.manager.remove_server(server_id)
|
|
717
|
+
|
|
718
|
+
if success:
|
|
719
|
+
emit_info(f"✓ Removed server: {server_name}", message_group=group_id)
|
|
720
|
+
|
|
721
|
+
# Also remove from mcp_servers.json
|
|
722
|
+
from code_puppy.config import MCP_SERVERS_FILE
|
|
723
|
+
import json
|
|
724
|
+
import os
|
|
725
|
+
|
|
726
|
+
if os.path.exists(MCP_SERVERS_FILE):
|
|
727
|
+
try:
|
|
728
|
+
with open(MCP_SERVERS_FILE, 'r') as f:
|
|
729
|
+
data = json.load(f)
|
|
730
|
+
servers = data.get("mcp_servers", {})
|
|
731
|
+
|
|
732
|
+
# Remove the server if it exists
|
|
733
|
+
if server_name in servers:
|
|
734
|
+
del servers[server_name]
|
|
735
|
+
|
|
736
|
+
# Save back
|
|
737
|
+
with open(MCP_SERVERS_FILE, 'w') as f:
|
|
738
|
+
json.dump(data, f, indent=2)
|
|
739
|
+
except Exception as e:
|
|
740
|
+
logger.warning(f"Could not update mcp_servers.json: {e}")
|
|
741
|
+
else:
|
|
742
|
+
emit_info(f"✗ Failed to remove server: {server_name}", message_group=group_id)
|
|
743
|
+
|
|
744
|
+
except Exception as e:
|
|
745
|
+
logger.error(f"Error removing server '{server_name}': {e}")
|
|
746
|
+
emit_info(f"Failed to remove server: {e}", message_group=group_id)
|
|
747
|
+
|
|
748
|
+
def cmd_logs(self, args: List[str]) -> None:
|
|
749
|
+
"""
|
|
750
|
+
Show recent events/logs for a server.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
args: Command arguments, expects [server_name] and optional [limit]
|
|
754
|
+
"""
|
|
755
|
+
import uuid
|
|
756
|
+
group_id = str(uuid.uuid4())
|
|
757
|
+
|
|
758
|
+
if not args:
|
|
759
|
+
emit_info("Usage: /mcp logs <server_name> [limit]", message_group=group_id)
|
|
760
|
+
return
|
|
761
|
+
|
|
762
|
+
server_name = args[0]
|
|
763
|
+
limit = 10 # Default limit
|
|
764
|
+
|
|
765
|
+
if len(args) > 1:
|
|
766
|
+
try:
|
|
767
|
+
limit = int(args[1])
|
|
768
|
+
if limit <= 0 or limit > 100:
|
|
769
|
+
emit_info("Limit must be between 1 and 100, using default: 10", message_group=group_id)
|
|
770
|
+
limit = 10
|
|
771
|
+
except ValueError:
|
|
772
|
+
emit_info(f"Invalid limit '{args[1]}', using default: 10", message_group=group_id)
|
|
773
|
+
|
|
774
|
+
try:
|
|
775
|
+
# Find server by name
|
|
776
|
+
server_id = self._find_server_id_by_name(server_name)
|
|
777
|
+
if not server_id:
|
|
778
|
+
emit_info(f"Server '{server_name}' not found", message_group=group_id)
|
|
779
|
+
self._suggest_similar_servers(server_name)
|
|
780
|
+
return
|
|
781
|
+
|
|
782
|
+
# Get server status which includes recent events
|
|
783
|
+
status = self.manager.get_server_status(server_id)
|
|
784
|
+
|
|
785
|
+
if not status.get("exists", True):
|
|
786
|
+
emit_info(f"Server '{server_name}' status not available", message_group=group_id)
|
|
787
|
+
return
|
|
788
|
+
|
|
789
|
+
recent_events = status.get("recent_events", [])
|
|
790
|
+
|
|
791
|
+
if not recent_events:
|
|
792
|
+
emit_info(f"No recent events for server: {server_name}", message_group=group_id)
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
# Show events in a table
|
|
796
|
+
table = Table(title=f"📋 Recent Events for {server_name} (last {limit})")
|
|
797
|
+
table.add_column("Time", style="dim", no_wrap=True)
|
|
798
|
+
table.add_column("Event", style="cyan")
|
|
799
|
+
table.add_column("Details", style="dim")
|
|
800
|
+
|
|
801
|
+
# Take only the requested number of events
|
|
802
|
+
events_to_show = recent_events[-limit:] if len(recent_events) > limit else recent_events
|
|
803
|
+
|
|
804
|
+
for event in reversed(events_to_show): # Show newest first
|
|
805
|
+
timestamp = datetime.fromisoformat(event["timestamp"])
|
|
806
|
+
time_str = timestamp.strftime("%H:%M:%S")
|
|
807
|
+
event_type = event["event_type"]
|
|
808
|
+
|
|
809
|
+
# Format details
|
|
810
|
+
details = event.get("details", {})
|
|
811
|
+
details_str = details.get("message", "")
|
|
812
|
+
if not details_str and "error" in details:
|
|
813
|
+
details_str = str(details["error"])
|
|
814
|
+
|
|
815
|
+
# Color code event types
|
|
816
|
+
event_style = "cyan"
|
|
817
|
+
if "error" in event_type.lower():
|
|
818
|
+
event_style = "red"
|
|
819
|
+
elif event_type in ["started", "enabled", "registered"]:
|
|
820
|
+
event_style = "green"
|
|
821
|
+
elif event_type in ["stopped", "disabled"]:
|
|
822
|
+
event_style = "yellow"
|
|
823
|
+
|
|
824
|
+
table.add_row(
|
|
825
|
+
time_str,
|
|
826
|
+
Text(event_type, style=event_style),
|
|
827
|
+
details_str or "-"
|
|
828
|
+
)
|
|
829
|
+
emit_info(table, message_group=group_id)
|
|
830
|
+
|
|
831
|
+
except Exception as e:
|
|
832
|
+
logger.error(f"Error getting logs for server '{server_name}': {e}")
|
|
833
|
+
emit_info(f"Failed to get server logs: {e}", message_group=group_id)
|
|
834
|
+
|
|
835
|
+
def cmd_help(self, args: List[str]) -> None:
|
|
836
|
+
"""
|
|
837
|
+
Show help for MCP commands.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
args: Command arguments (unused)
|
|
841
|
+
"""
|
|
842
|
+
from rich.text import Text
|
|
843
|
+
from rich.console import Console
|
|
844
|
+
|
|
845
|
+
# Create a console for rendering
|
|
846
|
+
console = Console()
|
|
847
|
+
|
|
848
|
+
# Build help text programmatically to avoid markup conflicts
|
|
849
|
+
help_lines = []
|
|
850
|
+
|
|
851
|
+
# Title
|
|
852
|
+
help_lines.append(Text("MCP Server Management Commands", style="bold magenta"))
|
|
853
|
+
help_lines.append(Text(""))
|
|
854
|
+
|
|
855
|
+
# Registry Commands
|
|
856
|
+
help_lines.append(Text("Registry Commands:", style="bold cyan"))
|
|
857
|
+
help_lines.append(Text("/mcp search", style="cyan") + Text(" [query] Search 30+ pre-configured servers"))
|
|
858
|
+
help_lines.append(Text("/mcp install", style="cyan") + Text(" <id> Install server from registry"))
|
|
859
|
+
help_lines.append(Text(""))
|
|
860
|
+
|
|
861
|
+
# Core Commands
|
|
862
|
+
help_lines.append(Text("Core Commands:", style="bold cyan"))
|
|
863
|
+
help_lines.append(Text("/mcp", style="cyan") + Text(" Show server status dashboard"))
|
|
864
|
+
help_lines.append(Text("/mcp list", style="cyan") + Text(" List all registered servers"))
|
|
865
|
+
help_lines.append(Text("/mcp start", style="cyan") + Text(" <name> Start a specific server"))
|
|
866
|
+
help_lines.append(Text("/mcp start-all", style="cyan") + Text(" Start all servers"))
|
|
867
|
+
help_lines.append(Text("/mcp stop", style="cyan") + Text(" <name> Stop a specific server"))
|
|
868
|
+
help_lines.append(Text("/mcp stop-all", style="cyan") + Text(" [group_id] Stop all running servers"))
|
|
869
|
+
help_lines.append(Text("/mcp restart", style="cyan") + Text(" <name> Restart a specific server"))
|
|
870
|
+
help_lines.append(Text(""))
|
|
871
|
+
|
|
872
|
+
# Management Commands
|
|
873
|
+
help_lines.append(Text("Management Commands:", style="bold cyan"))
|
|
874
|
+
help_lines.append(Text("/mcp status", style="cyan") + Text(" [name] Show detailed status (all servers or specific)"))
|
|
875
|
+
help_lines.append(Text("/mcp test", style="cyan") + Text(" <name> Test connectivity to a server"))
|
|
876
|
+
help_lines.append(Text("/mcp logs", style="cyan") + Text(" <name> [limit] Show recent events (default limit: 10)"))
|
|
877
|
+
help_lines.append(Text("/mcp add", style="cyan") + Text(" [json] Add new server (JSON or wizard)"))
|
|
878
|
+
help_lines.append(Text("/mcp remove", style="cyan") + Text(" <name> Remove/disable a server"))
|
|
879
|
+
help_lines.append(Text("/mcp help", style="cyan") + Text(" Show this help message"))
|
|
880
|
+
help_lines.append(Text(""))
|
|
881
|
+
|
|
882
|
+
# Status Indicators
|
|
883
|
+
help_lines.append(Text("Status Indicators:", style="bold"))
|
|
884
|
+
help_lines.append(Text("✓ Running ✗ Stopped ⚠ Error ⏸ Quarantined ⭐ Popular"))
|
|
885
|
+
help_lines.append(Text(""))
|
|
886
|
+
|
|
887
|
+
# Examples
|
|
888
|
+
help_lines.append(Text("Examples:", style="bold"))
|
|
889
|
+
examples_text = """/mcp search database # Find database servers
|
|
890
|
+
/mcp install postgres # Install PostgreSQL server
|
|
891
|
+
/mcp start filesystem # Start a specific server
|
|
892
|
+
/mcp start-all # Start all servers at once
|
|
893
|
+
/mcp stop-all # Stop all running servers
|
|
894
|
+
/mcp add {"name": "test", "type": "stdio", "command": "echo"}"""
|
|
895
|
+
help_lines.append(Text(examples_text, style="dim"))
|
|
896
|
+
|
|
897
|
+
# Combine all lines
|
|
898
|
+
final_text = Text()
|
|
899
|
+
for i, line in enumerate(help_lines):
|
|
900
|
+
if i > 0:
|
|
901
|
+
final_text.append("\n")
|
|
902
|
+
final_text.append_text(line)
|
|
903
|
+
|
|
904
|
+
import uuid
|
|
905
|
+
group_id = str(uuid.uuid4())
|
|
906
|
+
emit_info(final_text, message_group=group_id)
|
|
907
|
+
|
|
908
|
+
def cmd_search(self, args: List[str], group_id: str = None) -> None:
|
|
909
|
+
"""
|
|
910
|
+
Search for pre-configured MCP servers in the registry.
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
args: Search query terms
|
|
914
|
+
group_id: Optional message group ID for grouping related messages
|
|
915
|
+
"""
|
|
916
|
+
if group_id is None:
|
|
917
|
+
import uuid
|
|
918
|
+
group_id = str(uuid.uuid4())
|
|
919
|
+
|
|
920
|
+
try:
|
|
921
|
+
from code_puppy.mcp.server_registry_catalog import catalog
|
|
922
|
+
from rich.table import Table
|
|
923
|
+
|
|
924
|
+
if not args:
|
|
925
|
+
# Show popular servers if no query
|
|
926
|
+
emit_info("[bold cyan]Popular MCP Servers:[/bold cyan]\n", message_group=group_id)
|
|
927
|
+
servers = catalog.get_popular(15)
|
|
928
|
+
else:
|
|
929
|
+
query = ' '.join(args)
|
|
930
|
+
emit_info(f"[bold cyan]Searching for: {query}[/bold cyan]\n", message_group=group_id)
|
|
931
|
+
servers = catalog.search(query)
|
|
932
|
+
|
|
933
|
+
if not servers:
|
|
934
|
+
emit_info("[yellow]No servers found matching your search[/yellow]", message_group=group_id)
|
|
935
|
+
emit_info("Try: /mcp search database, /mcp search file, /mcp search git", message_group=group_id)
|
|
936
|
+
return
|
|
937
|
+
|
|
938
|
+
# Create results table
|
|
939
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
940
|
+
table.add_column("ID", style="cyan", width=20)
|
|
941
|
+
table.add_column("Name", style="green")
|
|
942
|
+
table.add_column("Category", style="yellow")
|
|
943
|
+
table.add_column("Description", style="white")
|
|
944
|
+
table.add_column("Tags", style="dim")
|
|
945
|
+
|
|
946
|
+
for server in servers[:20]: # Limit to 20 results
|
|
947
|
+
tags = ', '.join(server.tags[:3]) # Show first 3 tags
|
|
948
|
+
if len(server.tags) > 3:
|
|
949
|
+
tags += '...'
|
|
950
|
+
|
|
951
|
+
# Add verified/popular indicators
|
|
952
|
+
indicators = []
|
|
953
|
+
if server.verified:
|
|
954
|
+
indicators.append("✓")
|
|
955
|
+
if server.popular:
|
|
956
|
+
indicators.append("⭐")
|
|
957
|
+
name_display = server.display_name
|
|
958
|
+
if indicators:
|
|
959
|
+
name_display += f" {''.join(indicators)}"
|
|
960
|
+
|
|
961
|
+
table.add_row(
|
|
962
|
+
server.id,
|
|
963
|
+
name_display,
|
|
964
|
+
server.category,
|
|
965
|
+
server.description[:50] + "..." if len(server.description) > 50 else server.description,
|
|
966
|
+
tags
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
# The first message established the group, subsequent messages will auto-group
|
|
970
|
+
emit_system_message(table, message_group=group_id)
|
|
971
|
+
emit_info("\n[dim]✓ = Verified ⭐ = Popular[/dim]", message_group=group_id)
|
|
972
|
+
emit_info("[yellow]To install:[/yellow] /mcp install <id>", message_group=group_id)
|
|
973
|
+
emit_info("[yellow]For details:[/yellow] /mcp search <specific-term>", message_group=group_id)
|
|
974
|
+
|
|
975
|
+
except ImportError:
|
|
976
|
+
emit_info("[red]Server registry not available[/red]", message_group=group_id)
|
|
977
|
+
except Exception as e:
|
|
978
|
+
logger.error(f"Error searching servers: {e}")
|
|
979
|
+
emit_info(f"[red]Search failed: {e}[/red]", message_group=group_id)
|
|
980
|
+
|
|
981
|
+
def cmd_install(self, args: List[str], group_id: str = None) -> None:
|
|
982
|
+
"""
|
|
983
|
+
Install a pre-configured MCP server from the registry.
|
|
984
|
+
|
|
985
|
+
Args:
|
|
986
|
+
args: Server ID and optional custom name
|
|
987
|
+
"""
|
|
988
|
+
if group_id is None:
|
|
989
|
+
import uuid
|
|
990
|
+
group_id = str(uuid.uuid4())
|
|
991
|
+
|
|
992
|
+
try:
|
|
993
|
+
from code_puppy.mcp.server_registry_catalog import catalog
|
|
994
|
+
from code_puppy.mcp import ServerConfig
|
|
995
|
+
import json
|
|
996
|
+
|
|
997
|
+
if not args:
|
|
998
|
+
emit_info("Usage: /mcp install <server-id> [custom-name]", message_group=group_id)
|
|
999
|
+
emit_info("Use '/mcp search' to find available servers", message_group=group_id)
|
|
1000
|
+
return
|
|
1001
|
+
|
|
1002
|
+
server_id = args[0]
|
|
1003
|
+
custom_name = args[1] if len(args) > 1 else None
|
|
1004
|
+
|
|
1005
|
+
# Find server in registry
|
|
1006
|
+
template = catalog.get_by_id(server_id)
|
|
1007
|
+
if not template:
|
|
1008
|
+
emit_info(f"Server '{server_id}' not found in registry", message_group=group_id)
|
|
1009
|
+
|
|
1010
|
+
# Suggest similar servers
|
|
1011
|
+
suggestions = catalog.search(server_id)
|
|
1012
|
+
if suggestions:
|
|
1013
|
+
emit_info("Did you mean one of these?", message_group=group_id)
|
|
1014
|
+
for s in suggestions[:5]:
|
|
1015
|
+
emit_info(f" • {s.id} - {s.display_name}", message_group=group_id)
|
|
1016
|
+
return
|
|
1017
|
+
|
|
1018
|
+
# Show server details
|
|
1019
|
+
emit_info(f"[bold cyan]Installing: {template.display_name}[/bold cyan]", message_group=group_id)
|
|
1020
|
+
emit_info(f"[dim]{template.description}[/dim]", message_group=group_id)
|
|
1021
|
+
|
|
1022
|
+
# Check requirements
|
|
1023
|
+
if template.requires:
|
|
1024
|
+
emit_info(f"[yellow]Requirements:[/yellow] {', '.join(template.requires)}", message_group=group_id)
|
|
1025
|
+
|
|
1026
|
+
# Use custom name or generate one
|
|
1027
|
+
if not custom_name:
|
|
1028
|
+
# Check if default name exists
|
|
1029
|
+
existing = self.manager.registry.get_by_name(template.name)
|
|
1030
|
+
if existing:
|
|
1031
|
+
# Generate unique name
|
|
1032
|
+
import time
|
|
1033
|
+
custom_name = f"{template.name}-{int(time.time()) % 10000}"
|
|
1034
|
+
emit_info(f"[dim]Using name: {custom_name} (original already exists)[/dim]", message_group=group_id)
|
|
1035
|
+
else:
|
|
1036
|
+
custom_name = template.name
|
|
1037
|
+
|
|
1038
|
+
# Convert template to server config
|
|
1039
|
+
config_dict = template.to_server_config(custom_name)
|
|
1040
|
+
|
|
1041
|
+
# Create ServerConfig
|
|
1042
|
+
server_config = ServerConfig(
|
|
1043
|
+
id=f"{custom_name}_{hash(custom_name)}",
|
|
1044
|
+
name=custom_name,
|
|
1045
|
+
type=config_dict.pop('type'),
|
|
1046
|
+
enabled=True,
|
|
1047
|
+
config=config_dict
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
# Register the server
|
|
1051
|
+
server_id = self.manager.register_server(server_config)
|
|
1052
|
+
|
|
1053
|
+
if server_id:
|
|
1054
|
+
emit_info(f"✅ Installed '{custom_name}' from {template.display_name}", message_group=group_id)
|
|
1055
|
+
|
|
1056
|
+
# Save to mcp_servers.json
|
|
1057
|
+
from code_puppy.config import MCP_SERVERS_FILE
|
|
1058
|
+
import os
|
|
1059
|
+
|
|
1060
|
+
if os.path.exists(MCP_SERVERS_FILE):
|
|
1061
|
+
with open(MCP_SERVERS_FILE, 'r') as f:
|
|
1062
|
+
data = json.load(f)
|
|
1063
|
+
servers = data.get("mcp_servers", {})
|
|
1064
|
+
else:
|
|
1065
|
+
servers = {}
|
|
1066
|
+
data = {"mcp_servers": servers}
|
|
1067
|
+
|
|
1068
|
+
servers[custom_name] = config_dict
|
|
1069
|
+
servers[custom_name]['type'] = server_config.type
|
|
1070
|
+
|
|
1071
|
+
os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
|
|
1072
|
+
with open(MCP_SERVERS_FILE, 'w') as f:
|
|
1073
|
+
json.dump(data, f, indent=2)
|
|
1074
|
+
|
|
1075
|
+
# Show next steps
|
|
1076
|
+
if template.example_usage:
|
|
1077
|
+
emit_info(f"[yellow]Example:[/yellow] {template.example_usage}", message_group=group_id)
|
|
1078
|
+
|
|
1079
|
+
# Check for environment variables
|
|
1080
|
+
env_vars = []
|
|
1081
|
+
if 'env' in config_dict:
|
|
1082
|
+
for key, value in config_dict['env'].items():
|
|
1083
|
+
if value.startswith('$'):
|
|
1084
|
+
env_vars.append(value[1:])
|
|
1085
|
+
|
|
1086
|
+
if env_vars:
|
|
1087
|
+
emit_info(f"[yellow]Required environment variables:[/yellow] {', '.join(env_vars)}", message_group=group_id)
|
|
1088
|
+
emit_info("Set these before starting the server", message_group=group_id)
|
|
1089
|
+
|
|
1090
|
+
emit_info(f"Use '/mcp start {custom_name}' to start the server", message_group=group_id)
|
|
1091
|
+
|
|
1092
|
+
# Reload MCP servers
|
|
1093
|
+
from code_puppy.agent import reload_mcp_servers
|
|
1094
|
+
reload_mcp_servers()
|
|
1095
|
+
else:
|
|
1096
|
+
emit_info(f"Failed to install server", message_group=group_id)
|
|
1097
|
+
|
|
1098
|
+
except ImportError:
|
|
1099
|
+
emit_info("Server registry not available", message_group=group_id)
|
|
1100
|
+
except Exception as e:
|
|
1101
|
+
logger.error(f"Error installing server: {e}")
|
|
1102
|
+
emit_info(f"Installation failed: {e}", message_group=group_id)
|
|
1103
|
+
|
|
1104
|
+
def _find_server_id_by_name(self, server_name: str) -> Optional[str]:
|
|
1105
|
+
"""
|
|
1106
|
+
Find a server ID by its name.
|
|
1107
|
+
|
|
1108
|
+
Args:
|
|
1109
|
+
server_name: Name of the server to find
|
|
1110
|
+
|
|
1111
|
+
Returns:
|
|
1112
|
+
Server ID if found, None otherwise
|
|
1113
|
+
"""
|
|
1114
|
+
try:
|
|
1115
|
+
servers = self.manager.list_servers()
|
|
1116
|
+
for server in servers:
|
|
1117
|
+
if server.name.lower() == server_name.lower():
|
|
1118
|
+
return server.id
|
|
1119
|
+
return None
|
|
1120
|
+
except Exception as e:
|
|
1121
|
+
logger.error(f"Error finding server by name '{server_name}': {e}")
|
|
1122
|
+
return None
|
|
1123
|
+
|
|
1124
|
+
def _suggest_similar_servers(self, server_name: str, group_id: str = None) -> None:
|
|
1125
|
+
"""
|
|
1126
|
+
Suggest similar server names when a server is not found.
|
|
1127
|
+
|
|
1128
|
+
Args:
|
|
1129
|
+
server_name: The server name that was not found
|
|
1130
|
+
group_id: Optional message group ID for grouping related messages
|
|
1131
|
+
"""
|
|
1132
|
+
try:
|
|
1133
|
+
servers = self.manager.list_servers()
|
|
1134
|
+
if not servers:
|
|
1135
|
+
emit_info("No servers are registered", message_group=group_id)
|
|
1136
|
+
return
|
|
1137
|
+
|
|
1138
|
+
# Simple suggestion based on partial matching
|
|
1139
|
+
suggestions = []
|
|
1140
|
+
server_name_lower = server_name.lower()
|
|
1141
|
+
|
|
1142
|
+
for server in servers:
|
|
1143
|
+
if server_name_lower in server.name.lower():
|
|
1144
|
+
suggestions.append(server.name)
|
|
1145
|
+
|
|
1146
|
+
if suggestions:
|
|
1147
|
+
emit_info(f"Did you mean: {', '.join(suggestions)}", message_group=group_id)
|
|
1148
|
+
else:
|
|
1149
|
+
server_names = [s.name for s in servers]
|
|
1150
|
+
emit_info(f"Available servers: {', '.join(server_names)}", message_group=group_id)
|
|
1151
|
+
|
|
1152
|
+
except Exception as e:
|
|
1153
|
+
logger.error(f"Error suggesting similar servers: {e}")
|
|
1154
|
+
|
|
1155
|
+
def _format_state_indicator(self, state: ServerState) -> Text:
|
|
1156
|
+
"""
|
|
1157
|
+
Format a server state with appropriate color and icon.
|
|
1158
|
+
|
|
1159
|
+
Args:
|
|
1160
|
+
state: Server state to format
|
|
1161
|
+
|
|
1162
|
+
Returns:
|
|
1163
|
+
Rich Text object with colored state indicator
|
|
1164
|
+
"""
|
|
1165
|
+
state_map = {
|
|
1166
|
+
ServerState.RUNNING: ("✓ Run", "green"),
|
|
1167
|
+
ServerState.STOPPED: ("✗ Stop", "red"),
|
|
1168
|
+
ServerState.STARTING: ("↗ Start", "yellow"),
|
|
1169
|
+
ServerState.STOPPING: ("↙ Stop", "yellow"),
|
|
1170
|
+
ServerState.ERROR: ("⚠ Err", "red"),
|
|
1171
|
+
ServerState.QUARANTINED: ("⏸ Quar", "yellow"),
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
display, color = state_map.get(state, ("? Unk", "dim"))
|
|
1175
|
+
return Text(display, style=color)
|
|
1176
|
+
|
|
1177
|
+
def _format_uptime(self, uptime_seconds: Optional[float]) -> str:
|
|
1178
|
+
"""
|
|
1179
|
+
Format uptime in a human-readable format.
|
|
1180
|
+
|
|
1181
|
+
Args:
|
|
1182
|
+
uptime_seconds: Uptime in seconds, or None
|
|
1183
|
+
|
|
1184
|
+
Returns:
|
|
1185
|
+
Formatted uptime string
|
|
1186
|
+
"""
|
|
1187
|
+
if uptime_seconds is None or uptime_seconds <= 0:
|
|
1188
|
+
return "-"
|
|
1189
|
+
|
|
1190
|
+
# Convert to readable format
|
|
1191
|
+
if uptime_seconds < 60:
|
|
1192
|
+
return f"{int(uptime_seconds)}s"
|
|
1193
|
+
elif uptime_seconds < 3600:
|
|
1194
|
+
minutes = int(uptime_seconds // 60)
|
|
1195
|
+
seconds = int(uptime_seconds % 60)
|
|
1196
|
+
return f"{minutes}m {seconds}s"
|
|
1197
|
+
else:
|
|
1198
|
+
hours = int(uptime_seconds // 3600)
|
|
1199
|
+
minutes = int((uptime_seconds % 3600) // 60)
|
|
1200
|
+
return f"{hours}h {minutes}m"
|
|
1201
|
+
|
|
1202
|
+
def _show_detailed_server_status(self, server_id: str, server_name: str, group_id: str = None) -> None:
|
|
1203
|
+
"""
|
|
1204
|
+
Show comprehensive status information for a specific server.
|
|
1205
|
+
|
|
1206
|
+
Args:
|
|
1207
|
+
server_id: ID of the server
|
|
1208
|
+
server_name: Name of the server
|
|
1209
|
+
group_id: Optional message group ID
|
|
1210
|
+
"""
|
|
1211
|
+
if group_id is None:
|
|
1212
|
+
import uuid
|
|
1213
|
+
group_id = str(uuid.uuid4())
|
|
1214
|
+
|
|
1215
|
+
try:
|
|
1216
|
+
status = self.manager.get_server_status(server_id)
|
|
1217
|
+
|
|
1218
|
+
if not status.get("exists", True):
|
|
1219
|
+
emit_info(f"Server '{server_name}' not found or not accessible", message_group=group_id)
|
|
1220
|
+
return
|
|
1221
|
+
|
|
1222
|
+
# Create detailed status panel
|
|
1223
|
+
status_lines = []
|
|
1224
|
+
|
|
1225
|
+
# Basic information
|
|
1226
|
+
status_lines.append(f"[bold]Server:[/bold] {server_name}")
|
|
1227
|
+
status_lines.append(f"[bold]ID:[/bold] {server_id}")
|
|
1228
|
+
status_lines.append(f"[bold]Type:[/bold] {status.get('type', 'unknown').upper()}")
|
|
1229
|
+
|
|
1230
|
+
# State and status
|
|
1231
|
+
state = status.get('state', 'unknown')
|
|
1232
|
+
state_display = self._format_state_indicator(ServerState(state) if state in [s.value for s in ServerState] else ServerState.STOPPED)
|
|
1233
|
+
status_lines.append(f"[bold]State:[/bold] {state_display}")
|
|
1234
|
+
|
|
1235
|
+
enabled = status.get('enabled', False)
|
|
1236
|
+
status_lines.append(f"[bold]Enabled:[/bold] {'✓ Yes' if enabled else '✗ No'}")
|
|
1237
|
+
|
|
1238
|
+
# Check async lifecycle manager status if available
|
|
1239
|
+
try:
|
|
1240
|
+
from code_puppy.mcp.async_lifecycle import get_lifecycle_manager
|
|
1241
|
+
lifecycle_mgr = get_lifecycle_manager()
|
|
1242
|
+
if lifecycle_mgr.is_running(server_id):
|
|
1243
|
+
status_lines.append(f"[bold]Process:[/bold] [green]✓ Active (subprocess/connection running)[/green]")
|
|
1244
|
+
else:
|
|
1245
|
+
status_lines.append(f"[bold]Process:[/bold] [dim]Not active[/dim]")
|
|
1246
|
+
except Exception:
|
|
1247
|
+
pass # Lifecycle manager not available
|
|
1248
|
+
|
|
1249
|
+
quarantined = status.get('quarantined', False)
|
|
1250
|
+
if quarantined:
|
|
1251
|
+
status_lines.append(f"[bold]Quarantined:[/bold] [yellow]⚠ Yes[/yellow]")
|
|
1252
|
+
|
|
1253
|
+
# Timing information
|
|
1254
|
+
uptime = status.get('tracker_uptime')
|
|
1255
|
+
if uptime:
|
|
1256
|
+
uptime_str = self._format_uptime(uptime.total_seconds() if hasattr(uptime, 'total_seconds') else uptime)
|
|
1257
|
+
status_lines.append(f"[bold]Uptime:[/bold] {uptime_str}")
|
|
1258
|
+
|
|
1259
|
+
# Error information
|
|
1260
|
+
error_msg = status.get('error_message')
|
|
1261
|
+
if error_msg:
|
|
1262
|
+
status_lines.append(f"[bold]Error:[/bold] [red]{error_msg}[/red]")
|
|
1263
|
+
|
|
1264
|
+
# Event information
|
|
1265
|
+
event_count = status.get('recent_events_count', 0)
|
|
1266
|
+
status_lines.append(f"[bold]Recent Events:[/bold] {event_count}")
|
|
1267
|
+
|
|
1268
|
+
# Metadata
|
|
1269
|
+
metadata = status.get('tracker_metadata', {})
|
|
1270
|
+
if metadata:
|
|
1271
|
+
status_lines.append(f"[bold]Metadata:[/bold] {len(metadata)} keys")
|
|
1272
|
+
|
|
1273
|
+
# Create and show the panel
|
|
1274
|
+
panel_content = "\n".join(status_lines)
|
|
1275
|
+
panel = Panel(
|
|
1276
|
+
panel_content,
|
|
1277
|
+
title=f"🔌 {server_name} Status",
|
|
1278
|
+
border_style="cyan"
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
emit_info(panel, message_group=group_id)
|
|
1282
|
+
|
|
1283
|
+
# Show recent events if available
|
|
1284
|
+
recent_events = status.get('recent_events', [])
|
|
1285
|
+
if recent_events:
|
|
1286
|
+
emit_info("\n📋 Recent Events:", message_group=group_id)
|
|
1287
|
+
for event in recent_events[-5:]: # Show last 5 events
|
|
1288
|
+
timestamp = datetime.fromisoformat(event["timestamp"])
|
|
1289
|
+
time_str = timestamp.strftime("%H:%M:%S")
|
|
1290
|
+
event_type = event["event_type"]
|
|
1291
|
+
details = event.get("details", {})
|
|
1292
|
+
message = details.get("message", "")
|
|
1293
|
+
|
|
1294
|
+
emit_info(f" [dim]{time_str}[/dim] [cyan]{event_type}[/cyan] {message}", message_group=group_id)
|
|
1295
|
+
|
|
1296
|
+
except Exception as e:
|
|
1297
|
+
logger.error(f"Error showing detailed status for server '{server_name}': {e}")
|
|
1298
|
+
emit_info(f"Failed to get detailed status: {e}", message_group=group_id)
|