code-puppy 0.0.134__py3-none-any.whl → 0.0.136__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/agent.py +15 -17
- code_puppy/agents/agent_manager.py +320 -9
- code_puppy/agents/base_agent.py +58 -2
- code_puppy/agents/runtime_manager.py +68 -42
- code_puppy/command_line/command_handler.py +82 -33
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/add_command.py +183 -0
- code_puppy/command_line/mcp/base.py +35 -0
- code_puppy/command_line/mcp/handler.py +133 -0
- code_puppy/command_line/mcp/help_command.py +146 -0
- code_puppy/command_line/mcp/install_command.py +176 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +126 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +92 -0
- code_puppy/command_line/mcp/search_command.py +117 -0
- code_puppy/command_line/mcp/start_all_command.py +126 -0
- code_puppy/command_line/mcp/start_command.py +98 -0
- code_puppy/command_line/mcp/status_command.py +185 -0
- code_puppy/command_line/mcp/stop_all_command.py +109 -0
- code_puppy/command_line/mcp/stop_command.py +79 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +259 -0
- code_puppy/command_line/model_picker_completion.py +21 -4
- code_puppy/command_line/prompt_toolkit_completion.py +9 -0
- code_puppy/main.py +23 -17
- code_puppy/mcp/__init__.py +42 -16
- code_puppy/mcp/async_lifecycle.py +51 -49
- code_puppy/mcp/blocking_startup.py +125 -113
- code_puppy/mcp/captured_stdio_server.py +63 -70
- code_puppy/mcp/circuit_breaker.py +63 -47
- code_puppy/mcp/config_wizard.py +169 -136
- code_puppy/mcp/dashboard.py +79 -71
- code_puppy/mcp/error_isolation.py +147 -100
- code_puppy/mcp/examples/retry_example.py +55 -42
- code_puppy/mcp/health_monitor.py +152 -141
- code_puppy/mcp/managed_server.py +100 -97
- code_puppy/mcp/manager.py +168 -156
- code_puppy/mcp/registry.py +148 -110
- code_puppy/mcp/retry_manager.py +63 -61
- code_puppy/mcp/server_registry_catalog.py +271 -225
- code_puppy/mcp/status_tracker.py +80 -80
- code_puppy/mcp/system_tools.py +47 -52
- code_puppy/messaging/message_queue.py +20 -13
- code_puppy/messaging/renderers.py +30 -15
- code_puppy/state_management.py +103 -0
- code_puppy/tui/app.py +64 -7
- code_puppy/tui/components/chat_view.py +3 -3
- code_puppy/tui/components/human_input_modal.py +12 -8
- code_puppy/tui/screens/__init__.py +2 -2
- code_puppy/tui/screens/mcp_install_wizard.py +208 -179
- code_puppy/tui/tests/test_agent_command.py +3 -3
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
- code_puppy/command_line/mcp_commands.py +0 -1789
- {code_puppy-0.0.134.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.134.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
|