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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -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 +446 -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 +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- 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 +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- 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 +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -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 +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -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 +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -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 +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -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/oauth_puppy_html.py +228 -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/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
code_puppy/mcp_/__init__.py
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
133
|
-
|
|
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.
|
|
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
|
-
"""
|
|
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 =
|
|
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.
|
|
37
|
-
self.
|
|
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
|
|
44
|
-
#
|
|
45
|
-
self.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
self.
|
|
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
|
-
#
|
|
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.
|
|
67
|
+
return self.log_file
|
|
57
68
|
|
|
58
69
|
def _monitor_file(self):
|
|
59
|
-
"""Monitor the
|
|
60
|
-
if not self.
|
|
70
|
+
"""Monitor the log file for new content."""
|
|
71
|
+
if not self.log_path:
|
|
61
72
|
return
|
|
62
73
|
|
|
63
|
-
|
|
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.
|
|
67
|
-
f.seek(
|
|
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
|
-
|
|
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"
|
|
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.
|
|
108
|
+
if self.log_file:
|
|
94
109
|
try:
|
|
95
|
-
self.
|
|
110
|
+
self.log_file.flush()
|
|
111
|
+
self.log_file.close()
|
|
96
112
|
except Exception:
|
|
97
113
|
pass
|
|
98
114
|
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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"
|
|
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
|
-
#
|
|
197
|
-
server_name = getattr(self, "tool_prefix", self.command)
|
|
198
|
-
emit_info(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
225
|
+
except BaseException as e:
|
|
207
226
|
# Store error and mark as initialized (with error)
|
|
208
|
-
|
|
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: {
|
|
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"
|
|
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]
|
|
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
|
code_puppy/mcp_/config_wizard.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
code_puppy/mcp_/dashboard.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
284
|
-
self.
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
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"\
|
|
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
|
-
|
|
200
|
-
|
|
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
|
|
211
|
-
emit_stderr=
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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=
|
|
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
|