code-puppy 0.0.135__py3-none-any.whl → 0.0.137__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/config.py +5 -5
- 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.137.dist-info}/METADATA +1 -1
- {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/RECORD +60 -42
- code_puppy/command_line/mcp_commands.py +0 -1789
- {code_puppy-0.0.135.data → code_puppy-0.0.137.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,24 +4,12 @@ MCP Install Wizard Screen - TUI interface for installing MCP servers.
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
-
from typing import Dict, List, Optional
|
|
8
7
|
|
|
9
8
|
from textual import on
|
|
10
9
|
from textual.app import ComposeResult
|
|
11
|
-
from textual.containers import Container, Horizontal
|
|
10
|
+
from textual.containers import Container, Horizontal
|
|
12
11
|
from textual.screen import ModalScreen
|
|
13
|
-
from textual.widgets import
|
|
14
|
-
Button,
|
|
15
|
-
Input,
|
|
16
|
-
Label,
|
|
17
|
-
ListItem,
|
|
18
|
-
ListView,
|
|
19
|
-
Static,
|
|
20
|
-
Select,
|
|
21
|
-
TextArea
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
from code_puppy.messaging import emit_info
|
|
12
|
+
from textual.widgets import Button, Input, ListItem, ListView, Static, TextArea
|
|
25
13
|
|
|
26
14
|
|
|
27
15
|
class MCPInstallWizardScreen(ModalScreen):
|
|
@@ -140,7 +128,7 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
140
128
|
width: 2fr;
|
|
141
129
|
border: solid $primary;
|
|
142
130
|
}
|
|
143
|
-
|
|
131
|
+
|
|
144
132
|
#custom-json-container {
|
|
145
133
|
width: 100%;
|
|
146
134
|
height: 1fr;
|
|
@@ -148,7 +136,7 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
148
136
|
display: none;
|
|
149
137
|
padding: 1;
|
|
150
138
|
}
|
|
151
|
-
|
|
139
|
+
|
|
152
140
|
#custom-json-header {
|
|
153
141
|
width: 100%;
|
|
154
142
|
height: 2;
|
|
@@ -156,13 +144,13 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
156
144
|
color: $warning;
|
|
157
145
|
margin-bottom: 1;
|
|
158
146
|
}
|
|
159
|
-
|
|
147
|
+
|
|
160
148
|
#custom-name-input {
|
|
161
149
|
width: 100%;
|
|
162
150
|
margin-bottom: 1;
|
|
163
151
|
border: solid $primary;
|
|
164
152
|
}
|
|
165
|
-
|
|
153
|
+
|
|
166
154
|
#custom-json-input {
|
|
167
155
|
width: 100%;
|
|
168
156
|
height: 1fr;
|
|
@@ -170,7 +158,7 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
170
158
|
margin-bottom: 1;
|
|
171
159
|
background: $surface-darken-1;
|
|
172
160
|
}
|
|
173
|
-
|
|
161
|
+
|
|
174
162
|
#custom-json-button {
|
|
175
163
|
width: auto;
|
|
176
164
|
height: 3;
|
|
@@ -183,24 +171,30 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
183
171
|
"""Create the wizard layout."""
|
|
184
172
|
with Container(id="wizard-container"):
|
|
185
173
|
yield Static("🔌 MCP Server Install Wizard", id="wizard-header")
|
|
186
|
-
|
|
174
|
+
|
|
187
175
|
# Step 1: Search and select server
|
|
188
176
|
with Container(id="search-container"):
|
|
189
|
-
yield Input(
|
|
177
|
+
yield Input(
|
|
178
|
+
placeholder="Search MCP servers (e.g. 'github', 'postgres')...",
|
|
179
|
+
id="search-input",
|
|
180
|
+
)
|
|
190
181
|
yield ListView(id="results-list")
|
|
191
|
-
|
|
182
|
+
|
|
192
183
|
# Step 2: Configure server (hidden initially)
|
|
193
184
|
with Container(id="config-container"):
|
|
194
185
|
yield Static("Server Configuration", id="config-header")
|
|
195
186
|
yield Container(id="server-info")
|
|
196
187
|
yield Container(id="env-vars-container")
|
|
197
|
-
|
|
188
|
+
|
|
198
189
|
# Step 3: Custom JSON configuration (hidden initially)
|
|
199
190
|
with Container(id="custom-json-container"):
|
|
200
191
|
yield Static("📝 Custom JSON Configuration", id="custom-json-header")
|
|
201
|
-
yield Input(
|
|
192
|
+
yield Input(
|
|
193
|
+
placeholder="Server name (e.g. 'my-sqlite-db')",
|
|
194
|
+
id="custom-name-input",
|
|
195
|
+
)
|
|
202
196
|
yield TextArea(id="custom-json-input")
|
|
203
|
-
|
|
197
|
+
|
|
204
198
|
# Navigation buttons
|
|
205
199
|
with Horizontal(id="button-container"):
|
|
206
200
|
yield Button("Cancel", id="cancel-button", variant="default")
|
|
@@ -213,7 +207,7 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
213
207
|
"""Initialize the wizard."""
|
|
214
208
|
self._show_search_step()
|
|
215
209
|
self._load_popular_servers()
|
|
216
|
-
|
|
210
|
+
|
|
217
211
|
# Focus the search input
|
|
218
212
|
search_input = self.query_one("#search-input", Input)
|
|
219
213
|
search_input.focus()
|
|
@@ -225,7 +219,7 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
225
219
|
self.query_one("#search-container").display = True
|
|
226
220
|
self.query_one("#config-container").display = False
|
|
227
221
|
self.query_one("#custom-json-container").display = False
|
|
228
|
-
|
|
222
|
+
|
|
229
223
|
self.query_one("#back-button").display = False
|
|
230
224
|
self.query_one("#custom-json-button").display = True
|
|
231
225
|
self.query_one("#next-button").display = True
|
|
@@ -238,14 +232,14 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
238
232
|
self.query_one("#search-container").display = False
|
|
239
233
|
self.query_one("#config-container").display = True
|
|
240
234
|
self.query_one("#custom-json-container").display = False
|
|
241
|
-
|
|
235
|
+
|
|
242
236
|
self.query_one("#back-button").display = True
|
|
243
237
|
self.query_one("#custom-json-button").display = False
|
|
244
238
|
self.query_one("#next-button").display = False
|
|
245
239
|
self.query_one("#install-button").display = True
|
|
246
|
-
|
|
240
|
+
|
|
247
241
|
self._setup_server_config()
|
|
248
|
-
|
|
242
|
+
|
|
249
243
|
def _show_custom_json_step(self) -> None:
|
|
250
244
|
"""Show the custom JSON configuration step."""
|
|
251
245
|
self.step = "custom_json"
|
|
@@ -253,16 +247,16 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
253
247
|
self.query_one("#search-container").display = False
|
|
254
248
|
self.query_one("#config-container").display = False
|
|
255
249
|
self.query_one("#custom-json-container").display = True
|
|
256
|
-
|
|
250
|
+
|
|
257
251
|
self.query_one("#back-button").display = True
|
|
258
252
|
self.query_one("#custom-json-button").display = False
|
|
259
253
|
self.query_one("#next-button").display = False
|
|
260
254
|
self.query_one("#install-button").display = True
|
|
261
|
-
|
|
255
|
+
|
|
262
256
|
# Pre-populate with SQLite example
|
|
263
257
|
name_input = self.query_one("#custom-name-input", Input)
|
|
264
258
|
name_input.value = "my-sqlite-db"
|
|
265
|
-
|
|
259
|
+
|
|
266
260
|
json_input = self.query_one("#custom-json-input", TextArea)
|
|
267
261
|
json_input.text = """{
|
|
268
262
|
"type": "stdio",
|
|
@@ -270,7 +264,7 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
270
264
|
"args": ["-y", "@modelcontextprotocol/server-sqlite", "./database.db"],
|
|
271
265
|
"timeout": 30
|
|
272
266
|
}"""
|
|
273
|
-
|
|
267
|
+
|
|
274
268
|
# Focus the name input
|
|
275
269
|
name_input.focus()
|
|
276
270
|
|
|
@@ -278,65 +272,79 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
278
272
|
"""Load all available servers into the list."""
|
|
279
273
|
self.search_counter += 1
|
|
280
274
|
counter = self.search_counter
|
|
281
|
-
|
|
275
|
+
|
|
282
276
|
try:
|
|
283
277
|
from code_puppy.mcp.server_registry_catalog import catalog
|
|
278
|
+
|
|
284
279
|
# Load ALL servers instead of just popular ones
|
|
285
280
|
servers = catalog.servers
|
|
286
|
-
|
|
281
|
+
|
|
287
282
|
results_list = self.query_one("#results-list", ListView)
|
|
288
283
|
# Force clear by removing all children
|
|
289
284
|
results_list.remove_children()
|
|
290
|
-
|
|
285
|
+
|
|
291
286
|
if servers:
|
|
292
287
|
# Sort servers to show popular and verified first
|
|
293
|
-
sorted_servers = sorted(
|
|
294
|
-
|
|
288
|
+
sorted_servers = sorted(
|
|
289
|
+
servers,
|
|
290
|
+
key=lambda s: (not s.popular, not s.verified, s.display_name),
|
|
291
|
+
)
|
|
292
|
+
|
|
295
293
|
for i, server in enumerate(sorted_servers):
|
|
296
294
|
indicators = []
|
|
297
295
|
if server.verified:
|
|
298
296
|
indicators.append("✓")
|
|
299
297
|
if server.popular:
|
|
300
298
|
indicators.append("⭐")
|
|
301
|
-
|
|
299
|
+
|
|
302
300
|
display_name = f"{server.display_name} {''.join(indicators)}"
|
|
303
|
-
description =
|
|
304
|
-
|
|
301
|
+
description = (
|
|
302
|
+
server.description[:60] + "..."
|
|
303
|
+
if len(server.description) > 60
|
|
304
|
+
else server.description
|
|
305
|
+
)
|
|
306
|
+
|
|
305
307
|
item_text = f"{display_name}\n[dim]{description}[/dim]"
|
|
306
308
|
# Use counter to ensure globally unique IDs
|
|
307
309
|
item = ListItem(Static(item_text), id=f"item-{counter}-{i}")
|
|
308
310
|
item.server_data = server
|
|
309
311
|
results_list.append(item)
|
|
310
312
|
else:
|
|
311
|
-
no_servers_item = ListItem(
|
|
313
|
+
no_servers_item = ListItem(
|
|
314
|
+
Static("No servers found"), id=f"no-results-{counter}"
|
|
315
|
+
)
|
|
312
316
|
results_list.append(no_servers_item)
|
|
313
|
-
|
|
317
|
+
|
|
314
318
|
except ImportError:
|
|
315
319
|
results_list = self.query_one("#results-list", ListView)
|
|
316
320
|
results_list.remove_children()
|
|
317
|
-
error_item = ListItem(
|
|
321
|
+
error_item = ListItem(
|
|
322
|
+
Static("[red]Server registry not available[/red]"),
|
|
323
|
+
id=f"error-{counter}",
|
|
324
|
+
)
|
|
318
325
|
results_list.append(error_item)
|
|
319
326
|
|
|
320
327
|
@on(Input.Changed, "#search-input")
|
|
321
328
|
def on_search_changed(self, event: Input.Changed) -> None:
|
|
322
329
|
"""Handle search input changes."""
|
|
323
330
|
query = event.value.strip()
|
|
324
|
-
|
|
331
|
+
|
|
325
332
|
if not query:
|
|
326
333
|
self._load_popular_servers() # This now loads all servers
|
|
327
334
|
return
|
|
328
|
-
|
|
335
|
+
|
|
329
336
|
self.search_counter += 1
|
|
330
337
|
counter = self.search_counter
|
|
331
|
-
|
|
338
|
+
|
|
332
339
|
try:
|
|
333
340
|
from code_puppy.mcp.server_registry_catalog import catalog
|
|
341
|
+
|
|
334
342
|
servers = catalog.search(query)
|
|
335
|
-
|
|
343
|
+
|
|
336
344
|
results_list = self.query_one("#results-list", ListView)
|
|
337
345
|
# Force clear by removing all children
|
|
338
346
|
results_list.remove_children()
|
|
339
|
-
|
|
347
|
+
|
|
340
348
|
if servers:
|
|
341
349
|
for i, server in enumerate(servers[:15]): # Limit results
|
|
342
350
|
indicators = []
|
|
@@ -344,29 +352,39 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
344
352
|
indicators.append("✓")
|
|
345
353
|
if server.popular:
|
|
346
354
|
indicators.append("⭐")
|
|
347
|
-
|
|
355
|
+
|
|
348
356
|
display_name = f"{server.display_name} {''.join(indicators)}"
|
|
349
|
-
description =
|
|
350
|
-
|
|
357
|
+
description = (
|
|
358
|
+
server.description[:60] + "..."
|
|
359
|
+
if len(server.description) > 60
|
|
360
|
+
else server.description
|
|
361
|
+
)
|
|
362
|
+
|
|
351
363
|
item_text = f"{display_name}\n[dim]{description}[/dim]"
|
|
352
364
|
# Use counter to ensure globally unique IDs
|
|
353
365
|
item = ListItem(Static(item_text), id=f"item-{counter}-{i}")
|
|
354
366
|
item.server_data = server
|
|
355
367
|
results_list.append(item)
|
|
356
368
|
else:
|
|
357
|
-
no_results_item = ListItem(
|
|
369
|
+
no_results_item = ListItem(
|
|
370
|
+
Static(f"No servers found for '{query}'"),
|
|
371
|
+
id=f"no-results-{counter}",
|
|
372
|
+
)
|
|
358
373
|
results_list.append(no_results_item)
|
|
359
|
-
|
|
374
|
+
|
|
360
375
|
except ImportError:
|
|
361
376
|
results_list = self.query_one("#results-list", ListView)
|
|
362
377
|
results_list.remove_children()
|
|
363
|
-
error_item = ListItem(
|
|
378
|
+
error_item = ListItem(
|
|
379
|
+
Static("[red]Server registry not available[/red]"),
|
|
380
|
+
id=f"error-{counter}",
|
|
381
|
+
)
|
|
364
382
|
results_list.append(error_item)
|
|
365
383
|
|
|
366
384
|
@on(ListView.Selected, "#results-list")
|
|
367
385
|
def on_server_selected(self, event: ListView.Selected) -> None:
|
|
368
386
|
"""Handle server selection."""
|
|
369
|
-
if hasattr(event.item,
|
|
387
|
+
if hasattr(event.item, "server_data"):
|
|
370
388
|
self.selected_server = event.item.server_data
|
|
371
389
|
|
|
372
390
|
@on(Button.Pressed, "#next-button")
|
|
@@ -391,7 +409,7 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
391
409
|
def on_custom_json_clicked(self) -> None:
|
|
392
410
|
"""Handle custom JSON button click."""
|
|
393
411
|
self._show_custom_json_step()
|
|
394
|
-
|
|
412
|
+
|
|
395
413
|
@on(Button.Pressed, "#install-button")
|
|
396
414
|
def on_install_clicked(self) -> None:
|
|
397
415
|
"""Handle install button click."""
|
|
@@ -409,17 +427,17 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
409
427
|
"""Setup the server configuration step."""
|
|
410
428
|
if not self.selected_server:
|
|
411
429
|
return
|
|
412
|
-
|
|
430
|
+
|
|
413
431
|
# Show server info
|
|
414
432
|
server_info = self.query_one("#server-info", Container)
|
|
415
433
|
server_info.remove_children()
|
|
416
|
-
|
|
434
|
+
|
|
417
435
|
info_text = f"""[bold]{self.selected_server.display_name}[/bold]
|
|
418
436
|
{self.selected_server.description}
|
|
419
437
|
|
|
420
438
|
[yellow]Category:[/yellow] {self.selected_server.category}
|
|
421
|
-
[yellow]Type:[/yellow] {getattr(self.selected_server,
|
|
422
|
-
|
|
439
|
+
[yellow]Type:[/yellow] {getattr(self.selected_server, "type", "stdio")}"""
|
|
440
|
+
|
|
423
441
|
# Show requirements summary
|
|
424
442
|
requirements = self.selected_server.get_requirements()
|
|
425
443
|
req_items = []
|
|
@@ -429,17 +447,17 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
429
447
|
req_items.append(f"Env vars: {len(requirements.environment_vars)}")
|
|
430
448
|
if requirements.command_line_args:
|
|
431
449
|
req_items.append(f"Config args: {len(requirements.command_line_args)}")
|
|
432
|
-
|
|
450
|
+
|
|
433
451
|
if req_items:
|
|
434
452
|
info_text += f"\n[yellow]Requirements:[/yellow] {' | '.join(req_items)}"
|
|
435
|
-
|
|
453
|
+
|
|
436
454
|
server_info.mount(Static(info_text))
|
|
437
|
-
|
|
455
|
+
|
|
438
456
|
# Setup configuration requirements
|
|
439
457
|
config_container = self.query_one("#env-vars-container", Container)
|
|
440
458
|
config_container.remove_children()
|
|
441
459
|
config_container.mount(Static("[bold]Server Configuration:[/bold]"))
|
|
442
|
-
|
|
460
|
+
|
|
443
461
|
# Add server name input
|
|
444
462
|
config_container.mount(Static("\n[bold blue]Server Name:[/bold blue]"))
|
|
445
463
|
name_row = Horizontal(classes="env-var-row")
|
|
@@ -449,40 +467,42 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
449
467
|
placeholder=f"Default: {self.selected_server.name}",
|
|
450
468
|
value=self.selected_server.name,
|
|
451
469
|
classes="env-var-input",
|
|
452
|
-
id="server-name-input"
|
|
470
|
+
id="server-name-input",
|
|
453
471
|
)
|
|
454
472
|
name_row.mount(name_input)
|
|
455
|
-
|
|
473
|
+
|
|
456
474
|
try:
|
|
457
475
|
# Check system requirements first
|
|
458
476
|
self._setup_system_requirements(config_container)
|
|
459
|
-
|
|
477
|
+
|
|
460
478
|
# Setup environment variables
|
|
461
479
|
self._setup_environment_variables(config_container)
|
|
462
|
-
|
|
480
|
+
|
|
463
481
|
# Setup command line arguments
|
|
464
482
|
self._setup_command_line_args(config_container)
|
|
465
|
-
|
|
483
|
+
|
|
466
484
|
# Show package dependencies info
|
|
467
485
|
self._setup_package_dependencies(config_container)
|
|
468
|
-
|
|
486
|
+
|
|
469
487
|
except Exception as e:
|
|
470
|
-
config_container.mount(
|
|
488
|
+
config_container.mount(
|
|
489
|
+
Static(f"[red]Error loading configuration: {e}[/red]")
|
|
490
|
+
)
|
|
471
491
|
|
|
472
492
|
def _setup_system_requirements(self, parent: Container) -> None:
|
|
473
493
|
"""Setup system requirements validation."""
|
|
474
494
|
required_tools = self.selected_server.get_required_tools()
|
|
475
|
-
|
|
495
|
+
|
|
476
496
|
if not required_tools:
|
|
477
497
|
return
|
|
478
|
-
|
|
498
|
+
|
|
479
499
|
parent.mount(Static("\n[bold cyan]System Tools:[/bold cyan]"))
|
|
480
|
-
|
|
500
|
+
|
|
481
501
|
# Import here to avoid circular imports
|
|
482
502
|
from code_puppy.mcp.system_tools import detector
|
|
483
|
-
|
|
503
|
+
|
|
484
504
|
tool_status = detector.detect_tools(required_tools)
|
|
485
|
-
|
|
505
|
+
|
|
486
506
|
for tool_name, tool_info in tool_status.items():
|
|
487
507
|
if tool_info.available:
|
|
488
508
|
status_text = f"✅ {tool_name}"
|
|
@@ -492,7 +512,7 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
492
512
|
else:
|
|
493
513
|
status_text = f"❌ {tool_name} - {tool_info.error or 'Not found'}"
|
|
494
514
|
parent.mount(Static(f"[red]{status_text}[/red]"))
|
|
495
|
-
|
|
515
|
+
|
|
496
516
|
# Show installation suggestions
|
|
497
517
|
suggestions = detector.get_installation_suggestions(tool_name)
|
|
498
518
|
if suggestions:
|
|
@@ -501,97 +521,106 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
501
521
|
def _setup_environment_variables(self, parent: Container) -> None:
|
|
502
522
|
"""Setup environment variables inputs."""
|
|
503
523
|
env_vars = self.selected_server.get_environment_vars()
|
|
504
|
-
|
|
524
|
+
|
|
505
525
|
if not env_vars:
|
|
506
526
|
return
|
|
507
|
-
|
|
527
|
+
|
|
508
528
|
parent.mount(Static("\n[bold yellow]Environment Variables:[/bold yellow]"))
|
|
509
|
-
|
|
529
|
+
|
|
510
530
|
for var in env_vars:
|
|
511
531
|
# Check if already set
|
|
512
532
|
import os
|
|
533
|
+
|
|
513
534
|
current_value = os.environ.get(var, "")
|
|
514
|
-
|
|
535
|
+
|
|
515
536
|
row_container = Horizontal(classes="env-var-row")
|
|
516
537
|
parent.mount(row_container)
|
|
517
|
-
|
|
538
|
+
|
|
518
539
|
status_indicator = "✅" if current_value else "📝"
|
|
519
|
-
row_container.mount(
|
|
520
|
-
|
|
540
|
+
row_container.mount(
|
|
541
|
+
Static(f"{status_indicator} {var}:", classes="env-var-label")
|
|
542
|
+
)
|
|
543
|
+
|
|
521
544
|
env_input = Input(
|
|
522
|
-
placeholder=f"Enter {var} value..."
|
|
545
|
+
placeholder=f"Enter {var} value..."
|
|
546
|
+
if not current_value
|
|
547
|
+
else "Already set",
|
|
523
548
|
value=current_value,
|
|
524
|
-
classes="env-var-input",
|
|
525
|
-
id=f"env-{var}"
|
|
549
|
+
classes="env-var-input",
|
|
550
|
+
id=f"env-{var}",
|
|
526
551
|
)
|
|
527
552
|
row_container.mount(env_input)
|
|
528
553
|
|
|
529
554
|
def _setup_command_line_args(self, parent: Container) -> None:
|
|
530
555
|
"""Setup command line arguments inputs."""
|
|
531
556
|
cmd_args = self.selected_server.get_command_line_args()
|
|
532
|
-
|
|
557
|
+
|
|
533
558
|
if not cmd_args:
|
|
534
559
|
return
|
|
535
|
-
|
|
560
|
+
|
|
536
561
|
parent.mount(Static("\n[bold green]Command Line Arguments:[/bold green]"))
|
|
537
|
-
|
|
562
|
+
|
|
538
563
|
for arg_config in cmd_args:
|
|
539
564
|
name = arg_config.get("name", "")
|
|
540
565
|
prompt = arg_config.get("prompt", name)
|
|
541
566
|
default = arg_config.get("default", "")
|
|
542
567
|
required = arg_config.get("required", True)
|
|
543
|
-
|
|
568
|
+
|
|
544
569
|
row_container = Horizontal(classes="env-var-row")
|
|
545
570
|
parent.mount(row_container)
|
|
546
|
-
|
|
571
|
+
|
|
547
572
|
indicator = "⚡" if required else "🔧"
|
|
548
573
|
label_text = f"{indicator} {prompt}:"
|
|
549
574
|
if not required:
|
|
550
575
|
label_text += " (optional)"
|
|
551
|
-
|
|
576
|
+
|
|
552
577
|
row_container.mount(Static(label_text, classes="env-var-label"))
|
|
553
|
-
|
|
578
|
+
|
|
554
579
|
arg_input = Input(
|
|
555
580
|
placeholder=f"Default: {default}" if default else f"Enter {name}...",
|
|
556
581
|
value=default,
|
|
557
582
|
classes="env-var-input",
|
|
558
|
-
id=f"arg-{name}"
|
|
583
|
+
id=f"arg-{name}",
|
|
559
584
|
)
|
|
560
585
|
row_container.mount(arg_input)
|
|
561
586
|
|
|
562
587
|
def _setup_package_dependencies(self, parent: Container) -> None:
|
|
563
588
|
"""Setup package dependencies information."""
|
|
564
589
|
packages = self.selected_server.get_package_dependencies()
|
|
565
|
-
|
|
590
|
+
|
|
566
591
|
if not packages:
|
|
567
592
|
return
|
|
568
|
-
|
|
593
|
+
|
|
569
594
|
parent.mount(Static("\n[bold magenta]Package Dependencies:[/bold magenta]"))
|
|
570
|
-
|
|
595
|
+
|
|
571
596
|
# Import here to avoid circular imports
|
|
572
597
|
from code_puppy.mcp.system_tools import detector
|
|
573
|
-
|
|
598
|
+
|
|
574
599
|
package_status = detector.check_package_dependencies(packages)
|
|
575
|
-
|
|
600
|
+
|
|
576
601
|
for package, available in package_status.items():
|
|
577
602
|
if available:
|
|
578
603
|
parent.mount(Static(f"✅ {package} (installed)"))
|
|
579
604
|
else:
|
|
580
|
-
parent.mount(
|
|
605
|
+
parent.mount(
|
|
606
|
+
Static(
|
|
607
|
+
f"[yellow]📦 {package} (will be installed automatically)[/yellow]"
|
|
608
|
+
)
|
|
609
|
+
)
|
|
581
610
|
|
|
582
611
|
def _install_server(self) -> None:
|
|
583
612
|
"""Install the selected server with configuration."""
|
|
584
613
|
if not self.selected_server:
|
|
585
614
|
return
|
|
586
|
-
|
|
615
|
+
|
|
587
616
|
try:
|
|
588
617
|
# Collect configuration inputs
|
|
589
618
|
env_vars = {}
|
|
590
619
|
cmd_args = {}
|
|
591
620
|
server_name = self.selected_server.name # Default fallback
|
|
592
|
-
|
|
621
|
+
|
|
593
622
|
all_inputs = self.query(Input)
|
|
594
|
-
|
|
623
|
+
|
|
595
624
|
for input_widget in all_inputs:
|
|
596
625
|
if input_widget.id == "server-name-input":
|
|
597
626
|
custom_name = input_widget.value.strip()
|
|
@@ -607,168 +636,168 @@ class MCPInstallWizardScreen(ModalScreen):
|
|
|
607
636
|
value = input_widget.value.strip()
|
|
608
637
|
if value:
|
|
609
638
|
cmd_args[arg_name] = value
|
|
610
|
-
|
|
639
|
+
|
|
611
640
|
# Set environment variables in the current environment
|
|
612
641
|
for var, value in env_vars.items():
|
|
613
642
|
os.environ[var] = value
|
|
614
|
-
|
|
643
|
+
|
|
615
644
|
# Get server config with command line argument overrides
|
|
616
645
|
config_dict = self.selected_server.to_server_config(server_name, **cmd_args)
|
|
617
|
-
|
|
646
|
+
|
|
618
647
|
# Update the config with actual environment variable values
|
|
619
|
-
if
|
|
620
|
-
for env_key, env_value in config_dict[
|
|
648
|
+
if "env" in config_dict:
|
|
649
|
+
for env_key, env_value in config_dict["env"].items():
|
|
621
650
|
# If it's a placeholder like $GITHUB_TOKEN, replace with actual value
|
|
622
|
-
if env_value.startswith(
|
|
651
|
+
if env_value.startswith("$"):
|
|
623
652
|
var_name = env_value[1:] # Remove the $
|
|
624
653
|
if var_name in env_vars:
|
|
625
|
-
config_dict[
|
|
626
|
-
|
|
654
|
+
config_dict["env"][env_key] = env_vars[var_name]
|
|
655
|
+
|
|
627
656
|
# Create and register the server
|
|
628
657
|
from code_puppy.mcp import ServerConfig
|
|
629
658
|
from code_puppy.mcp.manager import get_mcp_manager
|
|
630
|
-
|
|
659
|
+
|
|
631
660
|
server_config = ServerConfig(
|
|
632
661
|
id=server_name,
|
|
633
662
|
name=server_name,
|
|
634
|
-
type=config_dict.pop(
|
|
663
|
+
type=config_dict.pop("type"),
|
|
635
664
|
enabled=True,
|
|
636
|
-
config=config_dict
|
|
665
|
+
config=config_dict,
|
|
637
666
|
)
|
|
638
|
-
|
|
667
|
+
|
|
639
668
|
manager = get_mcp_manager()
|
|
640
669
|
server_id = manager.register_server(server_config)
|
|
641
|
-
|
|
670
|
+
|
|
642
671
|
if server_id:
|
|
643
672
|
# Save to mcp_servers.json
|
|
644
673
|
from code_puppy.config import MCP_SERVERS_FILE
|
|
645
|
-
|
|
674
|
+
|
|
646
675
|
if os.path.exists(MCP_SERVERS_FILE):
|
|
647
|
-
with open(MCP_SERVERS_FILE,
|
|
676
|
+
with open(MCP_SERVERS_FILE, "r") as f:
|
|
648
677
|
data = json.load(f)
|
|
649
678
|
servers = data.get("mcp_servers", {})
|
|
650
679
|
else:
|
|
651
680
|
servers = {}
|
|
652
681
|
data = {"mcp_servers": servers}
|
|
653
|
-
|
|
682
|
+
|
|
654
683
|
servers[server_name] = config_dict
|
|
655
|
-
servers[server_name][
|
|
656
|
-
|
|
684
|
+
servers[server_name]["type"] = server_config.type
|
|
685
|
+
|
|
657
686
|
os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
|
|
658
|
-
with open(MCP_SERVERS_FILE,
|
|
687
|
+
with open(MCP_SERVERS_FILE, "w") as f:
|
|
659
688
|
json.dump(data, f, indent=2)
|
|
660
|
-
|
|
689
|
+
|
|
661
690
|
# Reload MCP servers
|
|
662
691
|
from code_puppy.agent import reload_mcp_servers
|
|
692
|
+
|
|
663
693
|
reload_mcp_servers()
|
|
664
|
-
|
|
665
|
-
self.dismiss(
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
694
|
+
|
|
695
|
+
self.dismiss(
|
|
696
|
+
{
|
|
697
|
+
"success": True,
|
|
698
|
+
"message": f"Successfully installed '{server_name}' from {self.selected_server.display_name}",
|
|
699
|
+
"server_name": server_name,
|
|
700
|
+
}
|
|
701
|
+
)
|
|
670
702
|
else:
|
|
671
|
-
self.dismiss({
|
|
672
|
-
|
|
673
|
-
"message": "Failed to register server"
|
|
674
|
-
})
|
|
675
|
-
|
|
703
|
+
self.dismiss({"success": False, "message": "Failed to register server"})
|
|
704
|
+
|
|
676
705
|
except Exception as e:
|
|
677
|
-
self.dismiss(
|
|
678
|
-
"success": False,
|
|
679
|
-
|
|
680
|
-
})
|
|
706
|
+
self.dismiss(
|
|
707
|
+
{"success": False, "message": f"Installation failed: {str(e)}"}
|
|
708
|
+
)
|
|
681
709
|
|
|
682
710
|
def _install_custom_json(self) -> None:
|
|
683
711
|
"""Install server from custom JSON configuration."""
|
|
684
712
|
try:
|
|
685
713
|
name_input = self.query_one("#custom-name-input", Input)
|
|
686
714
|
json_input = self.query_one("#custom-json-input", TextArea)
|
|
687
|
-
|
|
715
|
+
|
|
688
716
|
server_name = name_input.value.strip()
|
|
689
717
|
json_text = json_input.text.strip()
|
|
690
|
-
|
|
718
|
+
|
|
691
719
|
if not server_name:
|
|
692
720
|
# Show error - need a name
|
|
693
721
|
return
|
|
694
|
-
|
|
722
|
+
|
|
695
723
|
if not json_text:
|
|
696
724
|
# Show error - need JSON config
|
|
697
725
|
return
|
|
698
|
-
|
|
726
|
+
|
|
699
727
|
# Parse JSON
|
|
700
728
|
try:
|
|
701
729
|
config_dict = json.loads(json_text)
|
|
702
|
-
except json.JSONDecodeError
|
|
730
|
+
except json.JSONDecodeError:
|
|
703
731
|
# Show error - invalid JSON
|
|
704
732
|
return
|
|
705
|
-
|
|
733
|
+
|
|
706
734
|
# Validate required fields
|
|
707
|
-
if
|
|
735
|
+
if "type" not in config_dict:
|
|
708
736
|
# Show error - missing type
|
|
709
737
|
return
|
|
710
|
-
|
|
738
|
+
|
|
711
739
|
# Extract type and create server config
|
|
712
|
-
server_type = config_dict.pop(
|
|
713
|
-
|
|
740
|
+
server_type = config_dict.pop("type")
|
|
741
|
+
|
|
714
742
|
# Create and register the server
|
|
715
743
|
from code_puppy.mcp import ServerConfig
|
|
716
744
|
from code_puppy.mcp.manager import get_mcp_manager
|
|
717
|
-
|
|
745
|
+
|
|
718
746
|
server_config = ServerConfig(
|
|
719
747
|
id=server_name,
|
|
720
748
|
name=server_name,
|
|
721
749
|
type=server_type,
|
|
722
750
|
enabled=True,
|
|
723
|
-
config=config_dict
|
|
751
|
+
config=config_dict,
|
|
724
752
|
)
|
|
725
|
-
|
|
753
|
+
|
|
726
754
|
manager = get_mcp_manager()
|
|
727
755
|
server_id = manager.register_server(server_config)
|
|
728
|
-
|
|
756
|
+
|
|
729
757
|
if server_id:
|
|
730
758
|
# Save to mcp_servers.json
|
|
731
759
|
from code_puppy.config import MCP_SERVERS_FILE
|
|
732
|
-
|
|
760
|
+
|
|
733
761
|
if os.path.exists(MCP_SERVERS_FILE):
|
|
734
|
-
with open(MCP_SERVERS_FILE,
|
|
762
|
+
with open(MCP_SERVERS_FILE, "r") as f:
|
|
735
763
|
data = json.load(f)
|
|
736
764
|
servers = data.get("mcp_servers", {})
|
|
737
765
|
else:
|
|
738
766
|
servers = {}
|
|
739
767
|
data = {"mcp_servers": servers}
|
|
740
|
-
|
|
768
|
+
|
|
741
769
|
# Add the full config including type
|
|
742
770
|
full_config = config_dict.copy()
|
|
743
|
-
full_config[
|
|
771
|
+
full_config["type"] = server_type
|
|
744
772
|
servers[server_name] = full_config
|
|
745
|
-
|
|
773
|
+
|
|
746
774
|
os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
|
|
747
|
-
with open(MCP_SERVERS_FILE,
|
|
775
|
+
with open(MCP_SERVERS_FILE, "w") as f:
|
|
748
776
|
json.dump(data, f, indent=2)
|
|
749
|
-
|
|
777
|
+
|
|
750
778
|
# Reload MCP servers
|
|
751
779
|
from code_puppy.agent import reload_mcp_servers
|
|
780
|
+
|
|
752
781
|
reload_mcp_servers()
|
|
753
|
-
|
|
754
|
-
self.dismiss(
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
782
|
+
|
|
783
|
+
self.dismiss(
|
|
784
|
+
{
|
|
785
|
+
"success": True,
|
|
786
|
+
"message": f"Successfully installed custom server '{server_name}'",
|
|
787
|
+
"server_name": server_name,
|
|
788
|
+
}
|
|
789
|
+
)
|
|
759
790
|
else:
|
|
760
|
-
self.dismiss(
|
|
761
|
-
"success": False,
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
791
|
+
self.dismiss(
|
|
792
|
+
{"success": False, "message": "Failed to register custom server"}
|
|
793
|
+
)
|
|
794
|
+
|
|
765
795
|
except Exception as e:
|
|
766
|
-
self.dismiss(
|
|
767
|
-
"success": False,
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
796
|
+
self.dismiss(
|
|
797
|
+
{"success": False, "message": f"Installation failed: {str(e)}"}
|
|
798
|
+
)
|
|
799
|
+
|
|
771
800
|
def on_key(self, event) -> None:
|
|
772
801
|
"""Handle key events."""
|
|
773
802
|
if event.key == "escape":
|
|
774
|
-
self.on_cancel_clicked()
|
|
803
|
+
self.on_cancel_clicked()
|