code-puppy 0.0.127__py3-none-any.whl → 0.0.129__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 (35) hide show
  1. code_puppy/__init__.py +1 -0
  2. code_puppy/agent.py +65 -69
  3. code_puppy/agents/agent_code_puppy.py +0 -3
  4. code_puppy/agents/runtime_manager.py +231 -0
  5. code_puppy/command_line/command_handler.py +56 -25
  6. code_puppy/command_line/mcp_commands.py +1298 -0
  7. code_puppy/command_line/meta_command_handler.py +3 -2
  8. code_puppy/command_line/model_picker_completion.py +21 -8
  9. code_puppy/http_utils.py +1 -1
  10. code_puppy/main.py +99 -158
  11. code_puppy/mcp/__init__.py +23 -0
  12. code_puppy/mcp/async_lifecycle.py +237 -0
  13. code_puppy/mcp/circuit_breaker.py +218 -0
  14. code_puppy/mcp/config_wizard.py +437 -0
  15. code_puppy/mcp/dashboard.py +291 -0
  16. code_puppy/mcp/error_isolation.py +360 -0
  17. code_puppy/mcp/examples/retry_example.py +208 -0
  18. code_puppy/mcp/health_monitor.py +549 -0
  19. code_puppy/mcp/managed_server.py +346 -0
  20. code_puppy/mcp/manager.py +701 -0
  21. code_puppy/mcp/registry.py +412 -0
  22. code_puppy/mcp/retry_manager.py +321 -0
  23. code_puppy/mcp/server_registry_catalog.py +751 -0
  24. code_puppy/mcp/status_tracker.py +355 -0
  25. code_puppy/messaging/spinner/textual_spinner.py +6 -2
  26. code_puppy/model_factory.py +19 -4
  27. code_puppy/models.json +8 -6
  28. code_puppy/tui/app.py +19 -27
  29. code_puppy/tui/tests/test_agent_command.py +22 -15
  30. {code_puppy-0.0.127.data → code_puppy-0.0.129.data}/data/code_puppy/models.json +8 -6
  31. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/METADATA +4 -3
  32. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/RECORD +35 -19
  33. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/WHEEL +0 -0
  34. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/entry_points.txt +0 -0
  35. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.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)