code-puppy 0.0.214__py3-none-any.whl → 0.0.366__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 (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -17,6 +17,15 @@ from .error_isolation import (
17
17
  )
18
18
  from .managed_server import ManagedMCPServer, ServerConfig, ServerState
19
19
  from .manager import MCPManager, ServerInfo, get_mcp_manager
20
+ from .mcp_logs import (
21
+ clear_logs,
22
+ get_log_file_path,
23
+ get_log_stats,
24
+ get_mcp_logs_dir,
25
+ list_servers_with_logs,
26
+ read_logs,
27
+ write_log,
28
+ )
20
29
  from .registry import ServerRegistry
21
30
  from .retry_manager import RetryManager, RetryStats, get_retry_manager, retry_mcp_call
22
31
  from .status_tracker import Event, ServerStatusTracker
@@ -46,4 +55,12 @@ __all__ = [
46
55
  "MCPDashboard",
47
56
  "MCPConfigWizard",
48
57
  "run_add_wizard",
58
+ # Log management
59
+ "get_mcp_logs_dir",
60
+ "get_log_file_path",
61
+ "read_logs",
62
+ "write_log",
63
+ "clear_logs",
64
+ "list_servers_with_logs",
65
+ "get_log_stats",
49
66
  ]
@@ -108,10 +108,17 @@ class AsyncServerLifecycleManager:
108
108
 
109
109
  try:
110
110
  logger.info(f"Starting server lifecycle for {server_id}")
111
+ logger.info(
112
+ f"Server {server_id} _running_count before enter: {getattr(server, '_running_count', 'N/A')}"
113
+ )
111
114
 
112
115
  # Enter the server's context
113
116
  await exit_stack.enter_async_context(server)
114
117
 
118
+ logger.info(
119
+ f"Server {server_id} _running_count after enter: {getattr(server, '_running_count', 'N/A')}"
120
+ )
121
+
115
122
  # Store the managed context
116
123
  async with self._lock:
117
124
  self._servers[server_id] = ManagedServerContext(
@@ -122,26 +129,50 @@ class AsyncServerLifecycleManager:
122
129
  task=asyncio.current_task(),
123
130
  )
124
131
 
125
- logger.info(f"Server {server_id} started successfully")
132
+ logger.info(
133
+ f"Server {server_id} started successfully and stored in _servers"
134
+ )
126
135
 
127
136
  # Keep the task alive until cancelled
137
+ loop_count = 0
128
138
  while True:
129
139
  await asyncio.sleep(1)
140
+ loop_count += 1
130
141
 
131
142
  # Check if server is still running
132
- if not server.is_running:
133
- logger.warning(f"Server {server_id} stopped unexpectedly")
143
+ running_count = getattr(server, "_running_count", "N/A")
144
+ is_running = server.is_running
145
+ logger.debug(
146
+ f"Server {server_id} heartbeat #{loop_count}: "
147
+ f"is_running={is_running}, _running_count={running_count}"
148
+ )
149
+
150
+ if not is_running:
151
+ logger.warning(
152
+ f"Server {server_id} stopped unexpectedly! "
153
+ f"_running_count={running_count}"
154
+ )
134
155
  break
135
156
 
136
157
  except asyncio.CancelledError:
137
158
  logger.info(f"Server {server_id} lifecycle task cancelled")
138
159
  raise
139
160
  except Exception as e:
140
- logger.error(f"Error in server {server_id} lifecycle: {e}")
161
+ logger.error(f"Error in server {server_id} lifecycle: {e}", exc_info=True)
141
162
  finally:
163
+ running_count = getattr(server, "_running_count", "N/A")
164
+ logger.info(
165
+ f"Server {server_id} lifecycle ending, _running_count={running_count}"
166
+ )
167
+
142
168
  # Clean up the context
143
169
  await exit_stack.aclose()
144
170
 
171
+ running_count_after = getattr(server, "_running_count", "N/A")
172
+ logger.info(
173
+ f"Server {server_id} context closed, _running_count={running_count_after}"
174
+ )
175
+
145
176
  # Remove from managed servers
146
177
  async with self._lock:
147
178
  if server_id in self._servers:
@@ -2,14 +2,13 @@
2
2
  MCP Server with blocking startup capability and stderr capture.
3
3
 
4
4
  This module provides MCP servers that:
5
- 1. Capture stderr output from stdio servers
5
+ 1. Capture stderr output from stdio servers to persistent log files
6
6
  2. Block until fully initialized before allowing operations
7
- 3. Emit stderr to users via emit_info with message groups
7
+ 3. Optionally emit stderr to users (disabled by default to reduce console noise)
8
8
  """
9
9
 
10
10
  import asyncio
11
11
  import os
12
- import tempfile
13
12
  import threading
14
13
  import uuid
15
14
  from contextlib import asynccontextmanager
@@ -18,64 +17,80 @@ from typing import List, Optional
18
17
  from mcp.client.stdio import StdioServerParameters, stdio_client
19
18
  from pydantic_ai.mcp import MCPServerStdio
20
19
 
20
+ from code_puppy.mcp_.mcp_logs import get_log_file_path, rotate_log_if_needed, write_log
21
21
  from code_puppy.messaging import emit_info
22
22
 
23
23
 
24
24
  class StderrFileCapture:
25
- """Captures stderr to a file and monitors it in a background thread."""
25
+ """
26
+ Captures stderr to a persistent log file and optionally monitors it.
27
+
28
+ Logs are written to ~/.code_puppy/mcp_logs/<server_name>.log
29
+ """
26
30
 
27
31
  def __init__(
28
32
  self,
29
33
  server_name: str,
30
- emit_to_user: bool = True,
34
+ emit_to_user: bool = False, # Disabled by default to reduce console noise
31
35
  message_group: Optional[uuid.UUID] = None,
32
36
  ):
33
37
  self.server_name = server_name
34
38
  self.emit_to_user = emit_to_user
35
39
  self.message_group = message_group or uuid.uuid4()
36
- self.temp_file = None
37
- self.temp_path = None
40
+ self.log_file = None
41
+ self.log_path = None
38
42
  self.monitor_thread = None
39
43
  self.stop_monitoring = threading.Event()
40
44
  self.captured_lines = []
45
+ self._last_read_pos = 0
41
46
 
42
47
  def start(self):
43
- """Start capture by creating temp file and monitor thread."""
44
- # Create temp file
45
- self.temp_file = tempfile.NamedTemporaryFile(
46
- mode="w+", delete=False, suffix=".err"
47
- )
48
- self.temp_path = self.temp_file.name
48
+ """Start capture by opening persistent log file and monitor thread."""
49
+ # Rotate log if needed
50
+ rotate_log_if_needed(self.server_name)
51
+
52
+ # Get persistent log path
53
+ self.log_path = get_log_file_path(self.server_name)
49
54
 
50
- # Start monitoring thread
55
+ # Write startup marker
56
+ write_log(self.server_name, "--- Server starting ---", "INFO")
57
+
58
+ # Open log file for appending stderr
59
+ self.log_file = open(self.log_path, "a", encoding="utf-8")
60
+
61
+ # Start monitoring thread only if we need to emit to user or capture lines
51
62
  self.stop_monitoring.clear()
52
63
  self.monitor_thread = threading.Thread(target=self._monitor_file)
53
64
  self.monitor_thread.daemon = True
54
65
  self.monitor_thread.start()
55
66
 
56
- return self.temp_file
67
+ return self.log_file
57
68
 
58
69
  def _monitor_file(self):
59
- """Monitor the temp file for new content."""
60
- if not self.temp_path:
70
+ """Monitor the log file for new content."""
71
+ if not self.log_path:
61
72
  return
62
73
 
63
- last_pos = 0
74
+ # Start reading from current position (end of file before we started)
75
+ try:
76
+ self._last_read_pos = os.path.getsize(self.log_path)
77
+ except OSError:
78
+ self._last_read_pos = 0
79
+
64
80
  while not self.stop_monitoring.is_set():
65
81
  try:
66
- with open(self.temp_path, "r") as f:
67
- f.seek(last_pos)
82
+ with open(self.log_path, "r", encoding="utf-8", errors="replace") as f:
83
+ f.seek(self._last_read_pos)
68
84
  new_content = f.read()
69
85
  if new_content:
70
- last_pos = f.tell()
86
+ self._last_read_pos = f.tell()
71
87
  # Process new lines
72
88
  for line in new_content.splitlines():
73
89
  if line.strip():
74
90
  self.captured_lines.append(line)
75
91
  if self.emit_to_user:
76
92
  emit_info(
77
- f"[bold white on blue] MCP {self.server_name} [/bold white on blue] {line}",
78
- style="dim cyan",
93
+ f"MCP {self.server_name}: {line}",
79
94
  message_group=self.message_group,
80
95
  )
81
96
 
@@ -90,33 +105,37 @@ class StderrFileCapture:
90
105
  if self.monitor_thread:
91
106
  self.monitor_thread.join(timeout=1)
92
107
 
93
- if self.temp_file:
108
+ if self.log_file:
94
109
  try:
95
- self.temp_file.close()
110
+ self.log_file.flush()
111
+ self.log_file.close()
96
112
  except Exception:
97
113
  pass
98
114
 
99
- if self.temp_path and os.path.exists(self.temp_path):
115
+ # Write shutdown marker
116
+ write_log(self.server_name, "--- Server stopped ---", "INFO")
117
+
118
+ # Read any remaining content for in-memory capture
119
+ if self.log_path and os.path.exists(self.log_path):
100
120
  try:
101
- # Read any remaining content
102
- with open(self.temp_path, "r") as f:
121
+ with open(self.log_path, "r", encoding="utf-8", errors="replace") as f:
122
+ f.seek(self._last_read_pos)
103
123
  content = f.read()
104
124
  for line in content.splitlines():
105
125
  if line.strip() and line not in self.captured_lines:
106
126
  self.captured_lines.append(line)
107
127
  if self.emit_to_user:
108
128
  emit_info(
109
- f"[bold white on blue] MCP {self.server_name} [/bold white on blue] {line}",
110
- style="dim cyan",
129
+ f"MCP {self.server_name}: {line}",
111
130
  message_group=self.message_group,
112
131
  )
113
-
114
- os.unlink(self.temp_path)
115
132
  except Exception:
116
133
  pass
117
134
 
135
+ # Note: We do NOT delete the log file - it's persistent now!
136
+
118
137
  def get_captured_lines(self) -> List[str]:
119
- """Get all captured lines."""
138
+ """Get all captured lines from this session."""
120
139
  return self.captured_lines.copy()
121
140
 
122
141
 
@@ -193,25 +212,33 @@ class BlockingMCPServerStdio(SimpleCapturedMCPServerStdio):
193
212
  # Mark as initialized
194
213
  self._initialized.set()
195
214
 
196
- # Emit success message
197
- server_name = getattr(self, "tool_prefix", self.command)
198
- emit_info(
199
- f"✅ MCP Server '{server_name}' initialized successfully",
200
- style="green",
201
- message_group=self.message_group,
202
- )
215
+ # Success message removed to reduce console spam
216
+ # server_name = getattr(self, "tool_prefix", self.command)
217
+ # emit_info(
218
+ # f"✅ MCP Server '{server_name}' initialized successfully",
219
+ # style="green",
220
+ # message_group=self.message_group,
221
+ # )
203
222
 
204
223
  return result
205
224
 
206
- except Exception as e:
225
+ except BaseException as e:
207
226
  # Store error and mark as initialized (with error)
208
- self._init_error = e
227
+ # Unwrap ExceptionGroup if present (Python 3.11+)
228
+ if type(e).__name__ == "ExceptionGroup" and hasattr(e, "exceptions"):
229
+ # Use the first exception as the primary cause
230
+ self._init_error = e.exceptions[0]
231
+ error_details = f"{e.exceptions[0]}"
232
+ else:
233
+ self._init_error = e
234
+ error_details = str(e)
235
+
209
236
  self._initialized.set()
210
237
 
211
238
  # Emit error message
212
239
  server_name = getattr(self, "tool_prefix", self.command)
213
240
  emit_info(
214
- f"❌ MCP Server '{server_name}' failed to initialize: {e}",
241
+ f"❌ MCP Server '{server_name}' failed to initialize: {error_details}",
215
242
  style="red",
216
243
  message_group=self.message_group,
217
244
  )
@@ -249,7 +249,7 @@ class StderrCollector:
249
249
  if emit_to_user:
250
250
  from code_puppy.messaging import emit_info
251
251
 
252
- emit_info(f"[MCP {server_name}] {line}", style="dim cyan")
252
+ emit_info(f"MCP {server_name}: {line}")
253
253
 
254
254
  return handler
255
255
 
@@ -265,7 +265,7 @@ class StderrCollector:
265
265
  """Clear captured output."""
266
266
  if server_name:
267
267
  if server_name in self.servers:
268
- self.servers[server_name].clear()
268
+ del self.servers[server_name]
269
269
  # Also clear from all_lines
270
270
  self.all_lines = [
271
271
  entry for entry in self.all_lines if entry["server"] != server_name
@@ -9,7 +9,7 @@ import re
9
9
  from typing import Dict, Optional
10
10
  from urllib.parse import urlparse
11
11
 
12
- from rich.console import Console
12
+ from rich.text import Text
13
13
 
14
14
  from code_puppy.mcp_.manager import ServerConfig, get_mcp_manager
15
15
  from code_puppy.messaging import (
@@ -20,8 +20,6 @@ from code_puppy.messaging import (
20
20
  emit_warning,
21
21
  )
22
22
 
23
- console = Console()
24
-
25
23
 
26
24
  def prompt_ask(
27
25
  prompt_text: str, default: Optional[str] = None, choices: Optional[list] = None
@@ -491,7 +489,9 @@ def run_add_wizard(group_id: str = None) -> bool:
491
489
  json.dump(data, f, indent=2)
492
490
 
493
491
  emit_info(
494
- f"[dim]Configuration saved to {MCP_SERVERS_FILE}[/dim]",
492
+ Text.from_markup(
493
+ f"[dim]Configuration saved to {MCP_SERVERS_FILE}[/dim]"
494
+ ),
495
495
  message_group=group_id,
496
496
  )
497
497
  return True
@@ -16,11 +16,16 @@ from .status_tracker import ServerState
16
16
 
17
17
 
18
18
  class MCPDashboard:
19
- """Visual dashboard for MCP server status monitoring"""
19
+ """Visual dashboard for MCP server status monitoring.
20
+
21
+ Note: This class uses Rich Console directly for rendering Rich tables.
22
+ This is intentional - Rich tables require Console for proper formatting.
23
+ """
20
24
 
21
25
  def __init__(self):
22
- """Initialize the MCP Dashboard"""
23
- self.console = Console()
26
+ """Initialize the MCP Dashboard."""
27
+ # Note: Console is used here specifically for Rich table rendering
28
+ self._console = Console()
24
29
 
25
30
  def render_dashboard(self) -> Table:
26
31
  """
@@ -278,10 +283,14 @@ class MCPDashboard:
278
283
  return "error"
279
284
 
280
285
  def print_dashboard(self) -> None:
281
- """Print the dashboard to console"""
286
+ """Print the dashboard to console.
287
+
288
+ Note: Uses Rich Console directly for table rendering - Rich tables
289
+ require Console for proper formatting with colors and borders.
290
+ """
282
291
  table = self.render_dashboard()
283
- self.console.print(table)
284
- self.console.print() # Add spacing
292
+ self._console.print(table)
293
+ self._console.print() # Add spacing
285
294
 
286
295
  def get_dashboard_string(self) -> str:
287
296
  """
@@ -6,7 +6,7 @@ that adds management capabilities while maintaining 100% compatibility.
6
6
  """
7
7
 
8
8
  import json
9
- import logging
9
+ import os
10
10
  import uuid
11
11
  from dataclasses import dataclass, field
12
12
  from datetime import datetime, timedelta
@@ -27,8 +27,30 @@ from code_puppy.http_utils import create_async_client
27
27
  from code_puppy.mcp_.blocking_startup import BlockingMCPServerStdio
28
28
  from code_puppy.messaging import emit_info
29
29
 
30
- # Configure logging
31
- logger = logging.getLogger(__name__)
30
+
31
+ def _expand_env_vars(value: Any) -> Any:
32
+ """
33
+ Recursively expand environment variables in config values.
34
+
35
+ Supports $VAR and ${VAR} syntax. Works with:
36
+ - Strings: expands env vars
37
+ - Dicts: recursively expands all string values
38
+ - Lists: recursively expands all string elements
39
+ - Other types: returned as-is
40
+
41
+ Args:
42
+ value: The value to expand env vars in
43
+
44
+ Returns:
45
+ The value with env vars expanded
46
+ """
47
+ if isinstance(value, str):
48
+ return os.path.expandvars(value)
49
+ elif isinstance(value, dict):
50
+ return {k: _expand_env_vars(v) for k, v in value.items()}
51
+ elif isinstance(value, list):
52
+ return [_expand_env_vars(item) for item in value]
53
+ return value
32
54
 
33
55
 
34
56
  class ServerState(Enum):
@@ -62,7 +84,7 @@ async def process_tool_call(
62
84
  """A tool call processor that passes along the deps."""
63
85
  group_id = uuid.uuid4()
64
86
  emit_info(
65
- f"\n[bold white on purple] MCP Tool Call - {name}[/bold white on purple]",
87
+ f"\nMCP Tool Call - {name}",
66
88
  message_group=group_id,
67
89
  )
68
90
  emit_info("\nArgs:", message_group=group_id)
@@ -114,7 +136,6 @@ class ManagedMCPServer:
114
136
  # Always start as STOPPED - servers must be explicitly started
115
137
  self._state = ServerState.STOPPED
116
138
  except Exception as e:
117
- logger.error(f"Failed to create server {self.config.name}: {e}")
118
139
  self._state = ServerState.ERROR
119
140
  self._error_message = str(e)
120
141
 
@@ -157,9 +178,9 @@ class ManagedMCPServer:
157
178
  if "url" not in config:
158
179
  raise ValueError("SSE server requires 'url' in config")
159
180
 
160
- # Prepare arguments for MCPServerSSE
181
+ # Prepare arguments for MCPServerSSE (expand env vars in URL)
161
182
  sse_kwargs = {
162
- "url": config["url"],
183
+ "url": _expand_env_vars(config["url"]),
163
184
  }
164
185
 
165
186
  # Add optional parameters if provided
@@ -181,23 +202,26 @@ class ManagedMCPServer:
181
202
  if "command" not in config:
182
203
  raise ValueError("Stdio server requires 'command' in config")
183
204
 
184
- # Handle command and arguments
185
- command = config["command"]
205
+ # Handle command and arguments (expand env vars)
206
+ command = _expand_env_vars(config["command"])
186
207
  args = config.get("args", [])
187
208
  if isinstance(args, str):
188
- # If args is a string, split it
189
- args = args.split()
209
+ # If args is a string, split it then expand
210
+ args = [_expand_env_vars(a) for a in args.split()]
211
+ else:
212
+ args = _expand_env_vars(args)
190
213
 
191
214
  # Prepare arguments for MCPServerStdio
192
215
  stdio_kwargs = {"command": command, "args": list(args) if args else []}
193
216
 
194
- # Add optional parameters if provided
217
+ # Add optional parameters if provided (expand env vars in env and cwd)
195
218
  if "env" in config:
196
- stdio_kwargs["env"] = config["env"]
219
+ stdio_kwargs["env"] = _expand_env_vars(config["env"])
197
220
  if "cwd" in config:
198
- stdio_kwargs["cwd"] = config["cwd"]
199
- if "timeout" in config:
200
- stdio_kwargs["timeout"] = config["timeout"]
221
+ stdio_kwargs["cwd"] = _expand_env_vars(config["cwd"])
222
+ # Default timeout of 60s for stdio servers - some servers like Serena take a while to start
223
+ # Users can override this in their config
224
+ stdio_kwargs["timeout"] = config.get("timeout", 60)
201
225
  if "read_timeout" in config:
202
226
  stdio_kwargs["read_timeout"] = config["read_timeout"]
203
227
 
@@ -207,8 +231,8 @@ class ManagedMCPServer:
207
231
  self._pydantic_server = BlockingMCPServerStdio(
208
232
  **stdio_kwargs,
209
233
  process_tool_call=process_tool_call,
210
- tool_prefix=config["name"],
211
- emit_stderr=True, # Always emit stderr for now
234
+ tool_prefix=self.config.name,
235
+ emit_stderr=False, # Logs go to file, not console (use /mcp logs to view)
212
236
  message_group=message_group,
213
237
  )
214
238
 
@@ -216,9 +240,9 @@ class ManagedMCPServer:
216
240
  if "url" not in config:
217
241
  raise ValueError("HTTP server requires 'url' in config")
218
242
 
219
- # Prepare arguments for MCPServerStreamableHTTP
243
+ # Prepare arguments for MCPServerStreamableHTTP (expand env vars in URL)
220
244
  http_kwargs = {
221
- "url": config["url"],
245
+ "url": _expand_env_vars(config["url"]),
222
246
  }
223
247
 
224
248
  # Add optional parameters if provided
@@ -226,9 +250,15 @@ class ManagedMCPServer:
226
250
  http_kwargs["timeout"] = config["timeout"]
227
251
  if "read_timeout" in config:
228
252
  http_kwargs["read_timeout"] = config["read_timeout"]
229
- if "headers" in config:
230
- http_kwargs["headers"] = config.get("headers")
231
- # Create HTTP client if headers are provided but no client specified
253
+
254
+ # Pass headers directly instead of creating http_client
255
+ # Note: There's a bug in MCP 1.25.0 where passing http_client
256
+ # causes "'_AsyncGeneratorContextManager' object has no attribute 'stream'"
257
+ # The workaround is to pass headers directly and let pydantic-ai
258
+ # create the http_client internally.
259
+ if config.get("headers"):
260
+ # Expand environment variables in headers
261
+ http_kwargs["headers"] = _expand_env_vars(config["headers"])
232
262
 
233
263
  self._pydantic_server = MCPServerStreamableHTTP(
234
264
  **http_kwargs, process_tool_call=process_tool_call
@@ -237,12 +267,7 @@ class ManagedMCPServer:
237
267
  else:
238
268
  raise ValueError(f"Unsupported server type: {server_type}")
239
269
 
240
- logger.info(f"Created {server_type} server: {self.config.name}")
241
-
242
- except Exception as e:
243
- logger.error(
244
- f"Failed to create {server_type} server {self.config.name}: {e}"
245
- )
270
+ except Exception:
246
271
  raise
247
272
 
248
273
  def _get_http_client(self) -> httpx.AsyncClient:
@@ -253,8 +278,18 @@ class ManagedMCPServer:
253
278
  Configured async HTTP client with custom headers
254
279
  """
255
280
  headers = self.config.config.get("headers", {})
281
+
282
+ # Expand environment variables in headers
283
+ resolved_headers = {}
284
+ if isinstance(headers, dict):
285
+ for k, v in headers.items():
286
+ if isinstance(v, str):
287
+ resolved_headers[k] = os.path.expandvars(v)
288
+ else:
289
+ resolved_headers[k] = v
290
+
256
291
  timeout = self.config.config.get("timeout", 30)
257
- client = create_async_client(headers=headers, timeout=timeout)
292
+ client = create_async_client(headers=resolved_headers, timeout=timeout)
258
293
  return client
259
294
 
260
295
  def enable(self) -> None:
@@ -263,7 +298,6 @@ class ManagedMCPServer:
263
298
  if self._state == ServerState.STOPPED and self._pydantic_server is not None:
264
299
  self._state = ServerState.RUNNING
265
300
  self._start_time = datetime.now()
266
- logger.info(f"Enabled server: {self.config.name}")
267
301
 
268
302
  def disable(self) -> None:
269
303
  """Disable server availability."""
@@ -271,7 +305,6 @@ class ManagedMCPServer:
271
305
  if self._state == ServerState.RUNNING:
272
306
  self._state = ServerState.STOPPED
273
307
  self._stop_time = datetime.now()
274
- logger.info(f"Disabled server: {self.config.name}")
275
308
 
276
309
  def is_enabled(self) -> bool:
277
310
  """
@@ -290,12 +323,7 @@ class ManagedMCPServer:
290
323
  duration: Quarantine duration in seconds
291
324
  """
292
325
  self._quarantine_until = datetime.now() + timedelta(seconds=duration)
293
- previous_state = self._state
294
326
  self._state = ServerState.QUARANTINED
295
- logger.warning(
296
- f"Quarantined server {self.config.name} for {duration} seconds "
297
- f"(was {previous_state.value})"
298
- )
299
327
 
300
328
  def is_quarantined(self) -> bool:
301
329
  """
@@ -315,7 +343,6 @@ class ManagedMCPServer:
315
343
  self._state = (
316
344
  ServerState.RUNNING if self._enabled else ServerState.STOPPED
317
345
  )
318
- logger.info(f"Released quarantine for server: {self.config.name}")
319
346
  return False
320
347
 
321
348
  return True