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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. code_puppy/agent.py +15 -17
  2. code_puppy/agents/agent_manager.py +320 -9
  3. code_puppy/agents/base_agent.py +58 -2
  4. code_puppy/agents/runtime_manager.py +68 -42
  5. code_puppy/command_line/command_handler.py +82 -33
  6. code_puppy/command_line/mcp/__init__.py +10 -0
  7. code_puppy/command_line/mcp/add_command.py +183 -0
  8. code_puppy/command_line/mcp/base.py +35 -0
  9. code_puppy/command_line/mcp/handler.py +133 -0
  10. code_puppy/command_line/mcp/help_command.py +146 -0
  11. code_puppy/command_line/mcp/install_command.py +176 -0
  12. code_puppy/command_line/mcp/list_command.py +94 -0
  13. code_puppy/command_line/mcp/logs_command.py +126 -0
  14. code_puppy/command_line/mcp/remove_command.py +82 -0
  15. code_puppy/command_line/mcp/restart_command.py +92 -0
  16. code_puppy/command_line/mcp/search_command.py +117 -0
  17. code_puppy/command_line/mcp/start_all_command.py +126 -0
  18. code_puppy/command_line/mcp/start_command.py +98 -0
  19. code_puppy/command_line/mcp/status_command.py +185 -0
  20. code_puppy/command_line/mcp/stop_all_command.py +109 -0
  21. code_puppy/command_line/mcp/stop_command.py +79 -0
  22. code_puppy/command_line/mcp/test_command.py +107 -0
  23. code_puppy/command_line/mcp/utils.py +129 -0
  24. code_puppy/command_line/mcp/wizard_utils.py +259 -0
  25. code_puppy/command_line/model_picker_completion.py +21 -4
  26. code_puppy/command_line/prompt_toolkit_completion.py +9 -0
  27. code_puppy/main.py +23 -17
  28. code_puppy/mcp/__init__.py +42 -16
  29. code_puppy/mcp/async_lifecycle.py +51 -49
  30. code_puppy/mcp/blocking_startup.py +125 -113
  31. code_puppy/mcp/captured_stdio_server.py +63 -70
  32. code_puppy/mcp/circuit_breaker.py +63 -47
  33. code_puppy/mcp/config_wizard.py +169 -136
  34. code_puppy/mcp/dashboard.py +79 -71
  35. code_puppy/mcp/error_isolation.py +147 -100
  36. code_puppy/mcp/examples/retry_example.py +55 -42
  37. code_puppy/mcp/health_monitor.py +152 -141
  38. code_puppy/mcp/managed_server.py +100 -93
  39. code_puppy/mcp/manager.py +168 -156
  40. code_puppy/mcp/registry.py +148 -110
  41. code_puppy/mcp/retry_manager.py +63 -61
  42. code_puppy/mcp/server_registry_catalog.py +271 -225
  43. code_puppy/mcp/status_tracker.py +80 -80
  44. code_puppy/mcp/system_tools.py +47 -52
  45. code_puppy/messaging/message_queue.py +20 -13
  46. code_puppy/messaging/renderers.py +30 -15
  47. code_puppy/state_management.py +103 -0
  48. code_puppy/tui/app.py +64 -7
  49. code_puppy/tui/components/chat_view.py +3 -3
  50. code_puppy/tui/components/human_input_modal.py +12 -8
  51. code_puppy/tui/screens/__init__.py +2 -2
  52. code_puppy/tui/screens/mcp_install_wizard.py +208 -179
  53. code_puppy/tui/tests/test_agent_command.py +3 -3
  54. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
  55. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
  56. code_puppy/command_line/mcp_commands.py +0 -1789
  57. {code_puppy-0.0.135.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
  58. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
  59. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
  60. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
@@ -1,1789 +0,0 @@
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
-
19
- from code_puppy.state_management import is_tui_mode
20
- from code_puppy.messaging import emit_prompt
21
- from code_puppy.mcp.manager import get_mcp_manager, ServerInfo
22
- from code_puppy.mcp.managed_server import ServerConfig, ServerState
23
- from code_puppy.messaging import emit_info, emit_system_message
24
-
25
- # Configure logging
26
- logger = logging.getLogger(__name__)
27
-
28
-
29
- class MCPCommandHandler:
30
- """
31
- Command handler for MCP server management operations.
32
-
33
- Provides the /mcp command interface that allows users to manage MCP servers
34
- at runtime through commands like list, start, stop, restart, status, etc.
35
- Uses Rich library for formatted output with tables, colors, and status indicators.
36
-
37
- Example usage:
38
- handler = MCPCommandHandler()
39
- handler.handle_mcp_command("/mcp list")
40
- handler.handle_mcp_command("/mcp start filesystem")
41
- handler.handle_mcp_command("/mcp status filesystem")
42
- """
43
-
44
- def __init__(self):
45
- """Initialize the MCP command handler."""
46
- self.console = Console()
47
- self.manager = get_mcp_manager()
48
- logger.info("MCPCommandHandler initialized")
49
-
50
- def handle_mcp_command(self, command: str) -> bool:
51
- """
52
- Handle MCP commands and route to appropriate handler.
53
-
54
- Args:
55
- command: The full command string (e.g., "/mcp list", "/mcp start server")
56
-
57
- Returns:
58
- True if command was handled successfully, False otherwise
59
- """
60
- import uuid
61
- # Generate a group ID for this entire MCP command session
62
- group_id = str(uuid.uuid4())
63
-
64
- try:
65
- # Remove /mcp prefix and parse arguments
66
- command = command.strip()
67
- if not command.startswith("/mcp"):
68
- return False
69
-
70
- # Remove the /mcp prefix
71
- args_str = command[4:].strip()
72
-
73
- # If no subcommand, show status dashboard
74
- if not args_str:
75
- self.cmd_list([], group_id=group_id)
76
- return True
77
-
78
- # Parse arguments using shlex for proper handling of quoted strings
79
- try:
80
- args = shlex.split(args_str)
81
- except ValueError as e:
82
- emit_info(f"[red]Invalid command syntax: {e}[/red]", message_group=group_id)
83
- return True
84
-
85
- if not args:
86
- self.cmd_list([], group_id=group_id)
87
- return True
88
-
89
- subcommand = args[0].lower()
90
- sub_args = args[1:] if len(args) > 1 else []
91
-
92
- # Route to appropriate command handler
93
- command_map = {
94
- 'list': self.cmd_list,
95
- 'start': self.cmd_start,
96
- 'start-all': self.cmd_start_all,
97
- 'stop': self.cmd_stop,
98
- 'stop-all': self.cmd_stop_all,
99
- 'restart': self.cmd_restart,
100
- 'status': self.cmd_status,
101
- 'test': self.cmd_test,
102
- 'add': self.cmd_add,
103
- 'remove': self.cmd_remove,
104
- 'logs': self.cmd_logs,
105
- 'search': self.cmd_search,
106
- 'install': self.cmd_install,
107
- 'help': self.cmd_help,
108
- }
109
-
110
- handler = command_map.get(subcommand)
111
- if handler:
112
- handler(sub_args)
113
- return True
114
- else:
115
- emit_info(f"[yellow]Unknown MCP subcommand: {subcommand}[/yellow]", message_group=group_id)
116
- emit_info("Type '/mcp help' for available commands", message_group=group_id)
117
- return True
118
-
119
- except Exception as e:
120
- logger.error(f"Error handling MCP command '{command}': {e}")
121
- emit_info(f"Error executing MCP command: {e}", message_group=group_id)
122
- return True
123
-
124
- def cmd_list(self, args: List[str], group_id: str = None) -> None:
125
- """
126
- List all registered MCP servers in a formatted table.
127
-
128
- Args:
129
- args: Command arguments (unused for list command)
130
- group_id: Optional message group ID for grouping related messages
131
- """
132
- if group_id is None:
133
- import uuid
134
- group_id = str(uuid.uuid4())
135
-
136
- try:
137
- servers = self.manager.list_servers()
138
-
139
- if not servers:
140
- emit_info("No MCP servers registered", message_group=group_id)
141
- return
142
-
143
- # Create table for server list
144
- table = Table(title="🔌 MCP Server Status Dashboard")
145
- table.add_column("Name", style="cyan", no_wrap=True)
146
- table.add_column("Type", style="dim", no_wrap=True)
147
- table.add_column("State", justify="center")
148
- table.add_column("Enabled", justify="center")
149
- table.add_column("Uptime", style="dim")
150
- table.add_column("Status", style="dim")
151
-
152
- for server in servers:
153
- # Format state with appropriate color and icon
154
- state_display = self._format_state_indicator(server.state)
155
-
156
- # Format enabled status
157
- enabled_display = "✓" if server.enabled else "✗"
158
- enabled_style = "green" if server.enabled else "red"
159
-
160
- # Format uptime
161
- uptime_display = self._format_uptime(server.uptime_seconds)
162
-
163
- # Format status message
164
- status_display = server.error_message or "OK"
165
- if server.quarantined:
166
- status_display = "Quarantined"
167
-
168
- table.add_row(
169
- server.name,
170
- server.type.upper(),
171
- state_display,
172
- Text(enabled_display, style=enabled_style),
173
- uptime_display,
174
- status_display
175
- )
176
-
177
- emit_info(table, message_group=group_id)
178
-
179
- # Show summary
180
- total = len(servers)
181
- running = sum(1 for s in servers if s.state == ServerState.RUNNING and s.enabled)
182
- emit_info(f"\n📊 Summary: {running}/{total} servers running", message_group=group_id)
183
-
184
- except Exception as e:
185
- logger.error(f"Error listing MCP servers: {e}")
186
- emit_info(f"Failed to list servers: {e}", message_group=group_id)
187
-
188
- def cmd_start(self, args: List[str]) -> None:
189
- """
190
- Start a specific MCP server.
191
-
192
- Args:
193
- args: Command arguments, expects [server_name]
194
- """
195
- import uuid
196
- group_id = str(uuid.uuid4())
197
-
198
- if not args:
199
- emit_info("[yellow]Usage: /mcp start <server_name>[/yellow]", message_group=group_id)
200
- return
201
-
202
- server_name = args[0]
203
-
204
- try:
205
- # Find server by name
206
- server_id = self._find_server_id_by_name(server_name)
207
- if not server_id:
208
- emit_info(f"[red]Server '{server_name}' not found[/red]", message_group=group_id)
209
- self._suggest_similar_servers(server_name, group_id=group_id)
210
- return
211
-
212
- # Start the server (enable and start process)
213
- success = self.manager.start_server_sync(server_id)
214
-
215
- if success:
216
- # This and subsequent messages will auto-group with the first message
217
- emit_info(f"[green]✓ Started server: {server_name}[/green]", message_group=group_id)
218
-
219
- # Give async tasks a moment to complete
220
- import asyncio
221
- try:
222
- loop = asyncio.get_running_loop()
223
- # If we're in async context, wait a bit for server to start
224
- import time
225
- time.sleep(0.5) # Small delay to let async tasks progress
226
- except RuntimeError:
227
- pass # No async loop, server will start when agent uses it
228
-
229
- # Reload the agent to pick up the newly enabled server
230
- try:
231
- from code_puppy.agents.runtime_manager import get_runtime_agent_manager
232
- manager = get_runtime_agent_manager()
233
- manager.reload_agent()
234
- emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
235
- except Exception as e:
236
- logger.warning(f"Could not reload agent: {e}")
237
- else:
238
- emit_info(f"[red]✗ Failed to start server: {server_name}[/red]", message_group=group_id)
239
-
240
- except Exception as e:
241
- logger.error(f"Error starting server '{server_name}': {e}")
242
- emit_info(f"[red]Failed to start server: {e}[/red]", message_group=group_id)
243
-
244
- def cmd_start_all(self, args: List[str]) -> None:
245
- """
246
- Start all registered MCP servers.
247
-
248
- Args:
249
- args: Command arguments (unused)
250
- """
251
- import uuid
252
- group_id = str(uuid.uuid4())
253
-
254
- try:
255
- servers = self.manager.list_servers()
256
-
257
- if not servers:
258
- emit_info("[yellow]No servers registered[/yellow]", message_group=group_id)
259
- return
260
-
261
- started_count = 0
262
- failed_count = 0
263
- already_running = 0
264
-
265
- emit_info(f"Starting {len(servers)} servers...", message_group=group_id)
266
-
267
- for server_info in servers:
268
- server_id = server_info.id
269
- server_name = server_info.name
270
-
271
- # Skip if already running
272
- if server_info.state == ServerState.RUNNING:
273
- already_running += 1
274
- emit_info(f" • {server_name}: already running", message_group=group_id)
275
- continue
276
-
277
- # Try to start the server
278
- success = self.manager.start_server_sync(server_id)
279
-
280
- if success:
281
- started_count += 1
282
- emit_info(f" [green]✓ Started: {server_name}[/green]", message_group=group_id)
283
- else:
284
- failed_count += 1
285
- emit_info(f" [red]✗ Failed: {server_name}[/red]", message_group=group_id)
286
-
287
- # Summary
288
- emit_info("", message_group=group_id)
289
- if started_count > 0:
290
- emit_info(f"[green]Started {started_count} server(s)[/green]", message_group=group_id)
291
- if already_running > 0:
292
- emit_info(f"{already_running} server(s) already running", message_group=group_id)
293
- if failed_count > 0:
294
- emit_info(f"[yellow]Failed to start {failed_count} server(s)[/yellow]", message_group=group_id)
295
-
296
- # Reload agent if any servers were started
297
- if started_count > 0:
298
- # Give async tasks a moment to complete before reloading agent
299
- import asyncio
300
- try:
301
- loop = asyncio.get_running_loop()
302
- # If we're in async context, wait a bit for servers to start
303
- import time
304
- time.sleep(0.5) # Small delay to let async tasks progress
305
- except RuntimeError:
306
- pass # No async loop, servers will start when agent uses them
307
-
308
- try:
309
- from code_puppy.agents.runtime_manager import get_runtime_agent_manager
310
- manager = get_runtime_agent_manager()
311
- manager.reload_agent()
312
- emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
313
- except Exception as e:
314
- logger.warning(f"Could not reload agent: {e}")
315
-
316
- except Exception as e:
317
- logger.error(f"Error starting all servers: {e}")
318
- emit_info(f"[red]Failed to start servers: {e}[/red]", message_group=group_id)
319
-
320
- def cmd_stop(self, args: List[str]) -> None:
321
- """
322
- Stop a specific MCP server.
323
-
324
- Args:
325
- args: Command arguments, expects [server_name]
326
- """
327
- import uuid
328
- group_id = str(uuid.uuid4())
329
-
330
- if not args:
331
- emit_info("[yellow]Usage: /mcp stop <server_name>[/yellow]", message_group=group_id)
332
- return
333
-
334
- server_name = args[0]
335
-
336
- try:
337
- # Find server by name
338
- server_id = self._find_server_id_by_name(server_name)
339
- if not server_id:
340
- emit_info(f"Server '{server_name}' not found", message_group=group_id)
341
- self._suggest_similar_servers(server_name, group_id=group_id)
342
- return
343
-
344
- # Stop the server (disable and stop process)
345
- success = self.manager.stop_server_sync(server_id)
346
-
347
- if success:
348
- emit_info(f"✓ Stopped server: {server_name}", message_group=group_id)
349
-
350
- # Reload the agent to remove the disabled server
351
- try:
352
- from code_puppy.agents.runtime_manager import get_runtime_agent_manager
353
- manager = get_runtime_agent_manager()
354
- manager.reload_agent()
355
- emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
356
- except Exception as e:
357
- logger.warning(f"Could not reload agent: {e}")
358
- else:
359
- emit_info(f"✗ Failed to stop server: {server_name}", message_group=group_id)
360
-
361
- except Exception as e:
362
- logger.error(f"Error stopping server '{server_name}': {e}")
363
- emit_info(f"Failed to stop server: {e}", message_group=group_id)
364
-
365
- def cmd_stop_all(self, args: List[str]) -> None:
366
- """
367
- Stop all running MCP servers.
368
-
369
- Args:
370
- args: [group_id] - optional group ID for message grouping
371
- """
372
- group_id = args[0] if args else None
373
- if group_id is None:
374
- import uuid
375
- group_id = str(uuid.uuid4())
376
- try:
377
- servers = self.manager.list_servers()
378
-
379
- if not servers:
380
- emit_info("No servers registered", message_group=group_id)
381
- return
382
-
383
- stopped_count = 0
384
- failed_count = 0
385
- already_stopped = 0
386
-
387
- # Count running servers
388
- running_servers = [s for s in servers if s.state == ServerState.RUNNING]
389
-
390
- if not running_servers:
391
- emit_info("No servers are currently running", message_group=group_id)
392
- return
393
-
394
- emit_info(f"Stopping {len(running_servers)} running server(s)...", message_group=group_id)
395
-
396
- for server_info in running_servers:
397
- server_id = server_info.id
398
- server_name = server_info.name
399
-
400
- # Try to stop the server
401
- success = self.manager.stop_server_sync(server_id)
402
-
403
- if success:
404
- stopped_count += 1
405
- emit_info(f" ✓ Stopped: {server_name}", message_group=group_id)
406
- else:
407
- failed_count += 1
408
- emit_info(f" ✗ Failed: {server_name}", message_group=group_id)
409
-
410
- # Summary
411
- emit_info("", message_group=group_id)
412
- if stopped_count > 0:
413
- emit_info(f"Stopped {stopped_count} server(s)", message_group=group_id)
414
- if failed_count > 0:
415
- emit_info(f"Failed to stop {failed_count} server(s)", message_group=group_id)
416
-
417
- # Reload agent if any servers were stopped
418
- if stopped_count > 0:
419
- # Give async tasks a moment to complete before reloading agent
420
- import asyncio
421
- try:
422
- loop = asyncio.get_running_loop()
423
- # If we're in async context, wait a bit for servers to stop
424
- import time
425
- time.sleep(0.5) # Small delay to let async tasks progress
426
- except RuntimeError:
427
- pass # No async loop, servers will stop when needed
428
-
429
- try:
430
- from code_puppy.agents.runtime_manager import get_runtime_agent_manager
431
- manager = get_runtime_agent_manager()
432
- manager.reload_agent()
433
- emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
434
- except Exception as e:
435
- logger.warning(f"Could not reload agent: {e}")
436
-
437
- except Exception as e:
438
- logger.error(f"Error stopping all servers: {e}")
439
- emit_info(f"Failed to stop servers: {e}", message_group=group_id)
440
-
441
- def cmd_restart(self, args: List[str]) -> None:
442
- """
443
- Restart a specific MCP server.
444
-
445
- Args:
446
- args: Command arguments, expects [server_name]
447
- """
448
- import uuid
449
- group_id = str(uuid.uuid4())
450
-
451
- if not args:
452
- emit_info("Usage: /mcp restart <server_name>", message_group=group_id)
453
- return
454
-
455
- server_name = args[0]
456
-
457
- try:
458
- # Find server by name
459
- server_id = self._find_server_id_by_name(server_name)
460
- if not server_id:
461
- emit_info(f"Server '{server_name}' not found", message_group=group_id)
462
- self._suggest_similar_servers(server_name)
463
- return
464
-
465
- # Stop the server first
466
- emit_info(f"Stopping server: {server_name}", message_group=group_id)
467
- self.manager.stop_server_sync(server_id)
468
-
469
- # Then reload and start it
470
- emit_info(f"Reloading configuration...", message_group=group_id)
471
- reload_success = self.manager.reload_server(server_id)
472
-
473
- if reload_success:
474
- emit_info(f"Starting server: {server_name}", message_group=group_id)
475
- start_success = self.manager.start_server_sync(server_id)
476
-
477
- if start_success:
478
- emit_info(f"✓ Restarted server: {server_name}", message_group=group_id)
479
-
480
- # Reload the agent to pick up the server changes
481
- try:
482
- from code_puppy.agent import get_code_generation_agent
483
- get_code_generation_agent(force_reload=True)
484
- emit_info("[dim]Agent reloaded with updated servers[/dim]", message_group=group_id)
485
- except Exception as e:
486
- logger.warning(f"Could not reload agent: {e}")
487
- else:
488
- emit_info(f"✗ Failed to start server after reload: {server_name}", message_group=group_id)
489
- else:
490
- emit_info(f"✗ Failed to reload server configuration: {server_name}", message_group=group_id)
491
-
492
- except Exception as e:
493
- logger.error(f"Error restarting server '{server_name}': {e}")
494
- emit_info(f"Failed to restart server: {e}", message_group=group_id)
495
-
496
- def cmd_status(self, args: List[str]) -> None:
497
- """
498
- Show detailed status for a specific server or all servers.
499
-
500
- Args:
501
- args: Command arguments, expects [server_name] (optional)
502
- """
503
- import uuid
504
- group_id = str(uuid.uuid4())
505
-
506
- try:
507
- if args:
508
- # Show detailed status for specific server
509
- server_name = args[0]
510
- server_id = self._find_server_id_by_name(server_name)
511
-
512
- if not server_id:
513
- emit_info(f"Server '{server_name}' not found", message_group=group_id)
514
- self._suggest_similar_servers(server_name)
515
- return
516
-
517
- self._show_detailed_server_status(server_id, server_name, group_id)
518
- else:
519
- # Show brief status for all servers
520
- self.cmd_list([])
521
-
522
- except Exception as e:
523
- logger.error(f"Error showing server status: {e}")
524
- emit_info(f"Failed to get server status: {e}", message_group=group_id)
525
-
526
- def cmd_test(self, args: List[str]) -> None:
527
- """
528
- Test connectivity to a specific MCP server.
529
-
530
- Args:
531
- args: Command arguments, expects [server_name]
532
- """
533
- import uuid
534
- group_id = str(uuid.uuid4())
535
-
536
- if not args:
537
- emit_info("Usage: /mcp test <server_name>", message_group=group_id)
538
- return
539
-
540
- server_name = args[0]
541
-
542
- try:
543
- # Find server by name
544
- server_id = self._find_server_id_by_name(server_name)
545
- if not server_id:
546
- emit_info(f"Server '{server_name}' not found", message_group=group_id)
547
- self._suggest_similar_servers(server_name)
548
- return
549
-
550
- # Get managed server
551
- managed_server = self.manager.get_server(server_id)
552
- if not managed_server:
553
- emit_info(f"Server '{server_name}' not accessible", message_group=group_id)
554
- return
555
-
556
- emit_info(f"🔍 Testing connectivity to server: {server_name}", message_group=group_id)
557
-
558
- # Basic connectivity test - try to get the pydantic server
559
- try:
560
- pydantic_server = managed_server.get_pydantic_server()
561
- emit_info(f"✓ Server instance created successfully", message_group=group_id)
562
-
563
- # Try to get server info if available
564
- emit_info(f" • Server type: {managed_server.config.type}", message_group=group_id)
565
- emit_info(f" • Server enabled: {managed_server.is_enabled()}", message_group=group_id)
566
- emit_info(f" • Server quarantined: {managed_server.is_quarantined()}", message_group=group_id)
567
-
568
- if not managed_server.is_enabled():
569
- emit_info(" • Server is disabled - enable it with '/mcp start'", message_group=group_id)
570
-
571
- if managed_server.is_quarantined():
572
- emit_info(" • Server is quarantined - may have recent errors", message_group=group_id)
573
-
574
- emit_info(f"✓ Connectivity test passed for: {server_name}", message_group=group_id)
575
-
576
- except Exception as test_error:
577
- emit_info(f"✗ Connectivity test failed: {test_error}", message_group=group_id)
578
-
579
- except Exception as e:
580
- logger.error(f"Error testing server '{server_name}': {e}")
581
- emit_info(f"Failed to test server: {e}", message_group=group_id)
582
-
583
- def cmd_add(self, args: List[str]) -> None:
584
- """
585
- Add a new MCP server from JSON configuration or launch wizard.
586
-
587
- Usage:
588
- /mcp add - Launch interactive wizard
589
- /mcp add <json> - Add server from JSON config
590
-
591
- Example JSON:
592
- /mcp add {"name": "test", "type": "stdio", "command": "echo", "args": ["hello"]}
593
-
594
- Args:
595
- args: Command arguments - JSON config or empty for wizard
596
- """
597
- import uuid
598
- group_id = str(uuid.uuid4())
599
-
600
- # Check if in TUI mode and guide user to use Ctrl+T instead
601
- if is_tui_mode() and not args:
602
- emit_info("💡 In TUI mode, press Ctrl+T to open the MCP Install Wizard", message_group=group_id)
603
- emit_info(" The wizard provides a better interface for browsing and installing MCP servers.", message_group=group_id)
604
- return
605
-
606
- try:
607
- if args:
608
- # Parse JSON from arguments
609
- import json
610
- json_str = ' '.join(args)
611
-
612
- try:
613
- config_dict = json.loads(json_str)
614
- except json.JSONDecodeError as e:
615
- emit_info(f"Invalid JSON: {e}", message_group=group_id)
616
- emit_info("Usage: /mcp add <json> or /mcp add (for wizard)", message_group=group_id)
617
- emit_info('Example: /mcp add {"name": "test", "type": "stdio", "command": "echo"}', message_group=group_id)
618
- return
619
-
620
- # Validate required fields
621
- if 'name' not in config_dict:
622
- emit_info("Missing required field: 'name'", message_group=group_id)
623
- return
624
- if 'type' not in config_dict:
625
- emit_info("Missing required field: 'type'", message_group=group_id)
626
- return
627
-
628
- # Create ServerConfig
629
- from code_puppy.mcp import ServerConfig
630
-
631
- name = config_dict.pop('name')
632
- server_type = config_dict.pop('type')
633
- enabled = config_dict.pop('enabled', True)
634
-
635
- # Everything else goes into config
636
- server_config = ServerConfig(
637
- id=f"{name}_{hash(name)}",
638
- name=name,
639
- type=server_type,
640
- enabled=enabled,
641
- config=config_dict # Remaining fields are server-specific config
642
- )
643
-
644
- # Register the server
645
- server_id = self.manager.register_server(server_config)
646
-
647
- if server_id:
648
- emit_info(f"✅ Added server '{name}' (ID: {server_id})", message_group=group_id)
649
-
650
- # Save to mcp_servers.json for persistence
651
- from code_puppy.config import MCP_SERVERS_FILE
652
- import os
653
-
654
- # Load existing configs
655
- if os.path.exists(MCP_SERVERS_FILE):
656
- with open(MCP_SERVERS_FILE, 'r') as f:
657
- data = json.load(f)
658
- servers = data.get("mcp_servers", {})
659
- else:
660
- servers = {}
661
- data = {"mcp_servers": servers}
662
-
663
- # Add new server
664
- servers[name] = config_dict
665
- servers[name]['type'] = server_type
666
-
667
- # Save back
668
- os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
669
- with open(MCP_SERVERS_FILE, 'w') as f:
670
- json.dump(data, f, indent=2)
671
-
672
- # Reload MCP servers
673
- from code_puppy.agent import reload_mcp_servers
674
- reload_mcp_servers()
675
-
676
- emit_info("Use '/mcp list' to see all servers", message_group=group_id)
677
- else:
678
- emit_info(f"Failed to add server '{name}'", message_group=group_id)
679
-
680
- else:
681
- # No arguments - launch interactive wizard with server templates
682
- success = self._run_interactive_install_wizard(group_id)
683
-
684
- if success:
685
- # Reload the agent to pick up new server
686
- from code_puppy.agent import reload_mcp_servers
687
- reload_mcp_servers()
688
-
689
- except ImportError as e:
690
- logger.error(f"Failed to import: {e}")
691
- emit_info("Required module not available", message_group=group_id)
692
- except Exception as e:
693
- logger.error(f"Error adding server: {e}")
694
- emit_info(f"Failed to add server: {e}", message_group=group_id)
695
-
696
- def _run_interactive_install_wizard(self, group_id: str) -> bool:
697
- """Run the interactive MCP server installation wizard using server templates."""
698
- try:
699
- from code_puppy.mcp.server_registry_catalog import catalog
700
- from code_puppy.mcp.system_tools import detector
701
- from code_puppy.messaging import emit_prompt
702
- import os
703
- import json
704
-
705
- emit_info("🧙 Interactive MCP Server Installation Wizard", message_group=group_id)
706
- emit_info("", message_group=group_id)
707
-
708
- # Step 1: Browse and select server
709
- selected_server = self._interactive_server_selection(group_id)
710
- if not selected_server:
711
- return False
712
-
713
- # Step 2: Get custom server name
714
- server_name = self._interactive_get_server_name(selected_server, group_id)
715
- if not server_name:
716
- return False
717
-
718
- # Step 3: Handle requirements and configuration
719
- success = self._interactive_configure_server(selected_server, server_name, group_id)
720
- return success
721
-
722
- except ImportError:
723
- emit_info("Server catalog not available, falling back to basic wizard", message_group=group_id)
724
- # Fall back to the old wizard
725
- from code_puppy.mcp.config_wizard import run_add_wizard
726
- return run_add_wizard(group_id)
727
- except Exception as e:
728
- emit_info(f"Installation wizard failed: {e}", message_group=group_id)
729
- return False
730
-
731
- def _interactive_server_selection(self, group_id: str):
732
- """Interactive server selection from catalog."""
733
- from code_puppy.mcp.server_registry_catalog import catalog
734
- from code_puppy.messaging import emit_prompt
735
-
736
- while True:
737
- emit_info("📦 Available MCP Servers:", message_group=group_id)
738
- emit_info("", message_group=group_id)
739
-
740
- # Show popular servers first
741
- popular = catalog.get_popular(5)
742
- if popular:
743
- emit_info("[bold]Popular Servers:[/bold]", message_group=group_id)
744
- for i, server in enumerate(popular):
745
- indicators = []
746
- if server.verified:
747
- indicators.append("✓")
748
- if server.popular:
749
- indicators.append("⭐")
750
-
751
- emit_info(f" {i+1}. {server.display_name} {''.join(indicators)}", message_group=group_id)
752
- emit_info(f" {server.description[:80]}...", message_group=group_id)
753
- emit_info("", message_group=group_id)
754
-
755
- # Prompt for selection
756
- choice = emit_prompt("Enter server number (1-5), 'search <term>' to search, or 'list' to see all categories: ")
757
-
758
- if not choice.strip():
759
- if emit_prompt("Cancel installation? [y/N]: ").lower().startswith('y'):
760
- return None
761
- continue
762
-
763
- choice = choice.strip()
764
-
765
- # Handle numeric selection
766
- if choice.isdigit():
767
- try:
768
- index = int(choice) - 1
769
- if 0 <= index < len(popular):
770
- return popular[index]
771
- else:
772
- emit_info("Invalid selection. Please try again.", message_group=group_id)
773
- continue
774
- except ValueError:
775
- pass
776
-
777
- # Handle search
778
- if choice.lower().startswith('search '):
779
- search_term = choice[7:].strip()
780
- results = catalog.search(search_term)
781
- if results:
782
- emit_info(f"\n🔍 Search results for '{search_term}':", message_group=group_id)
783
- for i, server in enumerate(results[:10]):
784
- indicators = []
785
- if server.verified:
786
- indicators.append("✓")
787
- if server.popular:
788
- indicators.append("⭐")
789
- emit_info(f" {i+1}. {server.display_name} {''.join(indicators)}", message_group=group_id)
790
- emit_info(f" {server.description[:80]}...", message_group=group_id)
791
-
792
- selection = emit_prompt(f"\nSelect server (1-{min(len(results), 10)}): ")
793
- if selection.isdigit():
794
- try:
795
- index = int(selection) - 1
796
- if 0 <= index < len(results):
797
- return results[index]
798
- except ValueError:
799
- pass
800
- else:
801
- emit_info(f"No servers found for '{search_term}'", message_group=group_id)
802
- continue
803
-
804
- # Handle list categories
805
- if choice.lower() == 'list':
806
- categories = catalog.list_categories()
807
- emit_info("\n📂 Categories:", message_group=group_id)
808
- for i, category in enumerate(categories):
809
- servers_count = len(catalog.get_by_category(category))
810
- emit_info(f" {i+1}. {category} ({servers_count} servers)", message_group=group_id)
811
-
812
- cat_choice = emit_prompt(f"\nSelect category (1-{len(categories)}): ")
813
- if cat_choice.isdigit():
814
- try:
815
- index = int(cat_choice) - 1
816
- if 0 <= index < len(categories):
817
- category_servers = catalog.get_by_category(categories[index])
818
- emit_info(f"\n📦 {categories[index]} Servers:", message_group=group_id)
819
- for i, server in enumerate(category_servers):
820
- indicators = []
821
- if server.verified:
822
- indicators.append("✓")
823
- if server.popular:
824
- indicators.append("⭐")
825
- emit_info(f" {i+1}. {server.display_name} {''.join(indicators)}", message_group=group_id)
826
- emit_info(f" {server.description[:80]}...", message_group=group_id)
827
-
828
- server_choice = emit_prompt(f"\nSelect server (1-{len(category_servers)}): ")
829
- if server_choice.isdigit():
830
- try:
831
- index = int(server_choice) - 1
832
- if 0 <= index < len(category_servers):
833
- return category_servers[index]
834
- except ValueError:
835
- pass
836
- except ValueError:
837
- pass
838
- continue
839
-
840
- emit_info("Invalid choice. Please try again.", message_group=group_id)
841
-
842
- def _interactive_get_server_name(self, selected_server, group_id: str) -> str:
843
- """Get custom server name from user."""
844
- from code_puppy.messaging import emit_prompt
845
-
846
- emit_info(f"\n🏷️ Server: {selected_server.display_name}", message_group=group_id)
847
- emit_info(f"Description: {selected_server.description}", message_group=group_id)
848
- emit_info("", message_group=group_id)
849
-
850
- while True:
851
- name = emit_prompt(f"Enter custom name for this server [{selected_server.name}]: ").strip()
852
-
853
- if not name:
854
- name = selected_server.name
855
-
856
- # Validate name
857
- if not name.replace('-', '').replace('_', '').replace('.', '').isalnum():
858
- emit_info("Name must contain only letters, numbers, hyphens, underscores, and dots", message_group=group_id)
859
- continue
860
-
861
- # Check if name already exists
862
- existing_server = self._find_server_id_by_name(name)
863
- if existing_server:
864
- override = emit_prompt(f"Server '{name}' already exists. Override it? [y/N]: ")
865
- if not override.lower().startswith('y'):
866
- continue
867
-
868
- return name
869
-
870
- def _interactive_configure_server(self, selected_server, server_name: str, group_id: str) -> bool:
871
- """Configure the server with requirements validation."""
872
- from code_puppy.mcp.system_tools import detector
873
- from code_puppy.messaging import emit_prompt
874
- import os
875
- import json
876
-
877
- requirements = selected_server.get_requirements()
878
-
879
- emit_info(f"\n⚙️ Configuring server: {server_name}", message_group=group_id)
880
- emit_info("", message_group=group_id)
881
-
882
- # Step 1: Check system requirements
883
- if not self._interactive_check_system_requirements(requirements, group_id):
884
- return False
885
-
886
- # Step 2: Collect environment variables
887
- env_vars = self._interactive_collect_env_vars(requirements, group_id)
888
-
889
- # Step 3: Collect command line arguments
890
- cmd_args = self._interactive_collect_cmd_args(requirements, group_id)
891
-
892
- # Step 4: Show summary and confirm
893
- if not self._interactive_confirm_installation(selected_server, server_name, env_vars, cmd_args, group_id):
894
- return False
895
-
896
- # Step 5: Install the server
897
- return self._interactive_install_server(selected_server, server_name, env_vars, cmd_args, group_id)
898
-
899
- def _interactive_check_system_requirements(self, requirements, group_id: str) -> bool:
900
- """Check and validate system requirements."""
901
- from code_puppy.mcp.system_tools import detector
902
-
903
- required_tools = requirements.required_tools
904
- if not required_tools:
905
- return True
906
-
907
- emit_info("🔧 Checking system requirements...", message_group=group_id)
908
-
909
- tool_status = detector.detect_tools(required_tools)
910
- all_good = True
911
-
912
- for tool_name, tool_info in tool_status.items():
913
- if tool_info.available:
914
- status_text = f"✅ {tool_name}"
915
- if tool_info.version:
916
- status_text += f" ({tool_info.version})"
917
- emit_info(status_text, message_group=group_id)
918
- else:
919
- status_text = f"❌ {tool_name} - {tool_info.error or 'Not found'}"
920
- emit_info(status_text, message_group=group_id)
921
-
922
- # Show installation suggestions
923
- suggestions = detector.get_installation_suggestions(tool_name)
924
- if suggestions:
925
- emit_info(f" Install: {suggestions[0]}", message_group=group_id)
926
- all_good = False
927
-
928
- if not all_good:
929
- emit_info("", message_group=group_id)
930
- cont = emit_prompt("Some tools are missing. Continue anyway? [y/N]: ")
931
- if not cont.lower().startswith('y'):
932
- emit_info("Installation cancelled", message_group=group_id)
933
- return False
934
-
935
- emit_info("", message_group=group_id)
936
- return True
937
-
938
- def _interactive_collect_env_vars(self, requirements, group_id: str) -> dict:
939
- """Collect environment variables from user."""
940
- from code_puppy.messaging import emit_prompt
941
- import os
942
-
943
- env_vars = {}
944
- required_env_vars = requirements.environment_vars
945
-
946
- if not required_env_vars:
947
- return env_vars
948
-
949
- emit_info("🔐 Environment Variables:", message_group=group_id)
950
-
951
- for var in required_env_vars:
952
- # Check if already set
953
- current_value = os.environ.get(var, "")
954
-
955
- if current_value:
956
- emit_info(f"✅ {var} (already set)", message_group=group_id)
957
- env_vars[var] = current_value
958
- else:
959
- value = emit_prompt(f"📝 Enter value for {var}: ").strip()
960
- if value:
961
- env_vars[var] = value
962
- # Set in current environment too
963
- os.environ[var] = value
964
- else:
965
- emit_info(f"⚠️ {var} left empty", message_group=group_id)
966
-
967
- emit_info("", message_group=group_id)
968
- return env_vars
969
-
970
- def _interactive_collect_cmd_args(self, requirements, group_id: str) -> dict:
971
- """Collect command line arguments from user."""
972
- from code_puppy.messaging import emit_prompt
973
-
974
- cmd_args = {}
975
- required_args = requirements.command_line_args
976
-
977
- if not required_args:
978
- return cmd_args
979
-
980
- emit_info("⚡ Command Line Arguments:", message_group=group_id)
981
-
982
- for arg_config in required_args:
983
- name = arg_config.get("name", "")
984
- prompt_text = arg_config.get("prompt", name)
985
- default = arg_config.get("default", "")
986
- required = arg_config.get("required", True)
987
-
988
- indicator = "⚡" if required else "🔧"
989
- label = f"{indicator} {prompt_text}"
990
- if not required:
991
- label += " (optional)"
992
- if default:
993
- label += f" [{default}]"
994
-
995
- value = emit_prompt(f"{label}: ").strip()
996
-
997
- if not value and default:
998
- value = default
999
-
1000
- if value:
1001
- cmd_args[name] = value
1002
- elif required:
1003
- emit_info(f"⚠️ Required argument '{name}' left empty", message_group=group_id)
1004
-
1005
- emit_info("", message_group=group_id)
1006
- return cmd_args
1007
-
1008
- def _interactive_confirm_installation(self, selected_server, server_name: str, env_vars: dict, cmd_args: dict, group_id: str) -> bool:
1009
- """Show summary and confirm installation."""
1010
- from code_puppy.messaging import emit_prompt
1011
-
1012
- emit_info("📋 Installation Summary:", message_group=group_id)
1013
- emit_info(f" Server: {selected_server.display_name}", message_group=group_id)
1014
- emit_info(f" Name: {server_name}", message_group=group_id)
1015
- emit_info(f" Type: {selected_server.type}", message_group=group_id)
1016
-
1017
- if env_vars:
1018
- emit_info(f" Environment variables: {len(env_vars)} set", message_group=group_id)
1019
-
1020
- if cmd_args:
1021
- emit_info(f" Command arguments: {len(cmd_args)} configured", message_group=group_id)
1022
-
1023
- emit_info("", message_group=group_id)
1024
-
1025
- confirm = emit_prompt("Install this server configuration? [Y/n]: ")
1026
- return not confirm.lower().startswith('n')
1027
-
1028
- def _interactive_install_server(self, selected_server, server_name: str, env_vars: dict, cmd_args: dict, group_id: str) -> bool:
1029
- """Actually install and register the server."""
1030
- try:
1031
- # Get server config with command line argument overrides
1032
- config_dict = selected_server.to_server_config(server_name, **cmd_args)
1033
-
1034
- # Update the config with actual environment variable values
1035
- if 'env' in config_dict:
1036
- for env_key, env_value in config_dict['env'].items():
1037
- # If it's a placeholder like $GITHUB_TOKEN, replace with actual value
1038
- if env_value.startswith('$'):
1039
- var_name = env_value[1:] # Remove the $
1040
- if var_name in env_vars:
1041
- config_dict['env'][env_key] = env_vars[var_name]
1042
-
1043
- # Create and register the server
1044
- from code_puppy.mcp import ServerConfig
1045
-
1046
- server_config = ServerConfig(
1047
- id=server_name,
1048
- name=server_name,
1049
- type=config_dict.pop('type'),
1050
- enabled=True,
1051
- config=config_dict
1052
- )
1053
-
1054
- server_id = self.manager.register_server(server_config)
1055
-
1056
- if server_id:
1057
- # Save to mcp_servers.json for persistence
1058
- from code_puppy.config import MCP_SERVERS_FILE
1059
- import json
1060
- import os
1061
-
1062
- if os.path.exists(MCP_SERVERS_FILE):
1063
- with open(MCP_SERVERS_FILE, 'r') as f:
1064
- data = json.load(f)
1065
- servers = data.get("mcp_servers", {})
1066
- else:
1067
- servers = {}
1068
- data = {"mcp_servers": servers}
1069
-
1070
- servers[server_name] = config_dict
1071
- servers[server_name]['type'] = server_config.type
1072
-
1073
- os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
1074
- with open(MCP_SERVERS_FILE, 'w') as f:
1075
- json.dump(data, f, indent=2)
1076
-
1077
- emit_info(f"✅ Successfully installed '{server_name}' from {selected_server.display_name}!", message_group=group_id)
1078
- emit_info(f"Use '/mcp start {server_name}' to start the server", message_group=group_id)
1079
- return True
1080
- else:
1081
- emit_info(f"❌ Failed to register server", message_group=group_id)
1082
- return False
1083
-
1084
- except Exception as e:
1085
- emit_info(f"❌ Installation failed: {str(e)}", message_group=group_id)
1086
- return False
1087
-
1088
- def cmd_remove(self, args: List[str]) -> None:
1089
- """
1090
- Remove an MCP server.
1091
-
1092
- Args:
1093
- args: Command arguments, expects [server_name]
1094
- """
1095
- import uuid
1096
- group_id = str(uuid.uuid4())
1097
-
1098
- if not args:
1099
- emit_info("Usage: /mcp remove <server_name>", message_group=group_id)
1100
- return
1101
-
1102
- server_name = args[0]
1103
-
1104
- try:
1105
- # Find server by name
1106
- server_id = self._find_server_id_by_name(server_name)
1107
- if not server_id:
1108
- emit_info(f"Server '{server_name}' not found", message_group=group_id)
1109
- self._suggest_similar_servers(server_name)
1110
- return
1111
-
1112
- # Actually remove the server
1113
- success = self.manager.remove_server(server_id)
1114
-
1115
- if success:
1116
- emit_info(f"✓ Removed server: {server_name}", message_group=group_id)
1117
-
1118
- # Also remove from mcp_servers.json
1119
- from code_puppy.config import MCP_SERVERS_FILE
1120
- import json
1121
- import os
1122
-
1123
- if os.path.exists(MCP_SERVERS_FILE):
1124
- try:
1125
- with open(MCP_SERVERS_FILE, 'r') as f:
1126
- data = json.load(f)
1127
- servers = data.get("mcp_servers", {})
1128
-
1129
- # Remove the server if it exists
1130
- if server_name in servers:
1131
- del servers[server_name]
1132
-
1133
- # Save back
1134
- with open(MCP_SERVERS_FILE, 'w') as f:
1135
- json.dump(data, f, indent=2)
1136
- except Exception as e:
1137
- logger.warning(f"Could not update mcp_servers.json: {e}")
1138
- else:
1139
- emit_info(f"✗ Failed to remove server: {server_name}", message_group=group_id)
1140
-
1141
- except Exception as e:
1142
- logger.error(f"Error removing server '{server_name}': {e}")
1143
- emit_info(f"Failed to remove server: {e}", message_group=group_id)
1144
-
1145
- def cmd_logs(self, args: List[str]) -> None:
1146
- """
1147
- Show recent events/logs for a server.
1148
-
1149
- Args:
1150
- args: Command arguments, expects [server_name] and optional [limit]
1151
- """
1152
- import uuid
1153
- group_id = str(uuid.uuid4())
1154
-
1155
- if not args:
1156
- emit_info("Usage: /mcp logs <server_name> [limit]", message_group=group_id)
1157
- return
1158
-
1159
- server_name = args[0]
1160
- limit = 10 # Default limit
1161
-
1162
- if len(args) > 1:
1163
- try:
1164
- limit = int(args[1])
1165
- if limit <= 0 or limit > 100:
1166
- emit_info("Limit must be between 1 and 100, using default: 10", message_group=group_id)
1167
- limit = 10
1168
- except ValueError:
1169
- emit_info(f"Invalid limit '{args[1]}', using default: 10", message_group=group_id)
1170
-
1171
- try:
1172
- # Find server by name
1173
- server_id = self._find_server_id_by_name(server_name)
1174
- if not server_id:
1175
- emit_info(f"Server '{server_name}' not found", message_group=group_id)
1176
- self._suggest_similar_servers(server_name)
1177
- return
1178
-
1179
- # Get server status which includes recent events
1180
- status = self.manager.get_server_status(server_id)
1181
-
1182
- if not status.get("exists", True):
1183
- emit_info(f"Server '{server_name}' status not available", message_group=group_id)
1184
- return
1185
-
1186
- recent_events = status.get("recent_events", [])
1187
-
1188
- if not recent_events:
1189
- emit_info(f"No recent events for server: {server_name}", message_group=group_id)
1190
- return
1191
-
1192
- # Show events in a table
1193
- table = Table(title=f"📋 Recent Events for {server_name} (last {limit})")
1194
- table.add_column("Time", style="dim", no_wrap=True)
1195
- table.add_column("Event", style="cyan")
1196
- table.add_column("Details", style="dim")
1197
-
1198
- # Take only the requested number of events
1199
- events_to_show = recent_events[-limit:] if len(recent_events) > limit else recent_events
1200
-
1201
- for event in reversed(events_to_show): # Show newest first
1202
- timestamp = datetime.fromisoformat(event["timestamp"])
1203
- time_str = timestamp.strftime("%H:%M:%S")
1204
- event_type = event["event_type"]
1205
-
1206
- # Format details
1207
- details = event.get("details", {})
1208
- details_str = details.get("message", "")
1209
- if not details_str and "error" in details:
1210
- details_str = str(details["error"])
1211
-
1212
- # Color code event types
1213
- event_style = "cyan"
1214
- if "error" in event_type.lower():
1215
- event_style = "red"
1216
- elif event_type in ["started", "enabled", "registered"]:
1217
- event_style = "green"
1218
- elif event_type in ["stopped", "disabled"]:
1219
- event_style = "yellow"
1220
-
1221
- table.add_row(
1222
- time_str,
1223
- Text(event_type, style=event_style),
1224
- details_str or "-"
1225
- )
1226
- emit_info(table, message_group=group_id)
1227
-
1228
- except Exception as e:
1229
- logger.error(f"Error getting logs for server '{server_name}': {e}")
1230
- emit_info(f"Failed to get server logs: {e}", message_group=group_id)
1231
-
1232
- def cmd_help(self, args: List[str]) -> None:
1233
- """
1234
- Show help for MCP commands.
1235
-
1236
- Args:
1237
- args: Command arguments (unused)
1238
- """
1239
- from rich.text import Text
1240
- from rich.console import Console
1241
-
1242
- # Create a console for rendering
1243
- console = Console()
1244
-
1245
- # Build help text programmatically to avoid markup conflicts
1246
- help_lines = []
1247
-
1248
- # Title
1249
- help_lines.append(Text("MCP Server Management Commands", style="bold magenta"))
1250
- help_lines.append(Text(""))
1251
-
1252
- # Registry Commands
1253
- help_lines.append(Text("Registry Commands:", style="bold cyan"))
1254
- help_lines.append(Text("/mcp search", style="cyan") + Text(" [query] Search 30+ pre-configured servers"))
1255
- help_lines.append(Text("/mcp install", style="cyan") + Text(" <id> Install server from registry"))
1256
- help_lines.append(Text(""))
1257
-
1258
- # Core Commands
1259
- help_lines.append(Text("Core Commands:", style="bold cyan"))
1260
- help_lines.append(Text("/mcp", style="cyan") + Text(" Show server status dashboard"))
1261
- help_lines.append(Text("/mcp list", style="cyan") + Text(" List all registered servers"))
1262
- help_lines.append(Text("/mcp start", style="cyan") + Text(" <name> Start a specific server"))
1263
- help_lines.append(Text("/mcp start-all", style="cyan") + Text(" Start all servers"))
1264
- help_lines.append(Text("/mcp stop", style="cyan") + Text(" <name> Stop a specific server"))
1265
- help_lines.append(Text("/mcp stop-all", style="cyan") + Text(" [group_id] Stop all running servers"))
1266
- help_lines.append(Text("/mcp restart", style="cyan") + Text(" <name> Restart a specific server"))
1267
- help_lines.append(Text(""))
1268
-
1269
- # Management Commands
1270
- help_lines.append(Text("Management Commands:", style="bold cyan"))
1271
- help_lines.append(Text("/mcp status", style="cyan") + Text(" [name] Show detailed status (all servers or specific)"))
1272
- help_lines.append(Text("/mcp test", style="cyan") + Text(" <name> Test connectivity to a server"))
1273
- help_lines.append(Text("/mcp logs", style="cyan") + Text(" <name> [limit] Show recent events (default limit: 10)"))
1274
- help_lines.append(Text("/mcp add", style="cyan") + Text(" [json] Add new server (JSON or wizard)"))
1275
- help_lines.append(Text("/mcp remove", style="cyan") + Text(" <name> Remove/disable a server"))
1276
- help_lines.append(Text("/mcp help", style="cyan") + Text(" Show this help message"))
1277
- help_lines.append(Text(""))
1278
-
1279
- # Status Indicators
1280
- help_lines.append(Text("Status Indicators:", style="bold"))
1281
- help_lines.append(Text("✓ Running ✗ Stopped ⚠ Error ⏸ Quarantined ⭐ Popular"))
1282
- help_lines.append(Text(""))
1283
-
1284
- # Examples
1285
- help_lines.append(Text("Examples:", style="bold"))
1286
- examples_text = """/mcp search database # Find database servers
1287
- /mcp install postgres # Install PostgreSQL server
1288
- /mcp start filesystem # Start a specific server
1289
- /mcp start-all # Start all servers at once
1290
- /mcp stop-all # Stop all running servers
1291
- /mcp add {"name": "test", "type": "stdio", "command": "echo"}"""
1292
- help_lines.append(Text(examples_text, style="dim"))
1293
-
1294
- # Combine all lines
1295
- final_text = Text()
1296
- for i, line in enumerate(help_lines):
1297
- if i > 0:
1298
- final_text.append("\n")
1299
- final_text.append_text(line)
1300
-
1301
- import uuid
1302
- group_id = str(uuid.uuid4())
1303
- emit_info(final_text, message_group=group_id)
1304
-
1305
- def cmd_search(self, args: List[str], group_id: str = None) -> None:
1306
- """
1307
- Search for pre-configured MCP servers in the registry.
1308
-
1309
- Args:
1310
- args: Search query terms
1311
- group_id: Optional message group ID for grouping related messages
1312
- """
1313
- if group_id is None:
1314
- import uuid
1315
- group_id = str(uuid.uuid4())
1316
-
1317
- try:
1318
- from code_puppy.mcp.server_registry_catalog import catalog
1319
- from rich.table import Table
1320
-
1321
- if not args:
1322
- # Show popular servers if no query
1323
- emit_info("[bold cyan]Popular MCP Servers:[/bold cyan]\n", message_group=group_id)
1324
- servers = catalog.get_popular(15)
1325
- else:
1326
- query = ' '.join(args)
1327
- emit_info(f"[bold cyan]Searching for: {query}[/bold cyan]\n", message_group=group_id)
1328
- servers = catalog.search(query)
1329
-
1330
- if not servers:
1331
- emit_info("[yellow]No servers found matching your search[/yellow]", message_group=group_id)
1332
- emit_info("Try: /mcp search database, /mcp search file, /mcp search git", message_group=group_id)
1333
- return
1334
-
1335
- # Create results table
1336
- table = Table(show_header=True, header_style="bold magenta")
1337
- table.add_column("ID", style="cyan", width=20)
1338
- table.add_column("Name", style="green")
1339
- table.add_column("Category", style="yellow")
1340
- table.add_column("Description", style="white")
1341
- table.add_column("Tags", style="dim")
1342
-
1343
- for server in servers[:20]: # Limit to 20 results
1344
- tags = ', '.join(server.tags[:3]) # Show first 3 tags
1345
- if len(server.tags) > 3:
1346
- tags += '...'
1347
-
1348
- # Add verified/popular indicators
1349
- indicators = []
1350
- if server.verified:
1351
- indicators.append("✓")
1352
- if server.popular:
1353
- indicators.append("⭐")
1354
- name_display = server.display_name
1355
- if indicators:
1356
- name_display += f" {''.join(indicators)}"
1357
-
1358
- table.add_row(
1359
- server.id,
1360
- name_display,
1361
- server.category,
1362
- server.description[:50] + "..." if len(server.description) > 50 else server.description,
1363
- tags
1364
- )
1365
-
1366
- # The first message established the group, subsequent messages will auto-group
1367
- emit_system_message(table, message_group=group_id)
1368
- emit_info("\n[dim]✓ = Verified ⭐ = Popular[/dim]", message_group=group_id)
1369
- emit_info("[yellow]To install:[/yellow] /mcp install <id>", message_group=group_id)
1370
- emit_info("[yellow]For details:[/yellow] /mcp search <specific-term>", message_group=group_id)
1371
-
1372
- except ImportError:
1373
- emit_info("[red]Server registry not available[/red]", message_group=group_id)
1374
- except Exception as e:
1375
- logger.error(f"Error searching servers: {e}")
1376
- emit_info(f"[red]Search failed: {e}[/red]", message_group=group_id)
1377
-
1378
- def cmd_install(self, args: List[str], group_id: str = None) -> None:
1379
- """
1380
- Install a pre-configured MCP server from the registry.
1381
-
1382
- Args:
1383
- args: Server ID and optional custom name
1384
- """
1385
- if group_id is None:
1386
- import uuid
1387
- group_id = str(uuid.uuid4())
1388
-
1389
- try:
1390
- # If in TUI mode, show message to use Ctrl+T
1391
- if is_tui_mode():
1392
- emit_info("In TUI mode, use Ctrl+T to open the MCP Install Wizard", message_group=group_id)
1393
- return
1394
-
1395
- # In interactive mode, use the new comprehensive installer
1396
- if not args:
1397
- # No args - launch interactive wizard
1398
- success = self._run_interactive_install_wizard(group_id)
1399
- if success:
1400
- from code_puppy.agent import reload_mcp_servers
1401
- reload_mcp_servers()
1402
- return
1403
-
1404
- # Has args - install directly from catalog
1405
- server_id = args[0]
1406
- success = self._install_from_catalog(server_id, group_id)
1407
- if success:
1408
- from code_puppy.agent import reload_mcp_servers
1409
- reload_mcp_servers()
1410
- return
1411
-
1412
- except ImportError:
1413
- emit_info("Server registry not available", message_group=group_id)
1414
- except Exception as e:
1415
- logger.error(f"Error installing server: {e}")
1416
- emit_info(f"Installation failed: {e}", message_group=group_id)
1417
-
1418
- def _install_from_catalog(self, server_name_or_id: str, group_id: str) -> bool:
1419
- """Install a server directly from the catalog by name or ID."""
1420
- try:
1421
- from code_puppy.mcp.server_registry_catalog import catalog
1422
-
1423
- # Try to find server by ID first, then by name/search
1424
- selected_server = catalog.get_by_id(server_name_or_id)
1425
-
1426
- if not selected_server:
1427
- # Try searching by name
1428
- results = catalog.search(server_name_or_id)
1429
- if not results:
1430
- emit_info(f"❌ No server found matching '{server_name_or_id}'", message_group=group_id)
1431
- emit_info("Try '/mcp add' to browse available servers", message_group=group_id)
1432
- return False
1433
- elif len(results) == 1:
1434
- selected_server = results[0]
1435
- else:
1436
- # Multiple matches, show them
1437
- emit_info(f"🔍 Multiple servers found matching '{server_name_or_id}':", message_group=group_id)
1438
- for i, server in enumerate(results[:5]):
1439
- indicators = []
1440
- if server.verified:
1441
- indicators.append("✓")
1442
- if server.popular:
1443
- indicators.append("⭐")
1444
-
1445
- indicator_str = ''
1446
- if indicators:
1447
- indicator_str = ' ' + ''.join(indicators)
1448
-
1449
- emit_info(f" {i+1}. {server.display_name}{indicator_str}", message_group=group_id)
1450
- emit_info(f" ID: {server.id}", message_group=group_id)
1451
-
1452
- emit_info(f"Please use the exact server ID: '/mcp add <server_id>'", message_group=group_id)
1453
- return False
1454
-
1455
- # Show what we're installing
1456
- emit_info(f"📦 Installing: {selected_server.display_name}", message_group=group_id)
1457
- description = selected_server.description if selected_server.description else "No description available"
1458
- emit_info(f"Description: {description}", message_group=group_id)
1459
- emit_info("", message_group=group_id)
1460
-
1461
- # Get custom name (default to server name)
1462
- from code_puppy.messaging import emit_prompt
1463
- server_name = emit_prompt(f"Enter custom name for this server [{selected_server.name}]: ").strip()
1464
- if not server_name:
1465
- server_name = selected_server.name
1466
-
1467
- # Check if name already exists
1468
- existing_server = self._find_server_id_by_name(server_name)
1469
- if existing_server:
1470
- override = emit_prompt(f"Server '{server_name}' already exists. Override it? [y/N]: ")
1471
- if not override.lower().startswith('y'):
1472
- emit_info("Installation cancelled", message_group=group_id)
1473
- return False
1474
-
1475
- # Configure the server with requirements
1476
- requirements = selected_server.get_requirements()
1477
-
1478
- # Check system requirements
1479
- if not self._interactive_check_system_requirements(requirements, group_id):
1480
- return False
1481
-
1482
- # Collect environment variables
1483
- env_vars = self._interactive_collect_env_vars(requirements, group_id)
1484
-
1485
- # Collect command line arguments
1486
- cmd_args = self._interactive_collect_cmd_args(requirements, group_id)
1487
-
1488
- # Show summary and confirm
1489
- if not self._interactive_confirm_installation(selected_server, server_name, env_vars, cmd_args, group_id):
1490
- return False
1491
-
1492
- # Install the server
1493
- return self._interactive_install_server(selected_server, server_name, env_vars, cmd_args, group_id)
1494
-
1495
- except ImportError:
1496
- emit_info("Server catalog not available", message_group=group_id)
1497
- return False
1498
- except Exception as e:
1499
- import traceback
1500
- emit_info(f"❌ Installation failed: {str(e)}", message_group=group_id)
1501
- emit_info(f"[dim]Error details: {traceback.format_exc()}[/dim]", message_group=group_id)
1502
- return False
1503
-
1504
- def _find_server_id_by_name(self, server_name: str) -> Optional[str]:
1505
- """
1506
- Find a server ID by its name.
1507
-
1508
- Args:
1509
- server_name: Name of the server to find
1510
-
1511
- Returns:
1512
- Server ID if found, None otherwise
1513
- """
1514
- try:
1515
- servers = self.manager.list_servers()
1516
- for server in servers:
1517
- if server.name.lower() == server_name.lower():
1518
- return server.id
1519
- return None
1520
- except Exception as e:
1521
- logger.error(f"Error finding server by name '{server_name}': {e}")
1522
- return None
1523
-
1524
- def _suggest_similar_servers(self, server_name: str, group_id: str = None) -> None:
1525
- """
1526
- Suggest similar server names when a server is not found.
1527
-
1528
- Args:
1529
- server_name: The server name that was not found
1530
- group_id: Optional message group ID for grouping related messages
1531
- """
1532
- try:
1533
- servers = self.manager.list_servers()
1534
- if not servers:
1535
- emit_info("No servers are registered", message_group=group_id)
1536
- return
1537
-
1538
- # Simple suggestion based on partial matching
1539
- suggestions = []
1540
- server_name_lower = server_name.lower()
1541
-
1542
- for server in servers:
1543
- if server_name_lower in server.name.lower():
1544
- suggestions.append(server.name)
1545
-
1546
- if suggestions:
1547
- emit_info(f"Did you mean: {', '.join(suggestions)}", message_group=group_id)
1548
- else:
1549
- server_names = [s.name for s in servers]
1550
- emit_info(f"Available servers: {', '.join(server_names)}", message_group=group_id)
1551
-
1552
- except Exception as e:
1553
- logger.error(f"Error suggesting similar servers: {e}")
1554
-
1555
- def _format_state_indicator(self, state: ServerState) -> Text:
1556
- """
1557
- Format a server state with appropriate color and icon.
1558
-
1559
- Args:
1560
- state: Server state to format
1561
-
1562
- Returns:
1563
- Rich Text object with colored state indicator
1564
- """
1565
- state_map = {
1566
- ServerState.RUNNING: ("✓ Run", "green"),
1567
- ServerState.STOPPED: ("✗ Stop", "red"),
1568
- ServerState.STARTING: ("↗ Start", "yellow"),
1569
- ServerState.STOPPING: ("↙ Stop", "yellow"),
1570
- ServerState.ERROR: ("⚠ Err", "red"),
1571
- ServerState.QUARANTINED: ("⏸ Quar", "yellow"),
1572
- }
1573
-
1574
- display, color = state_map.get(state, ("? Unk", "dim"))
1575
- return Text(display, style=color)
1576
-
1577
- def _format_uptime(self, uptime_seconds: Optional[float]) -> str:
1578
- """
1579
- Format uptime in a human-readable format.
1580
-
1581
- Args:
1582
- uptime_seconds: Uptime in seconds, or None
1583
-
1584
- Returns:
1585
- Formatted uptime string
1586
- """
1587
- if uptime_seconds is None or uptime_seconds <= 0:
1588
- return "-"
1589
-
1590
- # Convert to readable format
1591
- if uptime_seconds < 60:
1592
- return f"{int(uptime_seconds)}s"
1593
- elif uptime_seconds < 3600:
1594
- minutes = int(uptime_seconds // 60)
1595
- seconds = int(uptime_seconds % 60)
1596
- return f"{minutes}m {seconds}s"
1597
- else:
1598
- hours = int(uptime_seconds // 3600)
1599
- minutes = int((uptime_seconds % 3600) // 60)
1600
- return f"{hours}h {minutes}m"
1601
-
1602
- def _show_detailed_server_status(self, server_id: str, server_name: str, group_id: str = None) -> None:
1603
- """
1604
- Show comprehensive status information for a specific server.
1605
-
1606
- Args:
1607
- server_id: ID of the server
1608
- server_name: Name of the server
1609
- group_id: Optional message group ID
1610
- """
1611
- if group_id is None:
1612
- import uuid
1613
- group_id = str(uuid.uuid4())
1614
-
1615
- try:
1616
- status = self.manager.get_server_status(server_id)
1617
-
1618
- if not status.get("exists", True):
1619
- emit_info(f"Server '{server_name}' not found or not accessible", message_group=group_id)
1620
- return
1621
-
1622
- # Create detailed status panel
1623
- status_lines = []
1624
-
1625
- # Basic information
1626
- status_lines.append(f"[bold]Server:[/bold] {server_name}")
1627
- status_lines.append(f"[bold]ID:[/bold] {server_id}")
1628
- status_lines.append(f"[bold]Type:[/bold] {status.get('type', 'unknown').upper()}")
1629
-
1630
- # State and status
1631
- state = status.get('state', 'unknown')
1632
- state_display = self._format_state_indicator(ServerState(state) if state in [s.value for s in ServerState] else ServerState.STOPPED)
1633
- status_lines.append(f"[bold]State:[/bold] {state_display}")
1634
-
1635
- enabled = status.get('enabled', False)
1636
- status_lines.append(f"[bold]Enabled:[/bold] {'✓ Yes' if enabled else '✗ No'}")
1637
-
1638
- # Check async lifecycle manager status if available
1639
- try:
1640
- from code_puppy.mcp.async_lifecycle import get_lifecycle_manager
1641
- lifecycle_mgr = get_lifecycle_manager()
1642
- if lifecycle_mgr.is_running(server_id):
1643
- status_lines.append(f"[bold]Process:[/bold] [green]✓ Active (subprocess/connection running)[/green]")
1644
- else:
1645
- status_lines.append(f"[bold]Process:[/bold] [dim]Not active[/dim]")
1646
- except Exception:
1647
- pass # Lifecycle manager not available
1648
-
1649
- quarantined = status.get('quarantined', False)
1650
- if quarantined:
1651
- status_lines.append(f"[bold]Quarantined:[/bold] [yellow]⚠ Yes[/yellow]")
1652
-
1653
- # Timing information
1654
- uptime = status.get('tracker_uptime')
1655
- if uptime:
1656
- uptime_str = self._format_uptime(uptime.total_seconds() if hasattr(uptime, 'total_seconds') else uptime)
1657
- status_lines.append(f"[bold]Uptime:[/bold] {uptime_str}")
1658
-
1659
- # Error information
1660
- error_msg = status.get('error_message')
1661
- if error_msg:
1662
- status_lines.append(f"[bold]Error:[/bold] [red]{error_msg}[/red]")
1663
-
1664
- # Event information
1665
- event_count = status.get('recent_events_count', 0)
1666
- status_lines.append(f"[bold]Recent Events:[/bold] {event_count}")
1667
-
1668
- # Metadata
1669
- metadata = status.get('tracker_metadata', {})
1670
- if metadata:
1671
- status_lines.append(f"[bold]Metadata:[/bold] {len(metadata)} keys")
1672
-
1673
- # Create and show the panel
1674
- panel_content = "\n".join(status_lines)
1675
- panel = Panel(
1676
- panel_content,
1677
- title=f"🔌 {server_name} Status",
1678
- border_style="cyan"
1679
- )
1680
-
1681
- emit_info(panel, message_group=group_id)
1682
-
1683
- # Show recent events if available
1684
- recent_events = status.get('recent_events', [])
1685
- if recent_events:
1686
- emit_info("\n📋 Recent Events:", message_group=group_id)
1687
- for event in recent_events[-5:]: # Show last 5 events
1688
- timestamp = datetime.fromisoformat(event["timestamp"])
1689
- time_str = timestamp.strftime("%H:%M:%S")
1690
- event_type = event["event_type"]
1691
- details = event.get("details", {})
1692
- message = details.get("message", "")
1693
-
1694
- emit_info(f" [dim]{time_str}[/dim] [cyan]{event_type}[/cyan] {message}", message_group=group_id)
1695
-
1696
- except Exception as e:
1697
- logger.error(f"Error showing detailed status for server '{server_name}': {e}")
1698
- emit_info(f"Failed to get detailed status: {e}", message_group=group_id)
1699
-
1700
- def _handle_interactive_requirements(self, template, custom_name: str, group_id: str) -> Dict:
1701
- """Handle comprehensive requirements in interactive mode."""
1702
- from code_puppy.messaging import emit_prompt
1703
-
1704
- requirements = template.get_requirements()
1705
- config_overrides = {}
1706
-
1707
- # 1. Check system requirements
1708
- if requirements.required_tools:
1709
- emit_info("[bold cyan]Checking system requirements...[/bold cyan]", message_group=group_id)
1710
- from code_puppy.mcp.system_tools import detector
1711
-
1712
- tool_status = detector.detect_tools(requirements.required_tools)
1713
- missing_tools = []
1714
-
1715
- for tool_name, tool_info in tool_status.items():
1716
- if tool_info.available:
1717
- emit_info(f"✅ {tool_name} ({tool_info.version or 'found'})", message_group=group_id)
1718
- else:
1719
- emit_info(f"❌ {tool_name} - {tool_info.error}", message_group=group_id)
1720
- missing_tools.append(tool_name)
1721
-
1722
- if missing_tools:
1723
- emit_info(f"[red]Missing required tools: {', '.join(missing_tools)}[/red]", message_group=group_id)
1724
-
1725
- # Show installation suggestions
1726
- for tool in missing_tools:
1727
- suggestions = detector.get_installation_suggestions(tool)
1728
- emit_info(f"Install {tool}: {suggestions[0]}", message_group=group_id)
1729
-
1730
- proceed = emit_prompt("Continue installation anyway? (y/N): ")
1731
- if proceed.lower() not in ['y', 'yes']:
1732
- raise Exception("Installation cancelled due to missing requirements")
1733
-
1734
- # 2. Environment variables
1735
- env_vars = template.get_environment_vars()
1736
- if env_vars:
1737
- emit_info("[bold yellow]Environment Variables:[/bold yellow]", message_group=group_id)
1738
-
1739
- for var in env_vars:
1740
- import os
1741
- if var in os.environ:
1742
- emit_info(f"✅ {var} (already set)", message_group=group_id)
1743
- else:
1744
- try:
1745
- value = emit_prompt(f"Enter {var}: ")
1746
- if value.strip():
1747
- os.environ[var] = value.strip()
1748
- emit_info(f"[green]Set {var}[/green]", message_group=group_id)
1749
- else:
1750
- emit_info(f"[yellow]Skipped {var} (empty value)[/yellow]", message_group=group_id)
1751
- except Exception as e:
1752
- emit_info(f"[yellow]Failed to get {var}: {e}[/yellow]", message_group=group_id)
1753
-
1754
- # 3. Command line arguments
1755
- cmd_args = requirements.command_line_args
1756
- if cmd_args:
1757
- emit_info("[bold green]Command Line Arguments:[/bold green]", message_group=group_id)
1758
-
1759
- for arg_config in cmd_args:
1760
- name = arg_config.get("name", "")
1761
- prompt_text = arg_config.get("prompt", name)
1762
- default = arg_config.get("default", "")
1763
- required = arg_config.get("required", True)
1764
-
1765
- try:
1766
- if default:
1767
- value = emit_prompt(f"{prompt_text} (default: {default}): ")
1768
- value = value.strip() or default
1769
- else:
1770
- value = emit_prompt(f"{prompt_text}: ")
1771
- value = value.strip()
1772
-
1773
- if value:
1774
- config_overrides[name] = value
1775
- emit_info(f"[green]Set {name}={value}[/green]", message_group=group_id)
1776
- elif required:
1777
- emit_info(f"[yellow]Required argument {name} not provided[/yellow]", message_group=group_id)
1778
-
1779
- except Exception as e:
1780
- emit_info(f"[yellow]Failed to get {name}: {e}[/yellow]", message_group=group_id)
1781
-
1782
- # 4. Package dependencies (informational)
1783
- packages = requirements.package_dependencies
1784
- if packages:
1785
- emit_info("[bold magenta]Package Dependencies:[/bold magenta]", message_group=group_id)
1786
- emit_info(f"This server requires: {', '.join(packages)}", message_group=group_id)
1787
- emit_info("These will be installed automatically when the server starts.", message_group=group_id)
1788
-
1789
- return config_overrides