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.
Files changed (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. 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