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.
Files changed (60) hide show
  1. code_puppy/agent.py +15 -17
  2. code_puppy/agents/agent_manager.py +320 -9
  3. code_puppy/agents/base_agent.py +58 -2
  4. code_puppy/agents/runtime_manager.py +68 -42
  5. code_puppy/command_line/command_handler.py +82 -33
  6. code_puppy/command_line/mcp/__init__.py +10 -0
  7. code_puppy/command_line/mcp/add_command.py +183 -0
  8. code_puppy/command_line/mcp/base.py +35 -0
  9. code_puppy/command_line/mcp/handler.py +133 -0
  10. code_puppy/command_line/mcp/help_command.py +146 -0
  11. code_puppy/command_line/mcp/install_command.py +176 -0
  12. code_puppy/command_line/mcp/list_command.py +94 -0
  13. code_puppy/command_line/mcp/logs_command.py +126 -0
  14. code_puppy/command_line/mcp/remove_command.py +82 -0
  15. code_puppy/command_line/mcp/restart_command.py +92 -0
  16. code_puppy/command_line/mcp/search_command.py +117 -0
  17. code_puppy/command_line/mcp/start_all_command.py +126 -0
  18. code_puppy/command_line/mcp/start_command.py +98 -0
  19. code_puppy/command_line/mcp/status_command.py +185 -0
  20. code_puppy/command_line/mcp/stop_all_command.py +109 -0
  21. code_puppy/command_line/mcp/stop_command.py +79 -0
  22. code_puppy/command_line/mcp/test_command.py +107 -0
  23. code_puppy/command_line/mcp/utils.py +129 -0
  24. code_puppy/command_line/mcp/wizard_utils.py +259 -0
  25. code_puppy/command_line/model_picker_completion.py +21 -4
  26. code_puppy/command_line/prompt_toolkit_completion.py +9 -0
  27. code_puppy/main.py +23 -17
  28. code_puppy/mcp/__init__.py +42 -16
  29. code_puppy/mcp/async_lifecycle.py +51 -49
  30. code_puppy/mcp/blocking_startup.py +125 -113
  31. code_puppy/mcp/captured_stdio_server.py +63 -70
  32. code_puppy/mcp/circuit_breaker.py +63 -47
  33. code_puppy/mcp/config_wizard.py +169 -136
  34. code_puppy/mcp/dashboard.py +79 -71
  35. code_puppy/mcp/error_isolation.py +147 -100
  36. code_puppy/mcp/examples/retry_example.py +55 -42
  37. code_puppy/mcp/health_monitor.py +152 -141
  38. code_puppy/mcp/managed_server.py +100 -97
  39. code_puppy/mcp/manager.py +168 -156
  40. code_puppy/mcp/registry.py +148 -110
  41. code_puppy/mcp/retry_manager.py +63 -61
  42. code_puppy/mcp/server_registry_catalog.py +271 -225
  43. code_puppy/mcp/status_tracker.py +80 -80
  44. code_puppy/mcp/system_tools.py +47 -52
  45. code_puppy/messaging/message_queue.py +20 -13
  46. code_puppy/messaging/renderers.py +30 -15
  47. code_puppy/state_management.py +103 -0
  48. code_puppy/tui/app.py +64 -7
  49. code_puppy/tui/components/chat_view.py +3 -3
  50. code_puppy/tui/components/human_input_modal.py +12 -8
  51. code_puppy/tui/screens/__init__.py +2 -2
  52. code_puppy/tui/screens/mcp_install_wizard.py +208 -179
  53. code_puppy/tui/tests/test_agent_command.py +3 -3
  54. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
  55. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
  56. code_puppy/command_line/mcp_commands.py +0 -1789
  57. {code_puppy-0.0.134.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
  58. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
  59. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
  60. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
@@ -7,8 +7,20 @@ all references to the agent are properly updated when it's reloaded.
7
7
 
8
8
  import asyncio
9
9
  import signal
10
+ import sys
10
11
  import uuid
11
- from typing import Optional, Any
12
+ from typing import Any, Optional
13
+
14
+ # ExceptionGroup is available in Python 3.11+
15
+ if sys.version_info >= (3, 11):
16
+ from builtins import ExceptionGroup
17
+ else:
18
+ # For Python 3.10 and below, we can define a simple fallback
19
+ class ExceptionGroup(Exception):
20
+ def __init__(self, message, exceptions):
21
+ super().__init__(message)
22
+ self.exceptions = exceptions
23
+
12
24
 
13
25
  import mcp
14
26
  from pydantic_ai import Agent
@@ -20,71 +32,79 @@ from code_puppy.messaging.message_queue import emit_info, emit_warning
20
32
  class RuntimeAgentManager:
21
33
  """
22
34
  Manages the runtime agent instance and ensures proper updates.
23
-
35
+
24
36
  This class acts as a proxy that always returns the current agent instance,
25
37
  ensuring that when the agent is reloaded, all code using this manager
26
38
  automatically gets the updated instance.
27
39
  """
28
-
40
+
29
41
  def __init__(self):
30
42
  """Initialize the runtime agent manager."""
31
43
  self._agent: Optional[Agent] = None
32
44
  self._last_model_name: Optional[str] = None
33
-
45
+
34
46
  def get_agent(self, force_reload: bool = False, message_group: str = "") -> Agent:
35
47
  """
36
48
  Get the current agent instance.
37
-
49
+
38
50
  This method always returns the most recent agent instance,
39
51
  automatically handling reloads when the model changes.
40
-
52
+
41
53
  Args:
42
54
  force_reload: If True, force a reload of the agent
43
-
55
+
44
56
  Returns:
45
57
  The current agent instance
46
58
  """
47
59
  from code_puppy.agent import get_code_generation_agent
48
-
60
+
49
61
  # Always get the current singleton - this ensures we have the latest
50
- current_agent = get_code_generation_agent(force_reload=force_reload, message_group=message_group)
62
+ current_agent = get_code_generation_agent(
63
+ force_reload=force_reload, message_group=message_group
64
+ )
51
65
  self._agent = current_agent
52
-
66
+
53
67
  return self._agent
54
-
68
+
55
69
  def reload_agent(self) -> Agent:
56
70
  """
57
71
  Force reload the agent.
58
-
72
+
59
73
  This is typically called after MCP servers are started/stopped.
60
-
74
+
61
75
  Returns:
62
76
  The newly loaded agent instance
63
77
  """
64
78
  message_group = uuid.uuid4()
65
- emit_info("[bold cyan]Reloading agent with updated configuration...[/bold cyan]", message_group=message_group)
79
+ emit_info(
80
+ "[bold cyan]Reloading agent with updated configuration...[/bold cyan]",
81
+ message_group=message_group,
82
+ )
66
83
  return self.get_agent(force_reload=True, message_group=message_group)
67
-
68
- async def run_with_mcp(self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs) -> Any:
84
+
85
+ async def run_with_mcp(
86
+ self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs
87
+ ) -> Any:
69
88
  """
70
89
  Run the agent with MCP servers and full cancellation support.
71
-
90
+
72
91
  This method ensures we're always using the current agent instance
73
92
  and handles Ctrl+C interruption properly by creating a cancellable task.
74
-
93
+
75
94
  Args:
76
95
  prompt: The user prompt to process
77
96
  usage_limits: Optional usage limits for the agent
78
97
  **kwargs: Additional arguments to pass to agent.run (e.g., message_history)
79
-
98
+
80
99
  Returns:
81
100
  The agent's response
82
-
101
+
83
102
  Raises:
84
103
  asyncio.CancelledError: When execution is cancelled by user
85
104
  """
86
105
  agent = self.get_agent()
87
106
  group_id = str(uuid.uuid4())
107
+
88
108
  # Function to run agent with MCP
89
109
  async def run_agent_task():
90
110
  try:
@@ -93,12 +113,15 @@ class RuntimeAgentManager:
93
113
  except* mcp.shared.exceptions.McpError as mcp_error:
94
114
  emit_warning(f"MCP server error: {str(mcp_error)}", group_id=group_id)
95
115
  emit_warning(f"{str(mcp_error)}", group_id=group_id)
96
- emit_warning(f"Try disabling any malfunctioning MCP servers", group_id=group_id)
116
+ emit_warning(
117
+ "Try disabling any malfunctioning MCP servers", group_id=group_id
118
+ )
97
119
  except* InterruptedError as ie:
98
120
  emit_warning(f"Interrupted: {str(ie)}")
99
121
  except* Exception as other_error:
100
122
  # Filter out CancelledError from the exception group - let it propagate
101
123
  remaining_exceptions = []
124
+
102
125
  def collect_non_cancelled_exceptions(exc):
103
126
  if isinstance(exc, ExceptionGroup):
104
127
  for sub_exc in exc.exceptions:
@@ -107,40 +130,41 @@ class RuntimeAgentManager:
107
130
  remaining_exceptions.append(exc)
108
131
  emit_warning(f"Unexpected error: {str(exc)}", group_id=group_id)
109
132
  emit_warning(f"{str(exc.args)}", group_id=group_id)
110
-
133
+
111
134
  collect_non_cancelled_exceptions(other_error)
112
-
135
+
113
136
  # If there are CancelledError exceptions in the group, re-raise them
114
137
  cancelled_exceptions = []
138
+
115
139
  def collect_cancelled_exceptions(exc):
116
140
  if isinstance(exc, ExceptionGroup):
117
141
  for sub_exc in exc.exceptions:
118
142
  collect_cancelled_exceptions(sub_exc)
119
143
  elif isinstance(exc, asyncio.CancelledError):
120
144
  cancelled_exceptions.append(exc)
121
-
145
+
122
146
  collect_cancelled_exceptions(other_error)
123
-
147
+
124
148
  if cancelled_exceptions:
125
149
  # Re-raise the first CancelledError to propagate cancellation
126
150
  raise cancelled_exceptions[0]
127
-
151
+
128
152
  # Create the task FIRST
129
153
  agent_task = asyncio.create_task(run_agent_task())
130
-
154
+
131
155
  # Import shell process killer
132
156
  from code_puppy.tools.command_runner import kill_all_running_shell_processes
133
-
157
+
134
158
  # Ensure the interrupt handler only acts once per task
135
159
  handled = False
136
-
160
+
137
161
  def keyboard_interrupt_handler(sig, frame):
138
162
  """Signal handler for Ctrl+C - replicating exact original logic"""
139
163
  nonlocal handled
140
164
  if handled:
141
165
  return
142
166
  handled = True
143
-
167
+
144
168
  # First, nuke any running shell processes triggered by tools
145
169
  try:
146
170
  killed = kill_all_running_shell_processes()
@@ -157,11 +181,11 @@ class RuntimeAgentManager:
157
181
  agent_task.cancel()
158
182
  # Don't call the original handler
159
183
  # This prevents the application from exiting
160
-
184
+
161
185
  try:
162
186
  # Save original handler and set our custom one AFTER task is created
163
187
  original_handler = signal.signal(signal.SIGINT, keyboard_interrupt_handler)
164
-
188
+
165
189
  # Wait for the task to complete or be cancelled
166
190
  result = await agent_task
167
191
  return result
@@ -181,32 +205,34 @@ class RuntimeAgentManager:
181
205
  # Restore original signal handler
182
206
  if original_handler:
183
207
  signal.signal(signal.SIGINT, original_handler)
184
-
185
- async def run(self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs) -> Any:
208
+
209
+ async def run(
210
+ self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs
211
+ ) -> Any:
186
212
  """
187
213
  Run the agent without explicitly managing MCP servers.
188
-
214
+
189
215
  Args:
190
216
  prompt: The user prompt to process
191
217
  usage_limits: Optional usage limits for the agent
192
218
  **kwargs: Additional arguments to pass to agent.run (e.g., message_history)
193
-
219
+
194
220
  Returns:
195
221
  The agent's response
196
222
  """
197
223
  agent = self.get_agent()
198
224
  return await agent.run(prompt, usage_limits=usage_limits, **kwargs)
199
-
225
+
200
226
  def __getattr__(self, name: str) -> Any:
201
227
  """
202
228
  Proxy all other attribute access to the current agent.
203
-
229
+
204
230
  This allows the manager to be used as a drop-in replacement
205
231
  for direct agent access.
206
-
232
+
207
233
  Args:
208
234
  name: The attribute name to access
209
-
235
+
210
236
  Returns:
211
237
  The attribute from the current agent
212
238
  """
@@ -221,11 +247,11 @@ _runtime_manager: Optional[RuntimeAgentManager] = None
221
247
  def get_runtime_agent_manager() -> RuntimeAgentManager:
222
248
  """
223
249
  Get the global runtime agent manager instance.
224
-
250
+
225
251
  Returns:
226
252
  The singleton RuntimeAgentManager instance
227
253
  """
228
254
  global _runtime_manager
229
255
  if _runtime_manager is None:
230
256
  _runtime_manager = RuntimeAgentManager()
231
- return _runtime_manager
257
+ return _runtime_manager
@@ -9,41 +9,85 @@ from code_puppy.command_line.utils import make_directory_table
9
9
  from code_puppy.config import get_config_keys
10
10
  from code_puppy.tools.tools_content import tools_content
11
11
 
12
+
12
13
  def get_commands_help():
13
14
  """Generate commands help using Rich Text objects to avoid markup conflicts."""
14
15
  from rich.text import Text
15
-
16
+
16
17
  # Build help text programmatically
17
18
  help_lines = []
18
-
19
+
19
20
  # Title
20
21
  help_lines.append(Text("Commands Help", style="bold magenta"))
21
-
22
- # Commands - build each line programmatically
23
- help_lines.append(Text("/help, /h", style="cyan") + Text(" Show this help message"))
24
- help_lines.append(Text("/cd", style="cyan") + Text(" <dir> Change directory or show directories"))
25
- help_lines.append(Text("/agent", style="cyan") + Text(" <name> Switch to a different agent or show available agents"))
26
- help_lines.append(Text("/exit, /quit", style="cyan") + Text(" Exit interactive mode"))
27
- help_lines.append(Text("/generate-pr-description", style="cyan") + Text(" [@dir] Generate comprehensive PR description"))
28
- help_lines.append(Text("/model", style="cyan") + Text(" <model> Set active model"))
29
- help_lines.append(Text("/mcp", style="cyan") + Text(" Manage MCP servers (list, start, stop, status, etc.)"))
30
- help_lines.append(Text("/motd", style="cyan") + Text(" Show the latest message of the day (MOTD)"))
31
- help_lines.append(Text("/show", style="cyan") + Text(" Show puppy config key-values"))
32
- help_lines.append(Text("/compact", style="cyan") + Text(" Summarize and compact current chat history"))
33
- help_lines.append(Text("/dump_context", style="cyan") + Text(" <name> Save current message history to file"))
34
- help_lines.append(Text("/load_context", style="cyan") + Text(" <name> Load message history from file"))
35
- help_lines.append(Text("/set", style="cyan") + Text(" Set puppy config key-values (e.g., /set yolo_mode true, /set compaction_strategy truncation)"))
36
- help_lines.append(Text("/tools", style="cyan") + Text(" Show available tools and capabilities"))
37
- help_lines.append(Text("/<unknown>", style="cyan") + Text(" Show unknown command warning"))
38
-
39
-
22
+
23
+ # Commands - build each line programmatically
24
+ help_lines.append(
25
+ Text("/help, /h", style="cyan") + Text(" Show this help message")
26
+ )
27
+ help_lines.append(
28
+ Text("/cd", style="cyan")
29
+ + Text(" <dir> Change directory or show directories")
30
+ )
31
+ help_lines.append(
32
+ Text("/agent", style="cyan")
33
+ + Text(" <name> Switch to a different agent or show available agents")
34
+ )
35
+ help_lines.append(
36
+ Text("/exit, /quit", style="cyan") + Text(" Exit interactive mode")
37
+ )
38
+ help_lines.append(
39
+ Text("/generate-pr-description", style="cyan")
40
+ + Text(" [@dir] Generate comprehensive PR description")
41
+ )
42
+ help_lines.append(
43
+ Text("/model, /m", style="cyan") + Text(" <model> Set active model")
44
+ )
45
+ help_lines.append(
46
+ Text("/mcp", style="cyan")
47
+ + Text(" Manage MCP servers (list, start, stop, status, etc.)")
48
+ )
49
+ help_lines.append(
50
+ Text("/motd", style="cyan")
51
+ + Text(" Show the latest message of the day (MOTD)")
52
+ )
53
+ help_lines.append(
54
+ Text("/show", style="cyan")
55
+ + Text(" Show puppy config key-values")
56
+ )
57
+ help_lines.append(
58
+ Text("/compact", style="cyan")
59
+ + Text(" Summarize and compact current chat history")
60
+ )
61
+ help_lines.append(
62
+ Text("/dump_context", style="cyan")
63
+ + Text(" <name> Save current message history to file")
64
+ )
65
+ help_lines.append(
66
+ Text("/load_context", style="cyan")
67
+ + Text(" <name> Load message history from file")
68
+ )
69
+ help_lines.append(
70
+ Text("/set", style="cyan")
71
+ + Text(
72
+ " Set puppy config key-values (e.g., /set yolo_mode true, /set compaction_strategy truncation)"
73
+ )
74
+ )
75
+ help_lines.append(
76
+ Text("/tools", style="cyan")
77
+ + Text(" Show available tools and capabilities")
78
+ )
79
+ help_lines.append(
80
+ Text("/<unknown>", style="cyan")
81
+ + Text(" Show unknown command warning")
82
+ )
83
+
40
84
  # Combine all lines
41
85
  final_text = Text()
42
86
  for i, line in enumerate(help_lines):
43
87
  if i > 0:
44
88
  final_text.append("\n")
45
89
  final_text.append_text(line)
46
-
90
+
47
91
  return final_text
48
92
 
49
93
 
@@ -69,9 +113,9 @@ def handle_command(command: str):
69
113
  from code_puppy.config import get_compaction_strategy
70
114
  from code_puppy.message_history_processor import (
71
115
  estimate_tokens_for_message,
116
+ get_protected_token_count,
72
117
  summarize_messages,
73
118
  truncation,
74
- get_protected_token_count,
75
119
  )
76
120
  from code_puppy.messaging import (
77
121
  emit_error,
@@ -152,17 +196,16 @@ def handle_command(command: str):
152
196
  return True
153
197
 
154
198
  if command.strip().startswith("/show"):
199
+ from code_puppy.agents import get_current_agent_config
155
200
  from code_puppy.command_line.model_picker_completion import get_active_model
156
201
  from code_puppy.config import (
202
+ get_compaction_strategy,
203
+ get_compaction_threshold,
157
204
  get_owner_name,
158
205
  get_protected_token_count,
159
206
  get_puppy_name,
160
- get_compaction_threshold,
161
207
  get_yolo_mode,
162
208
  )
163
- from code_puppy.agents import get_current_agent_config
164
-
165
- from code_puppy.config import get_compaction_strategy
166
209
 
167
210
  puppy_name = get_puppy_name()
168
211
  owner_name = get_owner_name()
@@ -234,10 +277,10 @@ def handle_command(command: str):
234
277
  if command.startswith("/agent"):
235
278
  # Handle agent switching
236
279
  from code_puppy.agents import (
280
+ get_agent_descriptions,
237
281
  get_available_agents,
238
282
  get_current_agent_config,
239
283
  set_current_agent,
240
- get_agent_descriptions,
241
284
  )
242
285
  from code_puppy.agents.runtime_manager import get_runtime_agent_manager
243
286
 
@@ -317,10 +360,14 @@ def handle_command(command: str):
317
360
  emit_warning("Usage: /agent [agent-name]")
318
361
  return True
319
362
 
320
- if command.startswith("/model"):
363
+ if command.startswith("/model") or command.startswith("/m "):
321
364
  # Try setting model and show confirmation
322
365
  # Handle both /model and /m for backward compatibility
323
- model_command = command.replace("/model", "/m") if command.startswith("/model") else command
366
+ model_command = command
367
+ if command.startswith("/model"):
368
+ # Convert /model to /m for internal processing
369
+ model_command = command.replace("/model", "/m", 1)
370
+
324
371
  new_input = update_model_in_input(model_command)
325
372
  if new_input is not None:
326
373
  from code_puppy.agents.runtime_manager import get_runtime_agent_manager
@@ -334,16 +381,18 @@ def handle_command(command: str):
334
381
  return True
335
382
  # If no model matched, show available models
336
383
  model_names = load_model_names()
337
- emit_warning("Usage: /model <model-name>")
384
+ emit_warning("Usage: /model <model-name> or /m <model-name>")
338
385
  emit_warning(f"Available models: {', '.join(model_names)}")
339
386
  return True
340
-
387
+
341
388
  if command.startswith("/mcp"):
342
- from code_puppy.command_line.mcp_commands import MCPCommandHandler
389
+ from code_puppy.command_line.mcp import MCPCommandHandler
390
+
343
391
  handler = MCPCommandHandler()
344
392
  return handler.handle_mcp_command(command)
345
393
  if command in ("/help", "/h"):
346
394
  import uuid
395
+
347
396
  group_id = str(uuid.uuid4())
348
397
  help_text = get_commands_help()
349
398
  emit_info(help_text, message_group_id=group_id)
@@ -0,0 +1,10 @@
1
+ """
2
+ MCP Command Line Interface - Namespace package for MCP server management commands.
3
+
4
+ This package provides a modular command interface for managing MCP servers.
5
+ Each command is implemented in its own module for better maintainability.
6
+ """
7
+
8
+ from .handler import MCPCommandHandler
9
+
10
+ __all__ = ["MCPCommandHandler"]
@@ -0,0 +1,183 @@
1
+ """
2
+ MCP Add Command - Adds new MCP servers from JSON configuration or wizard.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ from typing import List, Optional
9
+
10
+ from code_puppy.messaging import emit_info
11
+ from code_puppy.state_management import is_tui_mode
12
+
13
+ from .base import MCPCommandBase
14
+ from .wizard_utils import run_interactive_install_wizard
15
+
16
+ # Configure logging
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class AddCommand(MCPCommandBase):
21
+ """
22
+ Command handler for adding MCP servers.
23
+
24
+ Adds new MCP servers from JSON configuration or interactive wizard.
25
+ """
26
+
27
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
28
+ """
29
+ Add a new MCP server from JSON configuration or launch wizard.
30
+
31
+ Usage:
32
+ /mcp add - Launch interactive wizard
33
+ /mcp add <json> - Add server from JSON config
34
+
35
+ Example JSON:
36
+ /mcp add {"name": "test", "type": "stdio", "command": "echo", "args": ["hello"]}
37
+
38
+ Args:
39
+ args: Command arguments - JSON config or empty for wizard
40
+ group_id: Optional message group ID for grouping related messages
41
+ """
42
+ if group_id is None:
43
+ group_id = self.generate_group_id()
44
+
45
+ # Check if in TUI mode and guide user to use Ctrl+T instead
46
+ if is_tui_mode() and not args:
47
+ emit_info(
48
+ "💡 In TUI mode, press Ctrl+T to open the MCP Install Wizard",
49
+ message_group=group_id,
50
+ )
51
+ emit_info(
52
+ " The wizard provides a better interface for browsing and installing MCP servers.",
53
+ message_group=group_id,
54
+ )
55
+ return
56
+
57
+ try:
58
+ if args:
59
+ # Parse JSON from arguments
60
+ json_str = " ".join(args)
61
+
62
+ try:
63
+ config_dict = json.loads(json_str)
64
+ except json.JSONDecodeError as e:
65
+ emit_info(f"Invalid JSON: {e}", message_group=group_id)
66
+ emit_info(
67
+ "Usage: /mcp add <json> or /mcp add (for wizard)",
68
+ message_group=group_id,
69
+ )
70
+ emit_info(
71
+ 'Example: /mcp add {"name": "test", "type": "stdio", "command": "echo"}',
72
+ message_group=group_id,
73
+ )
74
+ return
75
+
76
+ # Validate required fields
77
+ if "name" not in config_dict:
78
+ emit_info("Missing required field: 'name'", message_group=group_id)
79
+ return
80
+ if "type" not in config_dict:
81
+ emit_info("Missing required field: 'type'", message_group=group_id)
82
+ return
83
+
84
+ # Add the server
85
+ success = self._add_server_from_json(config_dict, group_id)
86
+
87
+ if success:
88
+ # Reload MCP servers
89
+ try:
90
+ from code_puppy.agent import reload_mcp_servers
91
+
92
+ reload_mcp_servers()
93
+ except ImportError:
94
+ pass
95
+
96
+ emit_info(
97
+ "Use '/mcp list' to see all servers", message_group=group_id
98
+ )
99
+
100
+ else:
101
+ # No arguments - launch interactive wizard with server templates
102
+ success = run_interactive_install_wizard(self.manager, group_id)
103
+
104
+ if success:
105
+ # Reload the agent to pick up new server
106
+ try:
107
+ from code_puppy.agent import reload_mcp_servers
108
+
109
+ reload_mcp_servers()
110
+ except ImportError:
111
+ pass
112
+
113
+ except ImportError as e:
114
+ logger.error(f"Failed to import: {e}")
115
+ emit_info("Required module not available", message_group=group_id)
116
+ except Exception as e:
117
+ logger.error(f"Error in add command: {e}")
118
+ emit_info(f"[red]Error adding server: {e}[/red]", message_group=group_id)
119
+
120
+ def _add_server_from_json(self, config_dict: dict, group_id: str) -> bool:
121
+ """
122
+ Add a server from JSON configuration.
123
+
124
+ Args:
125
+ config_dict: Server configuration dictionary
126
+ group_id: Message group ID
127
+
128
+ Returns:
129
+ True if successful, False otherwise
130
+ """
131
+ try:
132
+ from code_puppy.config import MCP_SERVERS_FILE
133
+ from code_puppy.mcp.managed_server import ServerConfig
134
+
135
+ # Extract required fields
136
+ name = config_dict.pop("name")
137
+ server_type = config_dict.pop("type")
138
+ enabled = config_dict.pop("enabled", True)
139
+
140
+ # Everything else goes into config
141
+ server_config = ServerConfig(
142
+ id=f"{name}_{hash(name)}",
143
+ name=name,
144
+ type=server_type,
145
+ enabled=enabled,
146
+ config=config_dict, # Remaining fields are server-specific config
147
+ )
148
+
149
+ # Register the server
150
+ server_id = self.manager.register_server(server_config)
151
+
152
+ if not server_id:
153
+ emit_info(f"Failed to add server '{name}'", message_group=group_id)
154
+ return False
155
+
156
+ emit_info(
157
+ f"✅ Added server '{name}' (ID: {server_id})", message_group=group_id
158
+ )
159
+
160
+ # Save to mcp_servers.json for persistence
161
+ if os.path.exists(MCP_SERVERS_FILE):
162
+ with open(MCP_SERVERS_FILE, "r") as f:
163
+ data = json.load(f)
164
+ servers = data.get("mcp_servers", {})
165
+ else:
166
+ servers = {}
167
+ data = {"mcp_servers": servers}
168
+
169
+ # Add new server
170
+ servers[name] = config_dict.copy()
171
+ servers[name]["type"] = server_type
172
+
173
+ # Save back
174
+ os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
175
+ with open(MCP_SERVERS_FILE, "w") as f:
176
+ json.dump(data, f, indent=2)
177
+
178
+ return True
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error adding server from JSON: {e}")
182
+ emit_info(f"[red]Failed to add server: {e}[/red]", message_group=group_id)
183
+ return False
@@ -0,0 +1,35 @@
1
+ """
2
+ MCP Command Base Classes - Shared functionality for MCP command handlers.
3
+
4
+ Provides base classes and common utilities used across all MCP command modules.
5
+ """
6
+
7
+ import logging
8
+
9
+ from rich.console import Console
10
+
11
+ from code_puppy.mcp.manager import get_mcp_manager
12
+
13
+ # Configure logging
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class MCPCommandBase:
18
+ """
19
+ Base class for MCP command handlers.
20
+
21
+ Provides common functionality like console access and MCP manager access
22
+ that all command handlers need.
23
+ """
24
+
25
+ def __init__(self):
26
+ """Initialize the base command handler."""
27
+ self.console = Console()
28
+ self.manager = get_mcp_manager()
29
+ logger.debug(f"Initialized {self.__class__.__name__}")
30
+
31
+ def generate_group_id(self) -> str:
32
+ """Generate a unique group ID for message grouping."""
33
+ import uuid
34
+
35
+ return str(uuid.uuid4())