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.
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 -93
  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.135.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
  55. {code_puppy-0.0.135.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.135.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
  58. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
  59. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
  60. {code_puppy-0.0.135.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
@@ -5,22 +5,27 @@ This module provides a managed wrapper around pydantic-ai MCP server classes
5
5
  that adds management capabilities while maintaining 100% compatibility.
6
6
  """
7
7
 
8
- import asyncio
9
8
  import json
10
9
  import logging
11
10
  import uuid
12
11
  from dataclasses import dataclass, field
13
12
  from datetime import datetime, timedelta
14
13
  from enum import Enum
15
- from typing import Dict, Union, Optional, Any
14
+ from typing import Any, Dict, Optional, Union
15
+
16
16
  import httpx
17
17
  from pydantic_ai import RunContext
18
-
19
- from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP, CallToolFunc, ToolResult
18
+ from pydantic_ai.mcp import (
19
+ CallToolFunc,
20
+ MCPServerSSE,
21
+ MCPServerStdio,
22
+ MCPServerStreamableHTTP,
23
+ ToolResult,
24
+ )
20
25
 
21
26
  from code_puppy.http_utils import create_async_client
22
- from code_puppy.messaging import emit_info
23
27
  from code_puppy.mcp.blocking_startup import BlockingMCPServerStdio
28
+ from code_puppy.messaging import emit_info
24
29
 
25
30
  # Configure logging
26
31
  logger = logging.getLogger(__name__)
@@ -28,6 +33,7 @@ logger = logging.getLogger(__name__)
28
33
 
29
34
  class ServerState(Enum):
30
35
  """Enumeration of possible server states."""
36
+
31
37
  STOPPED = "stopped"
32
38
  STARTING = "starting"
33
39
  RUNNING = "running"
@@ -39,6 +45,7 @@ class ServerState(Enum):
39
45
  @dataclass
40
46
  class ServerConfig:
41
47
  """Configuration for an MCP server."""
48
+
42
49
  id: str
43
50
  name: str
44
51
  type: str # "sse", "stdio", or "http"
@@ -58,45 +65,41 @@ async def process_tool_call(
58
65
  f"\n[bold white on purple] MCP Tool Call - {name}[/bold white on purple]",
59
66
  message_group=group_id,
60
67
  )
61
- emit_info(
62
- "\nArgs:",
63
- message_group=group_id
64
- )
65
- emit_info(
66
- json.dumps(tool_args, indent=2),
67
- message_group=group_id
68
- )
69
- return await call_tool(name, tool_args, {'deps': ctx.deps})
68
+ emit_info("\nArgs:", message_group=group_id)
69
+ emit_info(json.dumps(tool_args, indent=2), message_group=group_id)
70
+ return await call_tool(name, tool_args, {"deps": ctx.deps})
70
71
 
71
72
 
72
73
  class ManagedMCPServer:
73
74
  """
74
75
  Managed wrapper around pydantic-ai MCP server classes.
75
-
76
+
76
77
  This class provides management capabilities like enable/disable,
77
78
  quarantine, and status tracking while maintaining 100% compatibility
78
79
  with the existing Agent interface through get_pydantic_server().
79
-
80
+
80
81
  Example usage:
81
82
  config = ServerConfig(
82
- id="123",
83
- name="test",
84
- type="sse",
83
+ id="123",
84
+ name="test",
85
+ type="sse",
85
86
  config={"url": "http://localhost:8080"}
86
87
  )
87
88
  managed = ManagedMCPServer(config)
88
89
  pydantic_server = managed.get_pydantic_server() # Returns actual MCPServerSSE
89
90
  """
90
-
91
+
91
92
  def __init__(self, server_config: ServerConfig):
92
93
  """
93
94
  Initialize managed server with configuration.
94
-
95
+
95
96
  Args:
96
97
  server_config: Server configuration containing type, connection details, etc.
97
98
  """
98
99
  self.config = server_config
99
- self._pydantic_server: Optional[Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]] = None
100
+ self._pydantic_server: Optional[
101
+ Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]
102
+ ] = None
100
103
  self._state = ServerState.STOPPED
101
104
  # Always start disabled - servers must be explicitly started with /mcp start
102
105
  self._enabled = False
@@ -104,7 +107,7 @@ class ManagedMCPServer:
104
107
  self._start_time: Optional[datetime] = None
105
108
  self._stop_time: Optional[datetime] = None
106
109
  self._error_message: Optional[str] = None
107
-
110
+
108
111
  # Initialize the pydantic server
109
112
  try:
110
113
  self._create_server()
@@ -114,49 +117,51 @@ class ManagedMCPServer:
114
117
  logger.error(f"Failed to create server {self.config.name}: {e}")
115
118
  self._state = ServerState.ERROR
116
119
  self._error_message = str(e)
117
-
118
- def get_pydantic_server(self) -> Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]:
120
+
121
+ def get_pydantic_server(
122
+ self,
123
+ ) -> Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]:
119
124
  """
120
125
  Get the actual pydantic-ai server instance.
121
-
126
+
122
127
  This method returns the real pydantic-ai MCP server objects for 100% compatibility
123
128
  with the existing Agent interface. Do not return custom classes or proxies.
124
-
129
+
125
130
  Returns:
126
131
  Actual pydantic-ai MCP server instance (MCPServerSSE, MCPServerStdio, or MCPServerStreamableHTTP)
127
-
132
+
128
133
  Raises:
129
134
  RuntimeError: If server creation failed or server is not available
130
135
  """
131
136
  if self._pydantic_server is None:
132
137
  raise RuntimeError(f"Server {self.config.name} is not available")
133
-
138
+
134
139
  if not self.is_enabled() or self.is_quarantined():
135
140
  raise RuntimeError(f"Server {self.config.name} is disabled or quarantined")
136
-
141
+
137
142
  return self._pydantic_server
138
-
143
+
139
144
  def _create_server(self) -> None:
140
145
  """
141
146
  Create appropriate pydantic-ai server based on config type.
142
-
147
+
143
148
  Raises:
144
149
  ValueError: If server type is unsupported or config is invalid
145
150
  Exception: If server creation fails
146
151
  """
147
152
  server_type = self.config.type.lower()
148
153
  config = self.config.config
149
-
154
+
150
155
  try:
151
156
  if server_type == "sse":
152
157
  if "url" not in config:
153
158
  raise ValueError("SSE server requires 'url' in config")
154
-
159
+
155
160
  # Prepare arguments for MCPServerSSE
156
161
  sse_kwargs = {
157
162
  "url": config["url"],
158
163
  }
159
-
164
+
160
165
  # Add optional parameters if provided
161
166
  if "timeout" in config:
162
167
  sse_kwargs["timeout"] = config["timeout"]
@@ -167,26 +172,25 @@ class ManagedMCPServer:
167
172
  elif config.get("headers"):
168
173
  # Create HTTP client if headers are provided but no client specified
169
174
  sse_kwargs["http_client"] = self._get_http_client()
170
-
171
- self._pydantic_server = MCPServerSSE(**sse_kwargs, process_tool_call=process_tool_call)
172
-
175
+
176
+ self._pydantic_server = MCPServerSSE(
177
+ **sse_kwargs, process_tool_call=process_tool_call
178
+ )
179
+
173
180
  elif server_type == "stdio":
174
181
  if "command" not in config:
175
182
  raise ValueError("Stdio server requires 'command' in config")
176
-
183
+
177
184
  # Handle command and arguments
178
185
  command = config["command"]
179
186
  args = config.get("args", [])
180
187
  if isinstance(args, str):
181
188
  # If args is a string, split it
182
189
  args = args.split()
183
-
190
+
184
191
  # Prepare arguments for MCPServerStdio
185
- stdio_kwargs = {
186
- "command": command,
187
- "args": list(args) if args else []
188
- }
189
-
192
+ stdio_kwargs = {"command": command, "args": list(args) if args else []}
193
+
190
194
  # Add optional parameters if provided
191
195
  if "env" in config:
192
196
  stdio_kwargs["env"] = config["env"]
@@ -196,27 +200,27 @@ class ManagedMCPServer:
196
200
  stdio_kwargs["timeout"] = config["timeout"]
197
201
  if "read_timeout" in config:
198
202
  stdio_kwargs["read_timeout"] = config["read_timeout"]
199
-
203
+
200
204
  # Use BlockingMCPServerStdio for proper initialization blocking and stderr capture
201
205
  # Create a unique message group for this server
202
206
  message_group = uuid.uuid4()
203
207
  self._pydantic_server = BlockingMCPServerStdio(
204
- **stdio_kwargs,
205
- process_tool_call=process_tool_call,
208
+ **stdio_kwargs,
209
+ process_tool_call=process_tool_call,
206
210
  tool_prefix=config["name"],
207
211
  emit_stderr=True, # Always emit stderr for now
208
- message_group=message_group
212
+ message_group=message_group,
209
213
  )
210
-
214
+
211
215
  elif server_type == "http":
212
216
  if "url" not in config:
213
217
  raise ValueError("HTTP server requires 'url' in config")
214
-
218
+
215
219
  # Prepare arguments for MCPServerStreamableHTTP
216
220
  http_kwargs = {
217
221
  "url": config["url"],
218
222
  }
219
-
223
+
220
224
  # Add optional parameters if provided
221
225
  if "timeout" in config:
222
226
  http_kwargs["timeout"] = config["timeout"]
@@ -227,33 +231,34 @@ class ManagedMCPServer:
227
231
  elif config.get("headers"):
228
232
  # Create HTTP client if headers are provided but no client specified
229
233
  http_kwargs["http_client"] = self._get_http_client()
230
-
231
- self._pydantic_server = MCPServerStreamableHTTP(**http_kwargs, process_tool_call=process_tool_call)
232
-
234
+
235
+ self._pydantic_server = MCPServerStreamableHTTP(
236
+ **http_kwargs, process_tool_call=process_tool_call
237
+ )
238
+
233
239
  else:
234
240
  raise ValueError(f"Unsupported server type: {server_type}")
235
-
241
+
236
242
  logger.info(f"Created {server_type} server: {self.config.name}")
237
-
243
+
238
244
  except Exception as e:
239
- logger.error(f"Failed to create {server_type} server {self.config.name}: {e}")
245
+ logger.error(
246
+ f"Failed to create {server_type} server {self.config.name}: {e}"
247
+ )
240
248
  raise
241
-
249
+
242
250
  def _get_http_client(self) -> httpx.AsyncClient:
243
251
  """
244
252
  Create httpx.AsyncClient with headers from config.
245
-
253
+
246
254
  Returns:
247
255
  Configured async HTTP client with custom headers
248
256
  """
249
257
  headers = self.config.config.get("headers", {})
250
258
  timeout = self.config.config.get("timeout", 30)
251
- client = create_async_client(
252
- headers=headers,
253
- timeout=timeout
254
- )
259
+ client = create_async_client(headers=headers, timeout=timeout)
255
260
  return client
256
-
261
+
257
262
  def enable(self) -> None:
258
263
  """Enable server availability."""
259
264
  self._enabled = True
@@ -261,7 +266,7 @@ class ManagedMCPServer:
261
266
  self._state = ServerState.RUNNING
262
267
  self._start_time = datetime.now()
263
268
  logger.info(f"Enabled server: {self.config.name}")
264
-
269
+
265
270
  def disable(self) -> None:
266
271
  """Disable server availability."""
267
272
  self._enabled = False
@@ -269,20 +274,20 @@ class ManagedMCPServer:
269
274
  self._state = ServerState.STOPPED
270
275
  self._stop_time = datetime.now()
271
276
  logger.info(f"Disabled server: {self.config.name}")
272
-
277
+
273
278
  def is_enabled(self) -> bool:
274
279
  """
275
280
  Check if server is enabled.
276
-
281
+
277
282
  Returns:
278
283
  True if server is enabled, False otherwise
279
284
  """
280
285
  return self._enabled
281
-
286
+
282
287
  def quarantine(self, duration: int) -> None:
283
288
  """
284
289
  Temporarily disable server for specified duration.
285
-
290
+
286
291
  Args:
287
292
  duration: Quarantine duration in seconds
288
293
  """
@@ -293,46 +298,48 @@ class ManagedMCPServer:
293
298
  f"Quarantined server {self.config.name} for {duration} seconds "
294
299
  f"(was {previous_state.value})"
295
300
  )
296
-
301
+
297
302
  def is_quarantined(self) -> bool:
298
303
  """
299
304
  Check if server is currently quarantined.
300
-
305
+
301
306
  Returns:
302
307
  True if server is quarantined, False otherwise
303
308
  """
304
309
  if self._quarantine_until is None:
305
310
  return False
306
-
311
+
307
312
  if datetime.now() >= self._quarantine_until:
308
313
  # Quarantine period has expired
309
314
  self._quarantine_until = None
310
315
  if self._state == ServerState.QUARANTINED:
311
316
  # Restore to running state if enabled
312
- self._state = ServerState.RUNNING if self._enabled else ServerState.STOPPED
317
+ self._state = (
318
+ ServerState.RUNNING if self._enabled else ServerState.STOPPED
319
+ )
313
320
  logger.info(f"Released quarantine for server: {self.config.name}")
314
321
  return False
315
-
322
+
316
323
  return True
317
-
324
+
318
325
  def get_captured_stderr(self) -> list[str]:
319
326
  """
320
327
  Get captured stderr output if this is a stdio server.
321
-
328
+
322
329
  Returns:
323
330
  List of captured stderr lines, or empty list if not applicable
324
331
  """
325
332
  if isinstance(self._pydantic_server, BlockingMCPServerStdio):
326
333
  return self._pydantic_server.get_captured_stderr()
327
334
  return []
328
-
335
+
329
336
  async def wait_until_ready(self, timeout: float = 30.0) -> bool:
330
337
  """
331
338
  Wait until the server is ready.
332
-
339
+
333
340
  Args:
334
341
  timeout: Maximum time to wait in seconds
335
-
342
+
336
343
  Returns:
337
344
  True if server is ready, False otherwise
338
345
  """
@@ -344,25 +351,25 @@ class ManagedMCPServer:
344
351
  return False
345
352
  # Non-stdio servers are considered ready immediately
346
353
  return True
347
-
354
+
348
355
  async def ensure_ready(self, timeout: float = 30.0):
349
356
  """
350
357
  Ensure server is ready, raising exception if not.
351
-
358
+
352
359
  Args:
353
360
  timeout: Maximum time to wait in seconds
354
-
361
+
355
362
  Raises:
356
363
  TimeoutError: If server doesn't initialize within timeout
357
364
  Exception: If server initialization failed
358
365
  """
359
366
  if isinstance(self._pydantic_server, BlockingMCPServerStdio):
360
367
  await self._pydantic_server.ensure_ready(timeout)
361
-
368
+
362
369
  def get_status(self) -> Dict[str, Any]:
363
370
  """
364
371
  Return current status information.
365
-
372
+
366
373
  Returns:
367
374
  Dictionary containing comprehensive status information
368
375
  """
@@ -370,11 +377,11 @@ class ManagedMCPServer:
370
377
  uptime = None
371
378
  if self._start_time and self._state == ServerState.RUNNING:
372
379
  uptime = (now - self._start_time).total_seconds()
373
-
380
+
374
381
  quarantine_remaining = None
375
382
  if self.is_quarantined():
376
383
  quarantine_remaining = (self._quarantine_until - now).total_seconds()
377
-
384
+
378
385
  return {
379
386
  "id": self.config.id,
380
387
  "name": self.config.name,
@@ -389,9 +396,9 @@ class ManagedMCPServer:
389
396
  "error_message": self._error_message,
390
397
  "config": self.config.config.copy(), # Copy to prevent modification
391
398
  "server_available": (
392
- self._pydantic_server is not None and
393
- self._enabled and
394
- not self.is_quarantined() and
395
- self._state == ServerState.RUNNING
396
- )
397
- }
399
+ self._pydantic_server is not None
400
+ and self._enabled
401
+ and not self.is_quarantined()
402
+ and self._state == ServerState.RUNNING
403
+ ),
404
+ }