codepp 0.0.437__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/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -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 +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ManagedMCPServer wrapper class implementation.
|
|
3
|
+
|
|
4
|
+
This module provides a managed wrapper around pydantic-ai MCP server classes
|
|
5
|
+
that adds management capabilities while maintaining 100% compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Dict, Optional, Union
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from pydantic_ai import RunContext
|
|
17
|
+
from pydantic_ai.mcp import (
|
|
18
|
+
CallToolFunc,
|
|
19
|
+
MCPServerSSE,
|
|
20
|
+
MCPServerStdio,
|
|
21
|
+
MCPServerStreamableHTTP,
|
|
22
|
+
ToolResult,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from code_puppy.http_utils import create_async_client
|
|
26
|
+
from code_puppy.mcp_.blocking_startup import BlockingMCPServerStdio
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _expand_env_vars(value: Any) -> Any:
|
|
30
|
+
"""
|
|
31
|
+
Recursively expand environment variables in config values.
|
|
32
|
+
|
|
33
|
+
Supports $VAR and ${VAR} syntax. Works with:
|
|
34
|
+
- Strings: expands env vars
|
|
35
|
+
- Dicts: recursively expands all string values
|
|
36
|
+
- Lists: recursively expands all string elements
|
|
37
|
+
- Other types: returned as-is
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
value: The value to expand env vars in
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The value with env vars expanded
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(value, str):
|
|
46
|
+
return os.path.expandvars(value)
|
|
47
|
+
elif isinstance(value, dict):
|
|
48
|
+
return {k: _expand_env_vars(v) for k, v in value.items()}
|
|
49
|
+
elif isinstance(value, list):
|
|
50
|
+
return [_expand_env_vars(item) for item in value]
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ServerState(Enum):
|
|
55
|
+
"""Enumeration of possible server states."""
|
|
56
|
+
|
|
57
|
+
STOPPED = "stopped"
|
|
58
|
+
STARTING = "starting"
|
|
59
|
+
RUNNING = "running"
|
|
60
|
+
STOPPING = "stopping"
|
|
61
|
+
ERROR = "error"
|
|
62
|
+
QUARANTINED = "quarantined"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ServerConfig:
|
|
67
|
+
"""Configuration for an MCP server."""
|
|
68
|
+
|
|
69
|
+
id: str
|
|
70
|
+
name: str
|
|
71
|
+
type: str # "sse", "stdio", or "http"
|
|
72
|
+
enabled: bool = True
|
|
73
|
+
config: Dict = field(default_factory=dict) # Raw config from JSON
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def process_tool_call(
|
|
77
|
+
ctx: RunContext[Any],
|
|
78
|
+
call_tool: CallToolFunc,
|
|
79
|
+
name: str,
|
|
80
|
+
tool_args: dict[str, Any],
|
|
81
|
+
) -> ToolResult:
|
|
82
|
+
"""A tool call processor that passes along the deps."""
|
|
83
|
+
from rich.console import Console
|
|
84
|
+
|
|
85
|
+
from code_puppy.config import get_banner_color
|
|
86
|
+
|
|
87
|
+
console = Console()
|
|
88
|
+
color = get_banner_color("mcp_tool_call")
|
|
89
|
+
banner = f"[bold white on {color}] MCP TOOL CALL [/bold white on {color}]"
|
|
90
|
+
console.print(f"\n{banner} 🔧 [bold cyan]{name}[/bold cyan]")
|
|
91
|
+
return await call_tool(name, tool_args, {"deps": ctx.deps})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ManagedMCPServer:
|
|
95
|
+
"""
|
|
96
|
+
Managed wrapper around pydantic-ai MCP server classes.
|
|
97
|
+
|
|
98
|
+
This class provides management capabilities like enable/disable,
|
|
99
|
+
quarantine, and status tracking while maintaining 100% compatibility
|
|
100
|
+
with the existing Agent interface through get_pydantic_server().
|
|
101
|
+
|
|
102
|
+
Example usage:
|
|
103
|
+
config = ServerConfig(
|
|
104
|
+
id="123",
|
|
105
|
+
name="test",
|
|
106
|
+
type="sse",
|
|
107
|
+
config={"url": "http://localhost:8080"}
|
|
108
|
+
)
|
|
109
|
+
managed = ManagedMCPServer(config)
|
|
110
|
+
pydantic_server = managed.get_pydantic_server() # Returns actual MCPServerSSE
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, server_config: ServerConfig):
|
|
114
|
+
"""
|
|
115
|
+
Initialize managed server with configuration.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
server_config: Server configuration containing type, connection details, etc.
|
|
119
|
+
"""
|
|
120
|
+
self.config = server_config
|
|
121
|
+
self._pydantic_server: Optional[
|
|
122
|
+
Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]
|
|
123
|
+
] = None
|
|
124
|
+
self._state = ServerState.STOPPED
|
|
125
|
+
# Always start disabled - servers must be explicitly started with /mcp start
|
|
126
|
+
self._enabled = False
|
|
127
|
+
self._quarantine_until: Optional[datetime] = None
|
|
128
|
+
self._start_time: Optional[datetime] = None
|
|
129
|
+
self._stop_time: Optional[datetime] = None
|
|
130
|
+
self._error_message: Optional[str] = None
|
|
131
|
+
|
|
132
|
+
# Initialize the pydantic server
|
|
133
|
+
try:
|
|
134
|
+
self._create_server()
|
|
135
|
+
# Always start as STOPPED - servers must be explicitly started
|
|
136
|
+
self._state = ServerState.STOPPED
|
|
137
|
+
except Exception as e:
|
|
138
|
+
self._state = ServerState.ERROR
|
|
139
|
+
self._error_message = str(e)
|
|
140
|
+
|
|
141
|
+
def get_pydantic_server(
|
|
142
|
+
self,
|
|
143
|
+
) -> Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]:
|
|
144
|
+
"""
|
|
145
|
+
Get the actual pydantic-ai server instance.
|
|
146
|
+
|
|
147
|
+
This method returns the real pydantic-ai MCP server objects for 100% compatibility
|
|
148
|
+
with the existing Agent interface. Do not return custom classes or proxies.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Actual pydantic-ai MCP server instance (MCPServerSSE, MCPServerStdio, or MCPServerStreamableHTTP)
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
RuntimeError: If server creation failed or server is not available
|
|
155
|
+
"""
|
|
156
|
+
if self._pydantic_server is None:
|
|
157
|
+
raise RuntimeError(f"Server {self.config.name} is not available")
|
|
158
|
+
|
|
159
|
+
if not self.is_enabled() or self.is_quarantined():
|
|
160
|
+
raise RuntimeError(f"Server {self.config.name} is disabled or quarantined")
|
|
161
|
+
|
|
162
|
+
return self._pydantic_server
|
|
163
|
+
|
|
164
|
+
def _create_server(self) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Create appropriate pydantic-ai server based on config type.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
ValueError: If server type is unsupported or config is invalid
|
|
170
|
+
Exception: If server creation fails
|
|
171
|
+
"""
|
|
172
|
+
server_type = self.config.type.lower()
|
|
173
|
+
config = self.config.config
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
if server_type == "sse":
|
|
177
|
+
if "url" not in config:
|
|
178
|
+
raise ValueError("SSE server requires 'url' in config")
|
|
179
|
+
|
|
180
|
+
# Prepare arguments for MCPServerSSE (expand env vars in URL)
|
|
181
|
+
sse_kwargs = {
|
|
182
|
+
"url": _expand_env_vars(config["url"]),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Add optional parameters if provided
|
|
186
|
+
if "timeout" in config:
|
|
187
|
+
sse_kwargs["timeout"] = config["timeout"]
|
|
188
|
+
if "read_timeout" in config:
|
|
189
|
+
sse_kwargs["read_timeout"] = config["read_timeout"]
|
|
190
|
+
if "http_client" in config:
|
|
191
|
+
sse_kwargs["http_client"] = config["http_client"]
|
|
192
|
+
elif config.get("headers"):
|
|
193
|
+
# Create HTTP client if headers are provided but no client specified
|
|
194
|
+
sse_kwargs["http_client"] = self._get_http_client()
|
|
195
|
+
|
|
196
|
+
self._pydantic_server = MCPServerSSE(
|
|
197
|
+
**sse_kwargs, process_tool_call=process_tool_call
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
elif server_type == "stdio":
|
|
201
|
+
if "command" not in config:
|
|
202
|
+
raise ValueError("Stdio server requires 'command' in config")
|
|
203
|
+
|
|
204
|
+
# Handle command and arguments (expand env vars)
|
|
205
|
+
command = _expand_env_vars(config["command"])
|
|
206
|
+
args = config.get("args", [])
|
|
207
|
+
if isinstance(args, str):
|
|
208
|
+
# If args is a string, split it then expand
|
|
209
|
+
args = [_expand_env_vars(a) for a in args.split()]
|
|
210
|
+
else:
|
|
211
|
+
args = _expand_env_vars(args)
|
|
212
|
+
|
|
213
|
+
# Prepare arguments for MCPServerStdio
|
|
214
|
+
stdio_kwargs = {"command": command, "args": list(args) if args else []}
|
|
215
|
+
|
|
216
|
+
# Add optional parameters if provided (expand env vars in env and cwd)
|
|
217
|
+
if "env" in config:
|
|
218
|
+
stdio_kwargs["env"] = _expand_env_vars(config["env"])
|
|
219
|
+
if "cwd" in config:
|
|
220
|
+
stdio_kwargs["cwd"] = _expand_env_vars(config["cwd"])
|
|
221
|
+
# Default timeout of 60s for stdio servers - some servers like Serena take a while to start
|
|
222
|
+
# Users can override this in their config
|
|
223
|
+
stdio_kwargs["timeout"] = config.get("timeout", 60)
|
|
224
|
+
if "read_timeout" in config:
|
|
225
|
+
stdio_kwargs["read_timeout"] = config["read_timeout"]
|
|
226
|
+
|
|
227
|
+
# Use BlockingMCPServerStdio for proper initialization blocking and stderr capture
|
|
228
|
+
# Create a unique message group for this server
|
|
229
|
+
message_group = uuid.uuid4()
|
|
230
|
+
self._pydantic_server = BlockingMCPServerStdio(
|
|
231
|
+
**stdio_kwargs,
|
|
232
|
+
process_tool_call=process_tool_call,
|
|
233
|
+
tool_prefix=self.config.name,
|
|
234
|
+
emit_stderr=False, # Logs go to file, not console (use /mcp logs to view)
|
|
235
|
+
message_group=message_group,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
elif server_type == "http":
|
|
239
|
+
if "url" not in config:
|
|
240
|
+
raise ValueError("HTTP server requires 'url' in config")
|
|
241
|
+
|
|
242
|
+
# Prepare arguments for MCPServerStreamableHTTP (expand env vars in URL)
|
|
243
|
+
http_kwargs = {
|
|
244
|
+
"url": _expand_env_vars(config["url"]),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Add optional parameters if provided
|
|
248
|
+
if "timeout" in config:
|
|
249
|
+
http_kwargs["timeout"] = config["timeout"]
|
|
250
|
+
if "read_timeout" in config:
|
|
251
|
+
http_kwargs["read_timeout"] = config["read_timeout"]
|
|
252
|
+
|
|
253
|
+
# Pass headers directly instead of creating http_client
|
|
254
|
+
# Note: There's a bug in MCP 1.25.0 where passing http_client
|
|
255
|
+
# causes "'_AsyncGeneratorContextManager' object has no attribute 'stream'"
|
|
256
|
+
# The workaround is to pass headers directly and let pydantic-ai
|
|
257
|
+
# create the http_client internally.
|
|
258
|
+
if config.get("headers"):
|
|
259
|
+
# Expand environment variables in headers
|
|
260
|
+
http_kwargs["headers"] = _expand_env_vars(config["headers"])
|
|
261
|
+
|
|
262
|
+
self._pydantic_server = MCPServerStreamableHTTP(
|
|
263
|
+
**http_kwargs, process_tool_call=process_tool_call
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
else:
|
|
267
|
+
raise ValueError(f"Unsupported server type: {server_type}")
|
|
268
|
+
|
|
269
|
+
except Exception:
|
|
270
|
+
raise
|
|
271
|
+
|
|
272
|
+
def _get_http_client(self) -> httpx.AsyncClient:
|
|
273
|
+
"""
|
|
274
|
+
Create httpx.AsyncClient with headers from config.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Configured async HTTP client with custom headers
|
|
278
|
+
"""
|
|
279
|
+
headers = self.config.config.get("headers", {})
|
|
280
|
+
|
|
281
|
+
# Expand environment variables in headers
|
|
282
|
+
resolved_headers = {}
|
|
283
|
+
if isinstance(headers, dict):
|
|
284
|
+
for k, v in headers.items():
|
|
285
|
+
if isinstance(v, str):
|
|
286
|
+
resolved_headers[k] = os.path.expandvars(v)
|
|
287
|
+
else:
|
|
288
|
+
resolved_headers[k] = v
|
|
289
|
+
|
|
290
|
+
timeout = self.config.config.get("timeout", 30)
|
|
291
|
+
client = create_async_client(headers=resolved_headers, timeout=timeout)
|
|
292
|
+
return client
|
|
293
|
+
|
|
294
|
+
def enable(self) -> None:
|
|
295
|
+
"""Enable server availability."""
|
|
296
|
+
self._enabled = True
|
|
297
|
+
if self._state == ServerState.STOPPED and self._pydantic_server is not None:
|
|
298
|
+
self._state = ServerState.RUNNING
|
|
299
|
+
self._start_time = datetime.now()
|
|
300
|
+
|
|
301
|
+
def disable(self) -> None:
|
|
302
|
+
"""Disable server availability."""
|
|
303
|
+
self._enabled = False
|
|
304
|
+
if self._state == ServerState.RUNNING:
|
|
305
|
+
self._state = ServerState.STOPPED
|
|
306
|
+
self._stop_time = datetime.now()
|
|
307
|
+
|
|
308
|
+
def is_enabled(self) -> bool:
|
|
309
|
+
"""
|
|
310
|
+
Check if server is enabled.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
True if server is enabled, False otherwise
|
|
314
|
+
"""
|
|
315
|
+
return self._enabled
|
|
316
|
+
|
|
317
|
+
def quarantine(self, duration: int) -> None:
|
|
318
|
+
"""
|
|
319
|
+
Temporarily disable server for specified duration.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
duration: Quarantine duration in seconds
|
|
323
|
+
"""
|
|
324
|
+
self._quarantine_until = datetime.now() + timedelta(seconds=duration)
|
|
325
|
+
self._state = ServerState.QUARANTINED
|
|
326
|
+
|
|
327
|
+
def is_quarantined(self) -> bool:
|
|
328
|
+
"""
|
|
329
|
+
Check if server is currently quarantined.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
True if server is quarantined, False otherwise
|
|
333
|
+
"""
|
|
334
|
+
if self._quarantine_until is None:
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
if datetime.now() >= self._quarantine_until:
|
|
338
|
+
# Quarantine period has expired
|
|
339
|
+
self._quarantine_until = None
|
|
340
|
+
if self._state == ServerState.QUARANTINED:
|
|
341
|
+
# Restore to running state if enabled
|
|
342
|
+
self._state = (
|
|
343
|
+
ServerState.RUNNING if self._enabled else ServerState.STOPPED
|
|
344
|
+
)
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
def get_captured_stderr(self) -> list[str]:
|
|
350
|
+
"""
|
|
351
|
+
Get captured stderr output if this is a stdio server.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
List of captured stderr lines, or empty list if not applicable
|
|
355
|
+
"""
|
|
356
|
+
if isinstance(self._pydantic_server, BlockingMCPServerStdio):
|
|
357
|
+
return self._pydantic_server.get_captured_stderr()
|
|
358
|
+
return []
|
|
359
|
+
|
|
360
|
+
async def wait_until_ready(self, timeout: float = 30.0) -> bool:
|
|
361
|
+
"""
|
|
362
|
+
Wait until the server is ready.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
timeout: Maximum time to wait in seconds
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
True if server is ready, False otherwise
|
|
369
|
+
"""
|
|
370
|
+
if isinstance(self._pydantic_server, BlockingMCPServerStdio):
|
|
371
|
+
try:
|
|
372
|
+
await self._pydantic_server.wait_until_ready(timeout)
|
|
373
|
+
return True
|
|
374
|
+
except Exception:
|
|
375
|
+
return False
|
|
376
|
+
# Non-stdio servers are considered ready immediately
|
|
377
|
+
return True
|
|
378
|
+
|
|
379
|
+
async def ensure_ready(self, timeout: float = 30.0):
|
|
380
|
+
"""
|
|
381
|
+
Ensure server is ready, raising exception if not.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
timeout: Maximum time to wait in seconds
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
TimeoutError: If server doesn't initialize within timeout
|
|
388
|
+
Exception: If server initialization failed
|
|
389
|
+
"""
|
|
390
|
+
if isinstance(self._pydantic_server, BlockingMCPServerStdio):
|
|
391
|
+
await self._pydantic_server.ensure_ready(timeout)
|
|
392
|
+
|
|
393
|
+
def get_status(self) -> Dict[str, Any]:
|
|
394
|
+
"""
|
|
395
|
+
Return current status information.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dictionary containing comprehensive status information
|
|
399
|
+
"""
|
|
400
|
+
now = datetime.now()
|
|
401
|
+
uptime = None
|
|
402
|
+
if self._start_time and self._state == ServerState.RUNNING:
|
|
403
|
+
uptime = (now - self._start_time).total_seconds()
|
|
404
|
+
|
|
405
|
+
quarantine_remaining = None
|
|
406
|
+
if self.is_quarantined():
|
|
407
|
+
quarantine_remaining = (self._quarantine_until - now).total_seconds()
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
"id": self.config.id,
|
|
411
|
+
"name": self.config.name,
|
|
412
|
+
"type": self.config.type,
|
|
413
|
+
"state": self._state.value,
|
|
414
|
+
"enabled": self._enabled,
|
|
415
|
+
"quarantined": self.is_quarantined(),
|
|
416
|
+
"quarantine_remaining_seconds": quarantine_remaining,
|
|
417
|
+
"uptime_seconds": uptime,
|
|
418
|
+
"start_time": self._start_time.isoformat() if self._start_time else None,
|
|
419
|
+
"stop_time": self._stop_time.isoformat() if self._stop_time else None,
|
|
420
|
+
"error_message": self._error_message,
|
|
421
|
+
"config": self.config.config.copy(), # Copy to prevent modification
|
|
422
|
+
"server_available": (
|
|
423
|
+
self._pydantic_server is not None
|
|
424
|
+
and self._enabled
|
|
425
|
+
and not self.is_quarantined()
|
|
426
|
+
and self._state == ServerState.RUNNING
|
|
427
|
+
),
|
|
428
|
+
}
|