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,469 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server with blocking startup capability and stderr capture.
|
|
3
|
+
|
|
4
|
+
This module provides MCP servers that:
|
|
5
|
+
1. Capture stderr output from stdio servers to persistent log files
|
|
6
|
+
2. Block until fully initialized before allowing operations
|
|
7
|
+
3. Optionally emit stderr to users (disabled by default to reduce console noise)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
import threading
|
|
13
|
+
import uuid
|
|
14
|
+
from collections import deque
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from typing import List, Optional
|
|
17
|
+
|
|
18
|
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
19
|
+
from pydantic_ai.mcp import MCPServerStdio
|
|
20
|
+
|
|
21
|
+
from code_puppy.mcp_.mcp_logs import get_log_file_path, rotate_log_if_needed, write_log
|
|
22
|
+
from code_puppy.messaging import emit_info
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StderrFileCapture:
|
|
26
|
+
"""
|
|
27
|
+
Captures stderr to a persistent log file and optionally monitors it.
|
|
28
|
+
|
|
29
|
+
Logs are written to ~/.code_puppy/mcp_logs/<server_name>.log
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
server_name: str,
|
|
35
|
+
emit_to_user: bool = False, # Disabled by default to reduce console noise
|
|
36
|
+
message_group: Optional[uuid.UUID] = None,
|
|
37
|
+
):
|
|
38
|
+
self.server_name = server_name
|
|
39
|
+
self.emit_to_user = emit_to_user
|
|
40
|
+
self.message_group = message_group or uuid.uuid4()
|
|
41
|
+
self.log_file = None
|
|
42
|
+
self.log_path = None
|
|
43
|
+
self.monitor_thread = None
|
|
44
|
+
self.stop_monitoring = threading.Event()
|
|
45
|
+
self.captured_lines: deque = deque(maxlen=1000)
|
|
46
|
+
self._last_read_pos = 0
|
|
47
|
+
|
|
48
|
+
def start(self):
|
|
49
|
+
"""Start capture by opening persistent log file and monitor thread."""
|
|
50
|
+
# Rotate log if needed
|
|
51
|
+
rotate_log_if_needed(self.server_name)
|
|
52
|
+
|
|
53
|
+
# Get persistent log path
|
|
54
|
+
self.log_path = get_log_file_path(self.server_name)
|
|
55
|
+
|
|
56
|
+
# Write startup marker
|
|
57
|
+
write_log(self.server_name, "--- Server starting ---", "INFO")
|
|
58
|
+
|
|
59
|
+
# Start monitoring thread only if we need to emit to user or capture lines
|
|
60
|
+
try:
|
|
61
|
+
# Open log file for appending stderr (inside try for proper cleanup)
|
|
62
|
+
self.log_file = open(self.log_path, "a", encoding="utf-8")
|
|
63
|
+
self.stop_monitoring.clear()
|
|
64
|
+
self.monitor_thread = threading.Thread(target=self._monitor_file)
|
|
65
|
+
self.monitor_thread.daemon = True
|
|
66
|
+
self.monitor_thread.start()
|
|
67
|
+
except Exception:
|
|
68
|
+
if self.log_file is not None:
|
|
69
|
+
self.log_file.close()
|
|
70
|
+
self.log_file = None
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
return self.log_file
|
|
74
|
+
|
|
75
|
+
def _monitor_file(self):
|
|
76
|
+
"""Monitor the log file for new content."""
|
|
77
|
+
if not self.log_path:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
# Start reading from current position (end of file before we started)
|
|
82
|
+
try:
|
|
83
|
+
self._last_read_pos = os.path.getsize(self.log_path)
|
|
84
|
+
except OSError:
|
|
85
|
+
self._last_read_pos = 0
|
|
86
|
+
|
|
87
|
+
while not self.stop_monitoring.is_set():
|
|
88
|
+
try:
|
|
89
|
+
with open(
|
|
90
|
+
self.log_path, "r", encoding="utf-8", errors="replace"
|
|
91
|
+
) as f:
|
|
92
|
+
f.seek(self._last_read_pos)
|
|
93
|
+
new_content = f.read()
|
|
94
|
+
if new_content:
|
|
95
|
+
self._last_read_pos = f.tell()
|
|
96
|
+
# Process new lines
|
|
97
|
+
for line in new_content.splitlines():
|
|
98
|
+
if line.strip():
|
|
99
|
+
self.captured_lines.append(line)
|
|
100
|
+
if self.emit_to_user:
|
|
101
|
+
emit_info(
|
|
102
|
+
f"MCP {self.server_name}: {line}",
|
|
103
|
+
message_group=self.message_group,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
except Exception:
|
|
107
|
+
pass # File might not exist yet or be deleted
|
|
108
|
+
|
|
109
|
+
self.stop_monitoring.wait(0.1) # Check every 100ms
|
|
110
|
+
finally:
|
|
111
|
+
if self.log_file is not None:
|
|
112
|
+
try:
|
|
113
|
+
self.log_file.close()
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
self.log_file = None
|
|
117
|
+
|
|
118
|
+
def stop(self):
|
|
119
|
+
"""Stop monitoring and clean up."""
|
|
120
|
+
self.stop_monitoring.set()
|
|
121
|
+
if self.monitor_thread:
|
|
122
|
+
self.monitor_thread.join(timeout=1)
|
|
123
|
+
|
|
124
|
+
if self.log_file:
|
|
125
|
+
try:
|
|
126
|
+
self.log_file.flush()
|
|
127
|
+
self.log_file.close()
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
# Write shutdown marker
|
|
132
|
+
write_log(self.server_name, "--- Server stopped ---", "INFO")
|
|
133
|
+
|
|
134
|
+
# Read any remaining content for in-memory capture
|
|
135
|
+
if self.log_path and os.path.exists(self.log_path):
|
|
136
|
+
try:
|
|
137
|
+
with open(self.log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
138
|
+
f.seek(self._last_read_pos)
|
|
139
|
+
content = f.read()
|
|
140
|
+
for line in content.splitlines():
|
|
141
|
+
if line.strip() and line not in self.captured_lines:
|
|
142
|
+
self.captured_lines.append(line)
|
|
143
|
+
if self.emit_to_user:
|
|
144
|
+
emit_info(
|
|
145
|
+
f"MCP {self.server_name}: {line}",
|
|
146
|
+
message_group=self.message_group,
|
|
147
|
+
)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
# Note: We do NOT delete the log file - it's persistent now!
|
|
152
|
+
|
|
153
|
+
def __del__(self):
|
|
154
|
+
"""Safety net to close log file handle if stop() was never called."""
|
|
155
|
+
if getattr(self, "log_file", None) is not None:
|
|
156
|
+
try:
|
|
157
|
+
self.log_file.close()
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
def get_captured_lines(self) -> List[str]:
|
|
162
|
+
"""Get all captured lines from this session."""
|
|
163
|
+
return list(self.captured_lines)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SimpleCapturedMCPServerStdio(MCPServerStdio):
|
|
167
|
+
"""
|
|
168
|
+
MCPServerStdio that captures stderr to a file and optionally emits to user.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
command: str,
|
|
174
|
+
args=(),
|
|
175
|
+
env=None,
|
|
176
|
+
cwd=None,
|
|
177
|
+
emit_stderr: bool = True,
|
|
178
|
+
message_group: Optional[uuid.UUID] = None,
|
|
179
|
+
**kwargs,
|
|
180
|
+
):
|
|
181
|
+
super().__init__(command=command, args=args, env=env, cwd=cwd, **kwargs)
|
|
182
|
+
self.emit_stderr = emit_stderr
|
|
183
|
+
self.message_group = message_group or uuid.uuid4()
|
|
184
|
+
self._stderr_capture = None
|
|
185
|
+
|
|
186
|
+
@asynccontextmanager
|
|
187
|
+
async def client_streams(self):
|
|
188
|
+
"""Create streams with stderr capture."""
|
|
189
|
+
server = StdioServerParameters(
|
|
190
|
+
command=self.command, args=list(self.args), env=self.env, cwd=self.cwd
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Create stderr capture
|
|
194
|
+
server_name = getattr(self, "tool_prefix", self.command)
|
|
195
|
+
self._stderr_capture = StderrFileCapture(
|
|
196
|
+
server_name, self.emit_stderr, self.message_group
|
|
197
|
+
)
|
|
198
|
+
stderr_file = self._stderr_capture.start()
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
async with stdio_client(server=server, errlog=stderr_file) as (
|
|
202
|
+
read_stream,
|
|
203
|
+
write_stream,
|
|
204
|
+
):
|
|
205
|
+
yield read_stream, write_stream
|
|
206
|
+
finally:
|
|
207
|
+
self._stderr_capture.stop()
|
|
208
|
+
|
|
209
|
+
def get_captured_stderr(self) -> List[str]:
|
|
210
|
+
"""Get captured stderr lines."""
|
|
211
|
+
if self._stderr_capture:
|
|
212
|
+
return self._stderr_capture.get_captured_lines()
|
|
213
|
+
return []
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class BlockingMCPServerStdio(SimpleCapturedMCPServerStdio):
|
|
217
|
+
"""
|
|
218
|
+
MCP Server that blocks until fully initialized.
|
|
219
|
+
|
|
220
|
+
This server ensures that initialization is complete before
|
|
221
|
+
allowing any operations, preventing race conditions.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def __init__(self, *args, **kwargs):
|
|
225
|
+
super().__init__(*args, **kwargs)
|
|
226
|
+
self._initialized = asyncio.Event()
|
|
227
|
+
self._init_error: Optional[Exception] = None
|
|
228
|
+
self._initialization_task = None
|
|
229
|
+
|
|
230
|
+
async def __aenter__(self):
|
|
231
|
+
"""Enter context and track initialization."""
|
|
232
|
+
try:
|
|
233
|
+
# Start initialization
|
|
234
|
+
result = await super().__aenter__()
|
|
235
|
+
|
|
236
|
+
# Mark as initialized
|
|
237
|
+
self._initialized.set()
|
|
238
|
+
|
|
239
|
+
# Success message removed to reduce console spam
|
|
240
|
+
# server_name = getattr(self, "tool_prefix", self.command)
|
|
241
|
+
# emit_info(
|
|
242
|
+
# f"✅ MCP Server '{server_name}' initialized successfully",
|
|
243
|
+
# style="green",
|
|
244
|
+
# message_group=self.message_group,
|
|
245
|
+
# )
|
|
246
|
+
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
except BaseException as e:
|
|
250
|
+
# Store error and mark as initialized (with error)
|
|
251
|
+
# Unwrap ExceptionGroup if present (Python 3.11+)
|
|
252
|
+
if type(e).__name__ == "ExceptionGroup" and hasattr(e, "exceptions"):
|
|
253
|
+
# Use the first exception as the primary cause
|
|
254
|
+
self._init_error = e.exceptions[0]
|
|
255
|
+
error_details = f"{e.exceptions[0]}"
|
|
256
|
+
else:
|
|
257
|
+
self._init_error = e
|
|
258
|
+
error_details = str(e)
|
|
259
|
+
|
|
260
|
+
self._initialized.set()
|
|
261
|
+
|
|
262
|
+
# Emit error message
|
|
263
|
+
server_name = getattr(self, "tool_prefix", self.command)
|
|
264
|
+
emit_info(
|
|
265
|
+
f"❌ MCP Server '{server_name}' failed to initialize: {error_details}",
|
|
266
|
+
style="red",
|
|
267
|
+
message_group=self.message_group,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
raise
|
|
271
|
+
|
|
272
|
+
async def wait_until_ready(self, timeout: float = 30.0) -> bool:
|
|
273
|
+
"""
|
|
274
|
+
Wait until the server is ready.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
timeout: Maximum time to wait in seconds
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
True if server is ready, False if timeout or error
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
TimeoutError: If server doesn't initialize within timeout
|
|
284
|
+
Exception: If server initialization failed
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
await asyncio.wait_for(self._initialized.wait(), timeout=timeout)
|
|
288
|
+
|
|
289
|
+
# Check if there was an initialization error
|
|
290
|
+
if self._init_error:
|
|
291
|
+
raise self._init_error
|
|
292
|
+
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
except asyncio.TimeoutError:
|
|
296
|
+
server_name = getattr(self, "tool_prefix", self.command)
|
|
297
|
+
raise TimeoutError(
|
|
298
|
+
f"Server '{server_name}' initialization timeout after {timeout}s"
|
|
299
|
+
) from None
|
|
300
|
+
|
|
301
|
+
async def ensure_ready(self, timeout: float = 30.0):
|
|
302
|
+
"""
|
|
303
|
+
Ensure server is ready before proceeding.
|
|
304
|
+
|
|
305
|
+
This is a convenience method that raises if not ready.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
timeout: Maximum time to wait in seconds
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
TimeoutError: If server doesn't initialize within timeout
|
|
312
|
+
Exception: If server initialization failed
|
|
313
|
+
"""
|
|
314
|
+
await self.wait_until_ready(timeout)
|
|
315
|
+
|
|
316
|
+
def is_ready(self) -> bool:
|
|
317
|
+
"""
|
|
318
|
+
Check if server is ready without blocking.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
True if server is initialized and ready
|
|
322
|
+
"""
|
|
323
|
+
return self._initialized.is_set() and self._init_error is None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class StartupMonitor:
|
|
327
|
+
"""
|
|
328
|
+
Monitor for tracking multiple server startups.
|
|
329
|
+
|
|
330
|
+
This class helps coordinate startup of multiple MCP servers
|
|
331
|
+
and ensures all are ready before proceeding.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
def __init__(self, message_group: Optional[uuid.UUID] = None):
|
|
335
|
+
self.servers = {}
|
|
336
|
+
self.startup_times = {}
|
|
337
|
+
self.message_group = message_group or uuid.uuid4()
|
|
338
|
+
|
|
339
|
+
def add_server(self, name: str, server: BlockingMCPServerStdio):
|
|
340
|
+
"""Add a server to monitor."""
|
|
341
|
+
self.servers[name] = server
|
|
342
|
+
|
|
343
|
+
async def wait_all_ready(self, timeout: float = 30.0) -> dict:
|
|
344
|
+
"""
|
|
345
|
+
Wait for all servers to be ready.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
timeout: Maximum time to wait for all servers
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Dictionary of server names to ready status
|
|
352
|
+
"""
|
|
353
|
+
import time
|
|
354
|
+
|
|
355
|
+
results = {}
|
|
356
|
+
|
|
357
|
+
# Create tasks for all servers
|
|
358
|
+
async def wait_server(name: str, server: BlockingMCPServerStdio):
|
|
359
|
+
start = time.time()
|
|
360
|
+
try:
|
|
361
|
+
await server.wait_until_ready(timeout)
|
|
362
|
+
self.startup_times[name] = time.time() - start
|
|
363
|
+
results[name] = True
|
|
364
|
+
emit_info(
|
|
365
|
+
f" {name}: Ready in {self.startup_times[name]:.2f}s",
|
|
366
|
+
style="dim green",
|
|
367
|
+
message_group=self.message_group,
|
|
368
|
+
)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
self.startup_times[name] = time.time() - start
|
|
371
|
+
results[name] = False
|
|
372
|
+
emit_info(
|
|
373
|
+
f" {name}: Failed after {self.startup_times[name]:.2f}s - {e}",
|
|
374
|
+
style="dim red",
|
|
375
|
+
message_group=self.message_group,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Wait for all servers in parallel
|
|
379
|
+
emit_info(
|
|
380
|
+
f"⏳ Waiting for {len(self.servers)} MCP servers to initialize...",
|
|
381
|
+
style="cyan",
|
|
382
|
+
message_group=self.message_group,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
tasks = [
|
|
386
|
+
asyncio.create_task(wait_server(name, server))
|
|
387
|
+
for name, server in self.servers.items()
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
391
|
+
|
|
392
|
+
# Report summary
|
|
393
|
+
ready_count = sum(1 for r in results.values() if r)
|
|
394
|
+
total_count = len(results)
|
|
395
|
+
|
|
396
|
+
if ready_count == total_count:
|
|
397
|
+
emit_info(
|
|
398
|
+
f"✅ All {total_count} servers ready!",
|
|
399
|
+
style="green bold",
|
|
400
|
+
message_group=self.message_group,
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
emit_info(
|
|
404
|
+
f"⚠️ {ready_count}/{total_count} servers ready",
|
|
405
|
+
style="yellow",
|
|
406
|
+
message_group=self.message_group,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
return results
|
|
410
|
+
|
|
411
|
+
def get_startup_report(self) -> str:
|
|
412
|
+
"""Get a report of startup times."""
|
|
413
|
+
lines = ["Server Startup Times:"]
|
|
414
|
+
for name, time_taken in self.startup_times.items():
|
|
415
|
+
status = "✅" if self.servers[name].is_ready() else "❌"
|
|
416
|
+
lines.append(f" {status} {name}: {time_taken:.2f}s")
|
|
417
|
+
return "\n".join(lines)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
async def start_servers_with_blocking(
|
|
421
|
+
*servers: BlockingMCPServerStdio,
|
|
422
|
+
timeout: float = 30.0,
|
|
423
|
+
message_group: Optional[uuid.UUID] = None,
|
|
424
|
+
):
|
|
425
|
+
"""
|
|
426
|
+
Start multiple servers and wait for all to be ready.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
*servers: Variable number of BlockingMCPServerStdio instances
|
|
430
|
+
timeout: Maximum time to wait for all servers
|
|
431
|
+
message_group: Optional UUID for grouping log messages
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
List of ready servers
|
|
435
|
+
|
|
436
|
+
Example:
|
|
437
|
+
server1 = BlockingMCPServerStdio(...)
|
|
438
|
+
server2 = BlockingMCPServerStdio(...)
|
|
439
|
+
ready = await start_servers_with_blocking(server1, server2)
|
|
440
|
+
"""
|
|
441
|
+
monitor = StartupMonitor(message_group=message_group)
|
|
442
|
+
|
|
443
|
+
for i, server in enumerate(servers):
|
|
444
|
+
name = getattr(server, "tool_prefix", f"server-{i}")
|
|
445
|
+
monitor.add_server(name, server)
|
|
446
|
+
|
|
447
|
+
# Start all servers
|
|
448
|
+
async def start_server(server):
|
|
449
|
+
async with server:
|
|
450
|
+
await asyncio.sleep(0.1) # Keep context alive briefly
|
|
451
|
+
return server
|
|
452
|
+
|
|
453
|
+
# Store tasks to prevent garbage collection; note that server contexts
|
|
454
|
+
# will still close after the brief sleep - callers should manage server
|
|
455
|
+
# lifecycle separately
|
|
456
|
+
_startup_tasks = [asyncio.create_task(start_server(server)) for server in servers]
|
|
457
|
+
|
|
458
|
+
# Wait for all to be ready
|
|
459
|
+
results = await monitor.wait_all_ready(timeout)
|
|
460
|
+
|
|
461
|
+
# Get the report
|
|
462
|
+
emit_info(monitor.get_startup_report(), message_group=monitor.message_group)
|
|
463
|
+
|
|
464
|
+
# Return ready servers
|
|
465
|
+
ready_servers = [
|
|
466
|
+
server for name, server in monitor.servers.items() if results.get(name, False)
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
return ready_servers
|