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.
- 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 -93
- 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.135.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
- {code_puppy-0.0.135.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.135.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
code_puppy/mcp/config_wizard.py
CHANGED
|
@@ -1,40 +1,52 @@
|
|
|
1
1
|
"""
|
|
2
2
|
MCP Configuration Wizard - Interactive setup for MCP servers.
|
|
3
|
+
|
|
4
|
+
Note: This module imports ServerConfig and get_mcp_manager directly from
|
|
5
|
+
.code_puppy.mcp.manager to avoid circular imports with the package __init__.py
|
|
3
6
|
"""
|
|
4
7
|
|
|
5
8
|
import re
|
|
6
9
|
from typing import Dict, Optional
|
|
7
10
|
from urllib.parse import urlparse
|
|
8
11
|
|
|
9
|
-
from code_puppy.mcp import ServerConfig, get_mcp_manager
|
|
10
|
-
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning, emit_prompt
|
|
11
12
|
from rich.console import Console
|
|
12
13
|
|
|
14
|
+
from code_puppy.mcp.manager import ServerConfig, get_mcp_manager
|
|
15
|
+
from code_puppy.messaging import (
|
|
16
|
+
emit_error,
|
|
17
|
+
emit_info,
|
|
18
|
+
emit_prompt,
|
|
19
|
+
emit_success,
|
|
20
|
+
emit_warning,
|
|
21
|
+
)
|
|
22
|
+
|
|
13
23
|
console = Console()
|
|
14
24
|
|
|
15
25
|
|
|
16
|
-
def prompt_ask(
|
|
26
|
+
def prompt_ask(
|
|
27
|
+
prompt_text: str, default: Optional[str] = None, choices: Optional[list] = None
|
|
28
|
+
) -> Optional[str]:
|
|
17
29
|
"""Helper function to replace rich.prompt.Prompt.ask with emit_prompt."""
|
|
18
30
|
try:
|
|
19
31
|
if default:
|
|
20
32
|
full_prompt = f"{prompt_text} [{default}]"
|
|
21
33
|
else:
|
|
22
34
|
full_prompt = prompt_text
|
|
23
|
-
|
|
35
|
+
|
|
24
36
|
if choices:
|
|
25
37
|
full_prompt += f" ({'/'.join(choices)})"
|
|
26
|
-
|
|
38
|
+
|
|
27
39
|
response = emit_prompt(full_prompt + ": ")
|
|
28
|
-
|
|
40
|
+
|
|
29
41
|
# Handle default value
|
|
30
42
|
if not response.strip() and default:
|
|
31
43
|
return default
|
|
32
|
-
|
|
44
|
+
|
|
33
45
|
# Handle choices validation
|
|
34
46
|
if choices and response.strip() and response.strip() not in choices:
|
|
35
47
|
emit_error(f"Invalid choice. Must be one of: {', '.join(choices)}")
|
|
36
48
|
return None
|
|
37
|
-
|
|
49
|
+
|
|
38
50
|
return response.strip() if response.strip() else None
|
|
39
51
|
except Exception as e:
|
|
40
52
|
emit_error(f"Input error: {e}")
|
|
@@ -46,14 +58,14 @@ def confirm_ask(prompt_text: str, default: bool = True) -> bool:
|
|
|
46
58
|
try:
|
|
47
59
|
default_text = "[Y/n]" if default else "[y/N]"
|
|
48
60
|
response = emit_prompt(f"{prompt_text} {default_text}: ")
|
|
49
|
-
|
|
61
|
+
|
|
50
62
|
if not response.strip():
|
|
51
63
|
return default
|
|
52
|
-
|
|
64
|
+
|
|
53
65
|
response_lower = response.strip().lower()
|
|
54
|
-
if response_lower in [
|
|
66
|
+
if response_lower in ["y", "yes", "true", "1"]:
|
|
55
67
|
return True
|
|
56
|
-
elif response_lower in [
|
|
68
|
+
elif response_lower in ["n", "no", "false", "0"]:
|
|
57
69
|
return False
|
|
58
70
|
else:
|
|
59
71
|
return default
|
|
@@ -64,36 +76,37 @@ def confirm_ask(prompt_text: str, default: bool = True) -> bool:
|
|
|
64
76
|
|
|
65
77
|
class MCPConfigWizard:
|
|
66
78
|
"""Interactive wizard for configuring MCP servers."""
|
|
67
|
-
|
|
79
|
+
|
|
68
80
|
def __init__(self):
|
|
69
81
|
self.manager = get_mcp_manager()
|
|
70
|
-
|
|
82
|
+
|
|
71
83
|
def run_wizard(self, group_id: str = None) -> Optional[ServerConfig]:
|
|
72
84
|
"""
|
|
73
85
|
Run the interactive configuration wizard.
|
|
74
|
-
|
|
86
|
+
|
|
75
87
|
Args:
|
|
76
88
|
group_id: Optional message group ID for grouping related messages
|
|
77
|
-
|
|
89
|
+
|
|
78
90
|
Returns:
|
|
79
91
|
ServerConfig if successful, None if cancelled
|
|
80
92
|
"""
|
|
81
93
|
if group_id is None:
|
|
82
94
|
import uuid
|
|
95
|
+
|
|
83
96
|
group_id = str(uuid.uuid4())
|
|
84
|
-
|
|
97
|
+
|
|
85
98
|
emit_info("🧙 MCP Server Configuration Wizard", message_group=group_id)
|
|
86
|
-
|
|
99
|
+
|
|
87
100
|
# Step 1: Server name
|
|
88
101
|
name = self.prompt_server_name(group_id)
|
|
89
102
|
if not name:
|
|
90
103
|
return None
|
|
91
|
-
|
|
104
|
+
|
|
92
105
|
# Step 2: Server type
|
|
93
106
|
server_type = self.prompt_server_type(group_id)
|
|
94
107
|
if not server_type:
|
|
95
108
|
return None
|
|
96
|
-
|
|
109
|
+
|
|
97
110
|
# Step 3: Type-specific configuration
|
|
98
111
|
config = {}
|
|
99
112
|
if server_type == "sse":
|
|
@@ -102,268 +115,273 @@ class MCPConfigWizard:
|
|
|
102
115
|
config = self.prompt_http_config(group_id)
|
|
103
116
|
elif server_type == "stdio":
|
|
104
117
|
config = self.prompt_stdio_config(group_id)
|
|
105
|
-
|
|
118
|
+
|
|
106
119
|
if not config:
|
|
107
120
|
return None
|
|
108
|
-
|
|
121
|
+
|
|
109
122
|
# Step 4: Create ServerConfig
|
|
110
123
|
server_config = ServerConfig(
|
|
111
124
|
id=f"{name}_{hash(name)}",
|
|
112
125
|
name=name,
|
|
113
126
|
type=server_type,
|
|
114
127
|
enabled=True,
|
|
115
|
-
config=config
|
|
128
|
+
config=config,
|
|
116
129
|
)
|
|
117
|
-
|
|
130
|
+
|
|
118
131
|
# Step 5: Show summary and confirm
|
|
119
132
|
if self.prompt_confirmation(server_config, group_id):
|
|
120
133
|
return server_config
|
|
121
|
-
|
|
134
|
+
|
|
122
135
|
return None
|
|
123
|
-
|
|
136
|
+
|
|
124
137
|
def prompt_server_name(self, group_id: str = None) -> Optional[str]:
|
|
125
138
|
"""Prompt for server name with validation."""
|
|
126
139
|
while True:
|
|
127
140
|
name = prompt_ask("Enter server name", default=None)
|
|
128
|
-
|
|
141
|
+
|
|
129
142
|
if not name:
|
|
130
143
|
if not confirm_ask("Cancel configuration?", default=False):
|
|
131
144
|
continue
|
|
132
145
|
return None
|
|
133
|
-
|
|
146
|
+
|
|
134
147
|
# Validate name
|
|
135
148
|
if not self.validate_name(name):
|
|
136
|
-
emit_error(
|
|
149
|
+
emit_error(
|
|
150
|
+
"Name must be alphanumeric with hyphens/underscores only",
|
|
151
|
+
message_group=group_id,
|
|
152
|
+
)
|
|
137
153
|
continue
|
|
138
|
-
|
|
154
|
+
|
|
139
155
|
# Check uniqueness
|
|
140
156
|
existing = self.manager.registry.get_by_name(name)
|
|
141
157
|
if existing:
|
|
142
158
|
emit_error(f"Server '{name}' already exists", message_group=group_id)
|
|
143
159
|
continue
|
|
144
|
-
|
|
160
|
+
|
|
145
161
|
return name
|
|
146
|
-
|
|
162
|
+
|
|
147
163
|
def prompt_server_type(self, group_id: str = None) -> Optional[str]:
|
|
148
164
|
"""Prompt for server type."""
|
|
149
165
|
emit_info("\nServer types:", message_group=group_id)
|
|
150
|
-
emit_info(
|
|
151
|
-
|
|
166
|
+
emit_info(
|
|
167
|
+
" sse - Server-Sent Events (HTTP streaming)", message_group=group_id
|
|
168
|
+
)
|
|
169
|
+
emit_info(" http - HTTP/REST API", message_group=group_id)
|
|
152
170
|
emit_info(" stdio - Local command (subprocess)", message_group=group_id)
|
|
153
|
-
|
|
171
|
+
|
|
154
172
|
while True:
|
|
155
|
-
server_type = prompt_ask(
|
|
156
|
-
|
|
173
|
+
server_type = prompt_ask(
|
|
174
|
+
"Select server type", choices=["sse", "http", "stdio"], default="stdio"
|
|
175
|
+
)
|
|
176
|
+
|
|
157
177
|
if server_type in ["sse", "http", "stdio"]:
|
|
158
178
|
return server_type
|
|
159
|
-
|
|
160
|
-
emit_error(
|
|
161
|
-
|
|
179
|
+
|
|
180
|
+
emit_error(
|
|
181
|
+
"Invalid type. Choose: sse, http, or stdio", message_group=group_id
|
|
182
|
+
)
|
|
183
|
+
|
|
162
184
|
def prompt_sse_config(self, group_id: str = None) -> Optional[Dict]:
|
|
163
185
|
"""Prompt for SSE server configuration."""
|
|
164
186
|
emit_info("Configuring SSE server", message_group=group_id)
|
|
165
|
-
|
|
187
|
+
|
|
166
188
|
# URL
|
|
167
189
|
url = self.prompt_url("SSE", group_id)
|
|
168
190
|
if not url:
|
|
169
191
|
return None
|
|
170
|
-
|
|
171
|
-
config = {
|
|
172
|
-
|
|
173
|
-
"url": url,
|
|
174
|
-
"timeout": 30
|
|
175
|
-
}
|
|
176
|
-
|
|
192
|
+
|
|
193
|
+
config = {"type": "sse", "url": url, "timeout": 30}
|
|
194
|
+
|
|
177
195
|
# Headers (optional)
|
|
178
196
|
if confirm_ask("Add custom headers?", default=False):
|
|
179
197
|
headers = self.prompt_headers(group_id)
|
|
180
198
|
if headers:
|
|
181
199
|
config["headers"] = headers
|
|
182
|
-
|
|
200
|
+
|
|
183
201
|
# Timeout
|
|
184
202
|
timeout_str = prompt_ask("Connection timeout (seconds)", default="30")
|
|
185
203
|
try:
|
|
186
204
|
config["timeout"] = int(timeout_str)
|
|
187
205
|
except ValueError:
|
|
188
206
|
config["timeout"] = 30
|
|
189
|
-
|
|
207
|
+
|
|
190
208
|
return config
|
|
191
|
-
|
|
209
|
+
|
|
192
210
|
def prompt_http_config(self, group_id: str = None) -> Optional[Dict]:
|
|
193
211
|
"""Prompt for HTTP server configuration."""
|
|
194
212
|
emit_info("Configuring HTTP server", message_group=group_id)
|
|
195
|
-
|
|
213
|
+
|
|
196
214
|
# URL
|
|
197
215
|
url = self.prompt_url("HTTP", group_id)
|
|
198
216
|
if not url:
|
|
199
217
|
return None
|
|
200
|
-
|
|
201
|
-
config = {
|
|
202
|
-
|
|
203
|
-
"url": url,
|
|
204
|
-
"timeout": 30
|
|
205
|
-
}
|
|
206
|
-
|
|
218
|
+
|
|
219
|
+
config = {"type": "http", "url": url, "timeout": 30}
|
|
220
|
+
|
|
207
221
|
# Headers (optional)
|
|
208
222
|
if confirm_ask("Add custom headers?", default=False):
|
|
209
223
|
headers = self.prompt_headers(group_id)
|
|
210
224
|
if headers:
|
|
211
225
|
config["headers"] = headers
|
|
212
|
-
|
|
226
|
+
|
|
213
227
|
# Timeout
|
|
214
228
|
timeout_str = prompt_ask("Request timeout (seconds)", default="30")
|
|
215
229
|
try:
|
|
216
230
|
config["timeout"] = int(timeout_str)
|
|
217
231
|
except ValueError:
|
|
218
232
|
config["timeout"] = 30
|
|
219
|
-
|
|
233
|
+
|
|
220
234
|
return config
|
|
221
|
-
|
|
235
|
+
|
|
222
236
|
def prompt_stdio_config(self, group_id: str = None) -> Optional[Dict]:
|
|
223
237
|
"""Prompt for Stdio server configuration."""
|
|
224
238
|
emit_info("Configuring Stdio server", message_group=group_id)
|
|
225
239
|
emit_info("Examples:", message_group=group_id)
|
|
226
|
-
emit_info(
|
|
227
|
-
|
|
240
|
+
emit_info(
|
|
241
|
+
" • npx -y @modelcontextprotocol/server-filesystem /path",
|
|
242
|
+
message_group=group_id,
|
|
243
|
+
)
|
|
244
|
+
emit_info(" • python mcp_server.py", message_group=group_id)
|
|
228
245
|
emit_info(" • node server.js", message_group=group_id)
|
|
229
|
-
|
|
246
|
+
|
|
230
247
|
# Command
|
|
231
248
|
command = prompt_ask("Enter command", default=None)
|
|
232
|
-
|
|
249
|
+
|
|
233
250
|
if not command:
|
|
234
251
|
return None
|
|
235
|
-
|
|
236
|
-
config = {
|
|
237
|
-
|
|
238
|
-
"command": command,
|
|
239
|
-
"args": [],
|
|
240
|
-
"timeout": 30
|
|
241
|
-
}
|
|
242
|
-
|
|
252
|
+
|
|
253
|
+
config = {"type": "stdio", "command": command, "args": [], "timeout": 30}
|
|
254
|
+
|
|
243
255
|
# Arguments
|
|
244
256
|
args_str = prompt_ask("Enter arguments (space-separated)", default="")
|
|
245
257
|
if args_str:
|
|
246
258
|
# Simple argument parsing (handles quoted strings)
|
|
247
259
|
import shlex
|
|
260
|
+
|
|
248
261
|
try:
|
|
249
262
|
config["args"] = shlex.split(args_str)
|
|
250
263
|
except ValueError:
|
|
251
264
|
config["args"] = args_str.split()
|
|
252
|
-
|
|
265
|
+
|
|
253
266
|
# Working directory (optional)
|
|
254
267
|
cwd = prompt_ask("Working directory (optional)", default="")
|
|
255
268
|
if cwd:
|
|
256
269
|
import os
|
|
270
|
+
|
|
257
271
|
if os.path.isdir(os.path.expanduser(cwd)):
|
|
258
272
|
config["cwd"] = os.path.expanduser(cwd)
|
|
259
273
|
else:
|
|
260
|
-
emit_warning(
|
|
261
|
-
|
|
274
|
+
emit_warning(
|
|
275
|
+
f"Directory '{cwd}' not found, ignoring", message_group=group_id
|
|
276
|
+
)
|
|
277
|
+
|
|
262
278
|
# Environment variables (optional)
|
|
263
279
|
if confirm_ask("Add environment variables?", default=False):
|
|
264
280
|
env = self.prompt_env_vars(group_id)
|
|
265
281
|
if env:
|
|
266
282
|
config["env"] = env
|
|
267
|
-
|
|
283
|
+
|
|
268
284
|
# Timeout
|
|
269
285
|
timeout_str = prompt_ask("Startup timeout (seconds)", default="30")
|
|
270
286
|
try:
|
|
271
287
|
config["timeout"] = int(timeout_str)
|
|
272
288
|
except ValueError:
|
|
273
289
|
config["timeout"] = 30
|
|
274
|
-
|
|
290
|
+
|
|
275
291
|
return config
|
|
276
|
-
|
|
292
|
+
|
|
277
293
|
def prompt_url(self, server_type: str, group_id: str = None) -> Optional[str]:
|
|
278
294
|
"""Prompt for and validate URL."""
|
|
279
295
|
while True:
|
|
280
296
|
url = prompt_ask(f"Enter {server_type} server URL", default=None)
|
|
281
|
-
|
|
297
|
+
|
|
282
298
|
if not url:
|
|
283
299
|
if confirm_ask("Cancel configuration?", default=False):
|
|
284
300
|
return None
|
|
285
301
|
continue
|
|
286
|
-
|
|
302
|
+
|
|
287
303
|
if self.validate_url(url):
|
|
288
304
|
return url
|
|
289
|
-
|
|
290
|
-
emit_error(
|
|
291
|
-
|
|
305
|
+
|
|
306
|
+
emit_error(
|
|
307
|
+
"Invalid URL. Must be http:// or https://", message_group=group_id
|
|
308
|
+
)
|
|
309
|
+
|
|
292
310
|
def prompt_headers(self, group_id: str = None) -> Dict[str, str]:
|
|
293
311
|
"""Prompt for HTTP headers."""
|
|
294
312
|
headers = {}
|
|
295
313
|
emit_info("Enter headers (format: Name: Value)", message_group=group_id)
|
|
296
314
|
emit_info("Press Enter with empty name to finish", message_group=group_id)
|
|
297
|
-
|
|
315
|
+
|
|
298
316
|
while True:
|
|
299
317
|
name = prompt_ask("Header name", default="")
|
|
300
318
|
if not name:
|
|
301
319
|
break
|
|
302
|
-
|
|
320
|
+
|
|
303
321
|
value = prompt_ask(f"Value for '{name}'", default="")
|
|
304
322
|
headers[name] = value
|
|
305
|
-
|
|
323
|
+
|
|
306
324
|
if not confirm_ask("Add another header?", default=True):
|
|
307
325
|
break
|
|
308
|
-
|
|
326
|
+
|
|
309
327
|
return headers
|
|
310
|
-
|
|
328
|
+
|
|
311
329
|
def prompt_env_vars(self, group_id: str = None) -> Dict[str, str]:
|
|
312
330
|
"""Prompt for environment variables."""
|
|
313
331
|
env = {}
|
|
314
332
|
emit_info("Enter environment variables", message_group=group_id)
|
|
315
333
|
emit_info("Press Enter with empty name to finish", message_group=group_id)
|
|
316
|
-
|
|
334
|
+
|
|
317
335
|
while True:
|
|
318
336
|
name = prompt_ask("Variable name", default="")
|
|
319
337
|
if not name:
|
|
320
338
|
break
|
|
321
|
-
|
|
339
|
+
|
|
322
340
|
value = prompt_ask(f"Value for '{name}'", default="")
|
|
323
341
|
env[name] = value
|
|
324
|
-
|
|
342
|
+
|
|
325
343
|
if not confirm_ask("Add another variable?", default=True):
|
|
326
344
|
break
|
|
327
|
-
|
|
345
|
+
|
|
328
346
|
return env
|
|
329
|
-
|
|
347
|
+
|
|
330
348
|
def validate_name(self, name: str) -> bool:
|
|
331
349
|
"""Validate server name."""
|
|
332
350
|
# Allow alphanumeric, hyphens, and underscores
|
|
333
|
-
return bool(re.match(r
|
|
334
|
-
|
|
351
|
+
return bool(re.match(r"^[a-zA-Z0-9_-]+$", name))
|
|
352
|
+
|
|
335
353
|
def validate_url(self, url: str) -> bool:
|
|
336
354
|
"""Validate URL format."""
|
|
337
355
|
try:
|
|
338
356
|
result = urlparse(url)
|
|
339
|
-
return result.scheme in (
|
|
357
|
+
return result.scheme in ("http", "https") and bool(result.netloc)
|
|
340
358
|
except Exception:
|
|
341
359
|
return False
|
|
342
|
-
|
|
360
|
+
|
|
343
361
|
def validate_command(self, command: str) -> bool:
|
|
344
362
|
"""Check if command exists (basic check)."""
|
|
345
|
-
import shutil
|
|
346
363
|
import os
|
|
347
|
-
|
|
364
|
+
import shutil
|
|
365
|
+
|
|
348
366
|
# If it's a path, check if file exists
|
|
349
|
-
if
|
|
367
|
+
if "/" in command or "\\" in command:
|
|
350
368
|
return os.path.isfile(command)
|
|
351
|
-
|
|
369
|
+
|
|
352
370
|
# Otherwise check if it's in PATH
|
|
353
371
|
return shutil.which(command) is not None
|
|
354
|
-
|
|
372
|
+
|
|
355
373
|
def test_connection(self, config: ServerConfig, group_id: str = None) -> bool:
|
|
356
374
|
"""
|
|
357
375
|
Test connection to the configured server.
|
|
358
|
-
|
|
376
|
+
|
|
359
377
|
Args:
|
|
360
378
|
config: Server configuration to test
|
|
361
|
-
|
|
379
|
+
|
|
362
380
|
Returns:
|
|
363
381
|
True if connection successful, False otherwise
|
|
364
382
|
"""
|
|
365
383
|
emit_info("Testing connection...", message_group=group_id)
|
|
366
|
-
|
|
384
|
+
|
|
367
385
|
try:
|
|
368
386
|
# Try to create the server instance
|
|
369
387
|
managed = self.manager.get_server(config.id)
|
|
@@ -371,101 +389,116 @@ class MCPConfigWizard:
|
|
|
371
389
|
# Temporarily register to test
|
|
372
390
|
self.manager.register_server(config)
|
|
373
391
|
managed = self.manager.get_server(config.id)
|
|
374
|
-
|
|
392
|
+
|
|
375
393
|
if managed:
|
|
376
394
|
# Try to get the pydantic server (this validates config)
|
|
377
395
|
server = managed.get_pydantic_server()
|
|
378
396
|
if server:
|
|
379
397
|
emit_success("✓ Configuration valid", message_group=group_id)
|
|
380
398
|
return True
|
|
381
|
-
|
|
399
|
+
|
|
382
400
|
emit_error("✗ Failed to create server instance", message_group=group_id)
|
|
383
401
|
return False
|
|
384
|
-
|
|
402
|
+
|
|
385
403
|
except Exception as e:
|
|
386
404
|
emit_error(f"✗ Configuration error: {e}", message_group=group_id)
|
|
387
405
|
return False
|
|
388
|
-
|
|
406
|
+
|
|
389
407
|
def prompt_confirmation(self, config: ServerConfig, group_id: str = None) -> bool:
|
|
390
408
|
"""Show summary and ask for confirmation."""
|
|
391
409
|
emit_info("Configuration Summary:", message_group=group_id)
|
|
392
410
|
emit_info(f" Name: {config.name}", message_group=group_id)
|
|
393
411
|
emit_info(f" Type: {config.type}", message_group=group_id)
|
|
394
|
-
|
|
412
|
+
|
|
395
413
|
if config.type in ["sse", "http"]:
|
|
396
414
|
emit_info(f" URL: {config.config.get('url')}", message_group=group_id)
|
|
397
415
|
elif config.type == "stdio":
|
|
398
|
-
emit_info(
|
|
399
|
-
|
|
416
|
+
emit_info(
|
|
417
|
+
f" Command: {config.config.get('command')}", message_group=group_id
|
|
418
|
+
)
|
|
419
|
+
args = config.config.get("args", [])
|
|
400
420
|
if args:
|
|
401
421
|
emit_info(f" Arguments: {' '.join(args)}", message_group=group_id)
|
|
402
|
-
|
|
403
|
-
emit_info(
|
|
404
|
-
|
|
422
|
+
|
|
423
|
+
emit_info(
|
|
424
|
+
f" Timeout: {config.config.get('timeout', 30)}s", message_group=group_id
|
|
425
|
+
)
|
|
426
|
+
|
|
405
427
|
# Test connection if requested
|
|
406
428
|
if confirm_ask("Test connection?", default=True):
|
|
407
429
|
if not self.test_connection(config, group_id):
|
|
408
430
|
if not confirm_ask("Continue anyway?", default=False):
|
|
409
431
|
return False
|
|
410
|
-
|
|
432
|
+
|
|
411
433
|
return confirm_ask("Save this configuration?", default=True)
|
|
412
434
|
|
|
413
435
|
|
|
414
436
|
def run_add_wizard(group_id: str = None) -> bool:
|
|
415
437
|
"""
|
|
416
438
|
Run the MCP add wizard and register the server.
|
|
417
|
-
|
|
439
|
+
|
|
418
440
|
Args:
|
|
419
441
|
group_id: Optional message group ID for grouping related messages
|
|
420
|
-
|
|
442
|
+
|
|
421
443
|
Returns:
|
|
422
444
|
True if server was added, False otherwise
|
|
423
445
|
"""
|
|
424
446
|
if group_id is None:
|
|
425
447
|
import uuid
|
|
448
|
+
|
|
426
449
|
group_id = str(uuid.uuid4())
|
|
427
|
-
|
|
450
|
+
|
|
428
451
|
wizard = MCPConfigWizard()
|
|
429
452
|
config = wizard.run_wizard(group_id)
|
|
430
|
-
|
|
453
|
+
|
|
431
454
|
if config:
|
|
432
455
|
try:
|
|
433
456
|
manager = get_mcp_manager()
|
|
434
457
|
server_id = manager.register_server(config)
|
|
435
|
-
|
|
436
|
-
emit_success(
|
|
458
|
+
|
|
459
|
+
emit_success(
|
|
460
|
+
f"\n✅ Server '{config.name}' added successfully!",
|
|
461
|
+
message_group=group_id,
|
|
462
|
+
)
|
|
437
463
|
emit_info(f"Server ID: {server_id}", message_group=group_id)
|
|
438
464
|
emit_info("Use '/mcp list' to see all servers", message_group=group_id)
|
|
439
|
-
emit_info(
|
|
440
|
-
|
|
465
|
+
emit_info(
|
|
466
|
+
f"Use '/mcp start {config.name}' to start the server",
|
|
467
|
+
message_group=group_id,
|
|
468
|
+
)
|
|
469
|
+
|
|
441
470
|
# Also save to mcp_servers.json for persistence
|
|
442
|
-
from code_puppy.config import MCP_SERVERS_FILE, load_mcp_server_configs
|
|
443
471
|
import json
|
|
444
472
|
import os
|
|
445
|
-
|
|
473
|
+
|
|
474
|
+
from code_puppy.config import MCP_SERVERS_FILE
|
|
475
|
+
|
|
446
476
|
# Load existing configs
|
|
447
477
|
if os.path.exists(MCP_SERVERS_FILE):
|
|
448
|
-
with open(MCP_SERVERS_FILE,
|
|
478
|
+
with open(MCP_SERVERS_FILE, "r") as f:
|
|
449
479
|
data = json.load(f)
|
|
450
480
|
servers = data.get("mcp_servers", {})
|
|
451
481
|
else:
|
|
452
482
|
servers = {}
|
|
453
483
|
data = {"mcp_servers": servers}
|
|
454
|
-
|
|
484
|
+
|
|
455
485
|
# Add new server
|
|
456
486
|
servers[config.name] = config.config
|
|
457
|
-
|
|
487
|
+
|
|
458
488
|
# Save back
|
|
459
489
|
os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
|
|
460
|
-
with open(MCP_SERVERS_FILE,
|
|
490
|
+
with open(MCP_SERVERS_FILE, "w") as f:
|
|
461
491
|
json.dump(data, f, indent=2)
|
|
462
|
-
|
|
463
|
-
emit_info(
|
|
492
|
+
|
|
493
|
+
emit_info(
|
|
494
|
+
f"[dim]Configuration saved to {MCP_SERVERS_FILE}[/dim]",
|
|
495
|
+
message_group=group_id,
|
|
496
|
+
)
|
|
464
497
|
return True
|
|
465
|
-
|
|
498
|
+
|
|
466
499
|
except Exception as e:
|
|
467
500
|
emit_error(f"Failed to add server: {e}", message_group=group_id)
|
|
468
501
|
return False
|
|
469
502
|
else:
|
|
470
503
|
emit_warning("Configuration cancelled", message_group=group_id)
|
|
471
|
-
return False
|
|
504
|
+
return False
|