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.
Files changed (61) hide show
  1. code_puppy/agent.py +15 -17
  2. code_puppy/agents/agent_manager.py +320 -9
  3. code_puppy/agents/base_agent.py +58 -2
  4. code_puppy/agents/runtime_manager.py +68 -42
  5. code_puppy/command_line/command_handler.py +82 -33
  6. code_puppy/command_line/mcp/__init__.py +10 -0
  7. code_puppy/command_line/mcp/add_command.py +183 -0
  8. code_puppy/command_line/mcp/base.py +35 -0
  9. code_puppy/command_line/mcp/handler.py +133 -0
  10. code_puppy/command_line/mcp/help_command.py +146 -0
  11. code_puppy/command_line/mcp/install_command.py +176 -0
  12. code_puppy/command_line/mcp/list_command.py +94 -0
  13. code_puppy/command_line/mcp/logs_command.py +126 -0
  14. code_puppy/command_line/mcp/remove_command.py +82 -0
  15. code_puppy/command_line/mcp/restart_command.py +92 -0
  16. code_puppy/command_line/mcp/search_command.py +117 -0
  17. code_puppy/command_line/mcp/start_all_command.py +126 -0
  18. code_puppy/command_line/mcp/start_command.py +98 -0
  19. code_puppy/command_line/mcp/status_command.py +185 -0
  20. code_puppy/command_line/mcp/stop_all_command.py +109 -0
  21. code_puppy/command_line/mcp/stop_command.py +79 -0
  22. code_puppy/command_line/mcp/test_command.py +107 -0
  23. code_puppy/command_line/mcp/utils.py +129 -0
  24. code_puppy/command_line/mcp/wizard_utils.py +259 -0
  25. code_puppy/command_line/model_picker_completion.py +21 -4
  26. code_puppy/command_line/prompt_toolkit_completion.py +9 -0
  27. code_puppy/config.py +5 -5
  28. code_puppy/main.py +23 -17
  29. code_puppy/mcp/__init__.py +42 -16
  30. code_puppy/mcp/async_lifecycle.py +51 -49
  31. code_puppy/mcp/blocking_startup.py +125 -113
  32. code_puppy/mcp/captured_stdio_server.py +63 -70
  33. code_puppy/mcp/circuit_breaker.py +63 -47
  34. code_puppy/mcp/config_wizard.py +169 -136
  35. code_puppy/mcp/dashboard.py +79 -71
  36. code_puppy/mcp/error_isolation.py +147 -100
  37. code_puppy/mcp/examples/retry_example.py +55 -42
  38. code_puppy/mcp/health_monitor.py +152 -141
  39. code_puppy/mcp/managed_server.py +100 -93
  40. code_puppy/mcp/manager.py +168 -156
  41. code_puppy/mcp/registry.py +148 -110
  42. code_puppy/mcp/retry_manager.py +63 -61
  43. code_puppy/mcp/server_registry_catalog.py +271 -225
  44. code_puppy/mcp/status_tracker.py +80 -80
  45. code_puppy/mcp/system_tools.py +47 -52
  46. code_puppy/messaging/message_queue.py +20 -13
  47. code_puppy/messaging/renderers.py +30 -15
  48. code_puppy/state_management.py +103 -0
  49. code_puppy/tui/app.py +64 -7
  50. code_puppy/tui/components/chat_view.py +3 -3
  51. code_puppy/tui/components/human_input_modal.py +12 -8
  52. code_puppy/tui/screens/__init__.py +2 -2
  53. code_puppy/tui/screens/mcp_install_wizard.py +208 -179
  54. code_puppy/tui/tests/test_agent_command.py +3 -3
  55. {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/METADATA +1 -1
  56. {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/RECORD +60 -42
  57. code_puppy/command_line/mcp_commands.py +0 -1789
  58. {code_puppy-0.0.135.data → code_puppy-0.0.137.data}/data/code_puppy/models.json +0 -0
  59. {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/WHEEL +0 -0
  60. {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/entry_points.txt +0 -0
  61. {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, Vertical
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(placeholder="Search MCP servers (e.g. 'github', 'postgres')...", id="search-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(placeholder="Server name (e.g. 'my-sqlite-db')", id="custom-name-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(servers, key=lambda s: (not s.popular, not s.verified, s.display_name))
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 = server.description[:60] + "..." if len(server.description) > 60 else server.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(Static("No servers found"), id=f"no-results-{counter}")
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(Static("[red]Server registry not available[/red]"), id=f"error-{counter}")
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 = server.description[:60] + "..." if len(server.description) > 60 else server.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(Static(f"No servers found for '{query}'"), id=f"no-results-{counter}")
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(Static("[red]Server registry not available[/red]"), id=f"error-{counter}")
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, 'server_data'):
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, 'type', 'stdio')}"""
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(Static(f"[red]Error loading configuration: {e}[/red]"))
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(Static(f"{status_indicator} {var}:", classes="env-var-label"))
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..." if not current_value else "Already set",
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(Static(f"[yellow]📦 {package} (will be installed automatically)[/yellow]"))
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 'env' in config_dict:
620
- for env_key, env_value in config_dict['env'].items():
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['env'][env_key] = env_vars[var_name]
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('type'),
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, 'r') as f:
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]['type'] = server_config.type
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, 'w') as f:
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
- "success": True,
667
- "message": f"Successfully installed '{server_name}' from {self.selected_server.display_name}",
668
- "server_name": server_name
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
- "success": False,
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
- "message": f"Installation failed: {str(e)}"
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 as e:
730
+ except json.JSONDecodeError:
703
731
  # Show error - invalid JSON
704
732
  return
705
-
733
+
706
734
  # Validate required fields
707
- if 'type' not in config_dict:
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('type')
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, 'r') as f:
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['type'] = server_type
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, 'w') as f:
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
- "success": True,
756
- "message": f"Successfully installed custom server '{server_name}'",
757
- "server_name": server_name
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
- "message": "Failed to register custom server"
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
- "message": f"Installation failed: {str(e)}"
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()