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_/manager.py
CHANGED
|
@@ -78,11 +78,76 @@ class MCPManager:
|
|
|
78
78
|
# Active managed servers (server_id -> ManagedMCPServer)
|
|
79
79
|
self._managed_servers: Dict[str, ManagedMCPServer] = {}
|
|
80
80
|
|
|
81
|
+
# Sync servers from mcp_servers.json into registry
|
|
82
|
+
self.sync_from_config()
|
|
83
|
+
|
|
81
84
|
# Load existing servers from registry
|
|
82
85
|
self._initialize_servers()
|
|
83
86
|
|
|
84
87
|
logger.info("MCPManager initialized with core components")
|
|
85
88
|
|
|
89
|
+
def sync_from_config(self) -> None:
|
|
90
|
+
"""Sync servers from mcp_servers.json into the registry.
|
|
91
|
+
|
|
92
|
+
This public method ensures that servers defined in the user's
|
|
93
|
+
configuration file are automatically registered with the manager.
|
|
94
|
+
It can be called during initialization or manually to reload
|
|
95
|
+
server configurations.
|
|
96
|
+
|
|
97
|
+
This is the single source of truth for syncing mcp_servers.json
|
|
98
|
+
into the registry, avoiding duplication with base_agent.py.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
from code_puppy.config import load_mcp_server_configs
|
|
102
|
+
|
|
103
|
+
configs = load_mcp_server_configs()
|
|
104
|
+
if not configs:
|
|
105
|
+
logger.debug("No servers found in mcp_servers.json")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
synced_count = 0
|
|
109
|
+
updated_count = 0
|
|
110
|
+
|
|
111
|
+
for name, conf in configs.items():
|
|
112
|
+
try:
|
|
113
|
+
# Create ServerConfig from the loaded configuration
|
|
114
|
+
server_config = ServerConfig(
|
|
115
|
+
id=conf.get("id", ""), # Empty ID will be auto-generated
|
|
116
|
+
name=name,
|
|
117
|
+
type=conf.get("type", "sse"),
|
|
118
|
+
enabled=conf.get("enabled", True),
|
|
119
|
+
config=conf,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Check if server already exists by name
|
|
123
|
+
existing = self.registry.get_by_name(name)
|
|
124
|
+
|
|
125
|
+
if not existing:
|
|
126
|
+
# Register new server
|
|
127
|
+
self.registry.register(server_config)
|
|
128
|
+
synced_count += 1
|
|
129
|
+
logger.debug(f"Synced new server from config: {name}")
|
|
130
|
+
else:
|
|
131
|
+
# Update existing server if config has changed
|
|
132
|
+
if existing.config != server_config.config:
|
|
133
|
+
server_config.id = existing.id # Keep existing ID
|
|
134
|
+
self.registry.update(existing.id, server_config)
|
|
135
|
+
updated_count += 1
|
|
136
|
+
logger.debug(f"Updated server from config: {name}")
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning(f"Failed to sync server '{name}' from config: {e}")
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if synced_count > 0 or updated_count > 0:
|
|
143
|
+
logger.info(
|
|
144
|
+
f"Synced {synced_count} new and updated {updated_count} servers from mcp_servers.json"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"Failed to sync from mcp_servers.json: {e}")
|
|
149
|
+
# Don't fail initialization if sync fails
|
|
150
|
+
|
|
86
151
|
def _initialize_servers(self) -> None:
|
|
87
152
|
"""Initialize managed servers from registry configurations."""
|
|
88
153
|
configs = self.registry.list_all()
|
|
@@ -404,41 +469,57 @@ class MCPManager:
|
|
|
404
469
|
def start_server_sync(self, server_id: str) -> bool:
|
|
405
470
|
"""
|
|
406
471
|
Synchronous wrapper for start_server.
|
|
472
|
+
|
|
473
|
+
IMPORTANT: This schedules the server start as a background task.
|
|
474
|
+
The server subprocess will start asynchronously - it may not be
|
|
475
|
+
immediately ready when this function returns.
|
|
407
476
|
"""
|
|
408
477
|
try:
|
|
409
|
-
asyncio.get_running_loop()
|
|
410
|
-
# We're in an async context
|
|
411
|
-
#
|
|
478
|
+
loop = asyncio.get_running_loop()
|
|
479
|
+
# We're in an async context - schedule the server start as a background task
|
|
480
|
+
# DO NOT use blocking time.sleep() here as it freezes the event loop!
|
|
412
481
|
|
|
413
|
-
#
|
|
414
|
-
|
|
415
|
-
|
|
482
|
+
# First, enable the server immediately so it's recognized as "starting"
|
|
483
|
+
managed_server = self._managed_servers.get(server_id)
|
|
484
|
+
if managed_server:
|
|
485
|
+
managed_server.enable()
|
|
486
|
+
self.status_tracker.set_status(server_id, ServerState.STARTING)
|
|
487
|
+
self.status_tracker.record_start_time(server_id)
|
|
416
488
|
|
|
417
|
-
# Schedule the
|
|
418
|
-
|
|
489
|
+
# Schedule the async start_server to run in the background
|
|
490
|
+
# This will properly start the subprocess and lifecycle task
|
|
491
|
+
async def start_server_background():
|
|
492
|
+
try:
|
|
493
|
+
result = await self.start_server(server_id)
|
|
494
|
+
if result:
|
|
495
|
+
logger.info(f"Background server start completed: {server_id}")
|
|
496
|
+
else:
|
|
497
|
+
logger.warning(f"Background server start failed: {server_id}")
|
|
498
|
+
return result
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(f"Background server start error for {server_id}: {e}")
|
|
501
|
+
self.status_tracker.set_status(server_id, ServerState.ERROR)
|
|
502
|
+
return False
|
|
503
|
+
|
|
504
|
+
# Create the task - it will run when the event loop gets control
|
|
505
|
+
task = loop.create_task(
|
|
506
|
+
start_server_background(), name=f"start_server_{server_id}"
|
|
507
|
+
)
|
|
419
508
|
|
|
420
|
-
#
|
|
421
|
-
|
|
509
|
+
# Store task reference to prevent garbage collection
|
|
510
|
+
if not hasattr(self, "_pending_start_tasks"):
|
|
511
|
+
self._pending_start_tasks = {}
|
|
512
|
+
self._pending_start_tasks[server_id] = task
|
|
422
513
|
|
|
423
|
-
|
|
514
|
+
# Add callback to clean up task reference when done
|
|
515
|
+
def cleanup_task(t):
|
|
516
|
+
if hasattr(self, "_pending_start_tasks"):
|
|
517
|
+
self._pending_start_tasks.pop(server_id, None)
|
|
424
518
|
|
|
425
|
-
|
|
426
|
-
if task.done():
|
|
427
|
-
try:
|
|
428
|
-
result = task.result()
|
|
429
|
-
return result
|
|
430
|
-
except Exception:
|
|
431
|
-
pass
|
|
519
|
+
task.add_done_callback(cleanup_task)
|
|
432
520
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if managed_server:
|
|
436
|
-
managed_server.enable()
|
|
437
|
-
self.status_tracker.set_status(server_id, ServerState.RUNNING)
|
|
438
|
-
self.status_tracker.record_start_time(server_id)
|
|
439
|
-
logger.info(f"Enabled server synchronously: {server_id}")
|
|
440
|
-
return True
|
|
441
|
-
return False
|
|
521
|
+
logger.info(f"Scheduled background start for server: {server_id}")
|
|
522
|
+
return True # Return immediately - server will start in background
|
|
442
523
|
|
|
443
524
|
except RuntimeError:
|
|
444
525
|
# No async loop, just enable the server
|
|
@@ -517,39 +598,52 @@ class MCPManager:
|
|
|
517
598
|
def stop_server_sync(self, server_id: str) -> bool:
|
|
518
599
|
"""
|
|
519
600
|
Synchronous wrapper for stop_server.
|
|
601
|
+
|
|
602
|
+
IMPORTANT: This schedules the server stop as a background task.
|
|
603
|
+
The server subprocess will stop asynchronously.
|
|
520
604
|
"""
|
|
521
605
|
try:
|
|
522
|
-
asyncio.get_running_loop()
|
|
606
|
+
loop = asyncio.get_running_loop()
|
|
607
|
+
# We're in an async context - schedule the server stop as a background task
|
|
608
|
+
# DO NOT use blocking time.sleep() here as it freezes the event loop!
|
|
523
609
|
|
|
524
|
-
#
|
|
525
|
-
|
|
526
|
-
|
|
610
|
+
# First, disable the server immediately
|
|
611
|
+
managed_server = self._managed_servers.get(server_id)
|
|
612
|
+
if managed_server:
|
|
613
|
+
managed_server.disable()
|
|
614
|
+
self.status_tracker.set_status(server_id, ServerState.STOPPING)
|
|
615
|
+
self.status_tracker.record_stop_time(server_id)
|
|
527
616
|
|
|
528
|
-
# Schedule the
|
|
529
|
-
|
|
617
|
+
# Schedule the async stop_server to run in the background
|
|
618
|
+
async def stop_server_background():
|
|
619
|
+
try:
|
|
620
|
+
result = await self.stop_server(server_id)
|
|
621
|
+
if result:
|
|
622
|
+
logger.info(f"Background server stop completed: {server_id}")
|
|
623
|
+
return result
|
|
624
|
+
except Exception as e:
|
|
625
|
+
logger.error(f"Background server stop error for {server_id}: {e}")
|
|
626
|
+
return False
|
|
530
627
|
|
|
531
|
-
#
|
|
532
|
-
|
|
628
|
+
# Create the task - it will run when the event loop gets control
|
|
629
|
+
task = loop.create_task(
|
|
630
|
+
stop_server_background(), name=f"stop_server_{server_id}"
|
|
631
|
+
)
|
|
533
632
|
|
|
534
|
-
|
|
633
|
+
# Store task reference to prevent garbage collection
|
|
634
|
+
if not hasattr(self, "_pending_stop_tasks"):
|
|
635
|
+
self._pending_stop_tasks = {}
|
|
636
|
+
self._pending_stop_tasks[server_id] = task
|
|
535
637
|
|
|
536
|
-
#
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return result
|
|
541
|
-
except Exception:
|
|
542
|
-
pass
|
|
638
|
+
# Add callback to clean up task reference when done
|
|
639
|
+
def cleanup_task(t):
|
|
640
|
+
if hasattr(self, "_pending_stop_tasks"):
|
|
641
|
+
self._pending_stop_tasks.pop(server_id, None)
|
|
543
642
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
self.status_tracker.set_status(server_id, ServerState.STOPPED)
|
|
549
|
-
self.status_tracker.record_stop_time(server_id)
|
|
550
|
-
logger.info(f"Disabled server synchronously: {server_id}")
|
|
551
|
-
return True
|
|
552
|
-
return False
|
|
643
|
+
task.add_done_callback(cleanup_task)
|
|
644
|
+
|
|
645
|
+
logger.info(f"Scheduled background stop for server: {server_id}")
|
|
646
|
+
return True # Return immediately - server will stop in background
|
|
553
647
|
|
|
554
648
|
except RuntimeError:
|
|
555
649
|
# No async loop, just disable the server
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server Log Management.
|
|
3
|
+
|
|
4
|
+
This module provides persistent log file management for MCP servers.
|
|
5
|
+
Logs are stored in STATE_DIR/mcp_logs/<server_name>.log
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from code_puppy.config import STATE_DIR
|
|
13
|
+
|
|
14
|
+
# Maximum log file size in bytes (5MB)
|
|
15
|
+
MAX_LOG_SIZE = 5 * 1024 * 1024
|
|
16
|
+
|
|
17
|
+
# Number of rotated logs to keep
|
|
18
|
+
MAX_ROTATED_LOGS = 3
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_mcp_logs_dir() -> Path:
|
|
22
|
+
"""
|
|
23
|
+
Get the directory for MCP server logs.
|
|
24
|
+
|
|
25
|
+
Creates the directory if it doesn't exist.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Path to the MCP logs directory
|
|
29
|
+
"""
|
|
30
|
+
logs_dir = Path(STATE_DIR) / "mcp_logs"
|
|
31
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
return logs_dir
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_log_file_path(server_name: str) -> Path:
|
|
36
|
+
"""
|
|
37
|
+
Get the log file path for a specific server.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
server_name: Name of the MCP server
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Path to the server's log file
|
|
44
|
+
"""
|
|
45
|
+
# Sanitize server name for filesystem
|
|
46
|
+
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in server_name)
|
|
47
|
+
return get_mcp_logs_dir() / f"{safe_name}.log"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def rotate_log_if_needed(server_name: str) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Rotate log file if it exceeds MAX_LOG_SIZE.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
server_name: Name of the MCP server
|
|
56
|
+
"""
|
|
57
|
+
log_path = get_log_file_path(server_name)
|
|
58
|
+
|
|
59
|
+
if not log_path.exists():
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
# Check if rotation is needed
|
|
63
|
+
if log_path.stat().st_size < MAX_LOG_SIZE:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
logs_dir = get_mcp_logs_dir()
|
|
67
|
+
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in server_name)
|
|
68
|
+
|
|
69
|
+
# Remove oldest rotated log if we're at the limit
|
|
70
|
+
oldest = logs_dir / f"{safe_name}.log.{MAX_ROTATED_LOGS}"
|
|
71
|
+
if oldest.exists():
|
|
72
|
+
oldest.unlink()
|
|
73
|
+
|
|
74
|
+
# Shift existing rotated logs
|
|
75
|
+
for i in range(MAX_ROTATED_LOGS - 1, 0, -1):
|
|
76
|
+
old_path = logs_dir / f"{safe_name}.log.{i}"
|
|
77
|
+
new_path = logs_dir / f"{safe_name}.log.{i + 1}"
|
|
78
|
+
if old_path.exists():
|
|
79
|
+
old_path.rename(new_path)
|
|
80
|
+
|
|
81
|
+
# Rotate current log
|
|
82
|
+
rotated_path = logs_dir / f"{safe_name}.log.1"
|
|
83
|
+
log_path.rename(rotated_path)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def write_log(server_name: str, message: str, level: str = "INFO") -> None:
|
|
87
|
+
"""
|
|
88
|
+
Write a log message for a server.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
server_name: Name of the MCP server
|
|
92
|
+
message: Log message to write
|
|
93
|
+
level: Log level (INFO, ERROR, WARN, DEBUG)
|
|
94
|
+
"""
|
|
95
|
+
rotate_log_if_needed(server_name)
|
|
96
|
+
|
|
97
|
+
log_path = get_log_file_path(server_name)
|
|
98
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
99
|
+
|
|
100
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
101
|
+
f.write(f"[{timestamp}] [{level}] {message}\n")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def read_logs(
|
|
105
|
+
server_name: str, lines: Optional[int] = None, include_rotated: bool = False
|
|
106
|
+
) -> List[str]:
|
|
107
|
+
"""
|
|
108
|
+
Read log lines for a server.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
server_name: Name of the MCP server
|
|
112
|
+
lines: Number of lines to return (from end). None means all lines.
|
|
113
|
+
include_rotated: Whether to include rotated log files
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of log lines (most recent last)
|
|
117
|
+
"""
|
|
118
|
+
all_lines = []
|
|
119
|
+
|
|
120
|
+
# Read rotated logs first (oldest to newest)
|
|
121
|
+
if include_rotated:
|
|
122
|
+
logs_dir = get_mcp_logs_dir()
|
|
123
|
+
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in server_name)
|
|
124
|
+
|
|
125
|
+
for i in range(MAX_ROTATED_LOGS, 0, -1):
|
|
126
|
+
rotated_path = logs_dir / f"{safe_name}.log.{i}"
|
|
127
|
+
if rotated_path.exists():
|
|
128
|
+
with open(rotated_path, "r", encoding="utf-8", errors="replace") as f:
|
|
129
|
+
all_lines.extend(f.read().splitlines())
|
|
130
|
+
|
|
131
|
+
# Read current log
|
|
132
|
+
log_path = get_log_file_path(server_name)
|
|
133
|
+
if log_path.exists():
|
|
134
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
135
|
+
all_lines.extend(f.read().splitlines())
|
|
136
|
+
|
|
137
|
+
# Return requested number of lines
|
|
138
|
+
if lines is not None and lines > 0:
|
|
139
|
+
return all_lines[-lines:]
|
|
140
|
+
|
|
141
|
+
return all_lines
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def clear_logs(server_name: str, include_rotated: bool = True) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Clear logs for a server.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
server_name: Name of the MCP server
|
|
150
|
+
include_rotated: Whether to also clear rotated log files
|
|
151
|
+
"""
|
|
152
|
+
log_path = get_log_file_path(server_name)
|
|
153
|
+
|
|
154
|
+
if log_path.exists():
|
|
155
|
+
log_path.unlink()
|
|
156
|
+
|
|
157
|
+
if include_rotated:
|
|
158
|
+
logs_dir = get_mcp_logs_dir()
|
|
159
|
+
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in server_name)
|
|
160
|
+
|
|
161
|
+
for i in range(1, MAX_ROTATED_LOGS + 1):
|
|
162
|
+
rotated_path = logs_dir / f"{safe_name}.log.{i}"
|
|
163
|
+
if rotated_path.exists():
|
|
164
|
+
rotated_path.unlink()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def list_servers_with_logs() -> List[str]:
|
|
168
|
+
"""
|
|
169
|
+
List all servers that have log files.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of server names with log files
|
|
173
|
+
"""
|
|
174
|
+
logs_dir = get_mcp_logs_dir()
|
|
175
|
+
servers = set()
|
|
176
|
+
|
|
177
|
+
for path in logs_dir.glob("*.log*"):
|
|
178
|
+
# Extract server name from filename
|
|
179
|
+
name = path.stem
|
|
180
|
+
# Remove .log suffix and rotation numbers
|
|
181
|
+
name = name.replace(".log", "").rstrip(".0123456789")
|
|
182
|
+
if name:
|
|
183
|
+
servers.add(name)
|
|
184
|
+
|
|
185
|
+
return sorted(servers)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_log_stats(server_name: str) -> dict:
|
|
189
|
+
"""
|
|
190
|
+
Get statistics about a server's logs.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
server_name: Name of the MCP server
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dictionary with log statistics
|
|
197
|
+
"""
|
|
198
|
+
log_path = get_log_file_path(server_name)
|
|
199
|
+
|
|
200
|
+
stats = {
|
|
201
|
+
"exists": log_path.exists(),
|
|
202
|
+
"size_bytes": 0,
|
|
203
|
+
"line_count": 0,
|
|
204
|
+
"rotated_count": 0,
|
|
205
|
+
"total_size_bytes": 0,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if log_path.exists():
|
|
209
|
+
stats["size_bytes"] = log_path.stat().st_size
|
|
210
|
+
stats["total_size_bytes"] = stats["size_bytes"]
|
|
211
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
212
|
+
stats["line_count"] = sum(1 for _ in f)
|
|
213
|
+
|
|
214
|
+
# Count rotated logs
|
|
215
|
+
logs_dir = get_mcp_logs_dir()
|
|
216
|
+
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in server_name)
|
|
217
|
+
|
|
218
|
+
for i in range(1, MAX_ROTATED_LOGS + 1):
|
|
219
|
+
rotated_path = logs_dir / f"{safe_name}.log.{i}"
|
|
220
|
+
if rotated_path.exists():
|
|
221
|
+
stats["rotated_count"] += 1
|
|
222
|
+
stats["total_size_bytes"] += rotated_path.stat().st_size
|
|
223
|
+
|
|
224
|
+
return stats
|
code_puppy/mcp_/registry.py
CHANGED
|
@@ -12,6 +12,7 @@ import uuid
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Dict, List, Optional
|
|
14
14
|
|
|
15
|
+
from code_puppy import config
|
|
15
16
|
from .managed_server import ServerConfig
|
|
16
17
|
|
|
17
18
|
# Configure logging
|
|
@@ -23,7 +24,7 @@ class ServerRegistry:
|
|
|
23
24
|
Registry for managing MCP server configurations.
|
|
24
25
|
|
|
25
26
|
Provides CRUD operations for server configurations with thread-safe access,
|
|
26
|
-
validation, and persistent storage to
|
|
27
|
+
validation, and persistent storage to XDG_DATA_HOME/code_puppy/mcp_registry.json.
|
|
27
28
|
|
|
28
29
|
All operations are thread-safe and use JSON serialization for ServerConfig objects.
|
|
29
30
|
Handles file not existing gracefully and validates configurations according to
|
|
@@ -36,13 +37,12 @@ class ServerRegistry:
|
|
|
36
37
|
|
|
37
38
|
Args:
|
|
38
39
|
storage_path: Optional custom path for registry storage.
|
|
39
|
-
Defaults to
|
|
40
|
+
Defaults to XDG_DATA_HOME/code_puppy/mcp_registry.json
|
|
40
41
|
"""
|
|
41
42
|
if storage_path is None:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
self._storage_path = code_puppy_dir / "mcp_registry.json"
|
|
43
|
+
data_dir = Path(config.DATA_DIR)
|
|
44
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
45
|
+
self._storage_path = data_dir / "mcp_registry.json"
|
|
46
46
|
else:
|
|
47
47
|
self._storage_path = Path(storage_path)
|
|
48
48
|
|
|
@@ -121,13 +121,13 @@ class MCPServerTemplate:
|
|
|
121
121
|
if "env" in config:
|
|
122
122
|
for env_key, env_value in config["env"].items():
|
|
123
123
|
if isinstance(env_value, str) and "${" in env_value:
|
|
124
|
-
# Replace placeholders in env values
|
|
124
|
+
# Replace all placeholders in env values
|
|
125
|
+
new_value = env_value
|
|
125
126
|
for key, value in cmd_args.items():
|
|
126
127
|
placeholder = f"${{{key}}}"
|
|
127
|
-
if placeholder in
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
128
|
+
if placeholder in new_value:
|
|
129
|
+
new_value = new_value.replace(placeholder, str(value))
|
|
130
|
+
config["env"][env_key] = new_value
|
|
131
131
|
|
|
132
132
|
return config
|
|
133
133
|
|
|
@@ -803,6 +803,25 @@ MCP_SERVER_REGISTRY: List[MCPServerTemplate] = [
|
|
|
803
803
|
),
|
|
804
804
|
example_usage="Cloud-based service - no local setup required",
|
|
805
805
|
),
|
|
806
|
+
MCPServerTemplate(
|
|
807
|
+
id="sse-example",
|
|
808
|
+
name="sse-example",
|
|
809
|
+
display_name="SSE Example Server",
|
|
810
|
+
description="Example Server-Sent Events MCP server for testing SSE connections",
|
|
811
|
+
category="Development",
|
|
812
|
+
tags=["sse", "example", "testing", "events"],
|
|
813
|
+
type="sse",
|
|
814
|
+
config={
|
|
815
|
+
"url": "http://localhost:8080/sse",
|
|
816
|
+
"headers": {"Authorization": "Bearer $SSE_API_KEY"},
|
|
817
|
+
},
|
|
818
|
+
verified=False,
|
|
819
|
+
popular=False,
|
|
820
|
+
requires=MCPServerRequirements(
|
|
821
|
+
environment_vars=["SSE_API_KEY"],
|
|
822
|
+
),
|
|
823
|
+
example_usage="Example SSE server - for testing purposes",
|
|
824
|
+
),
|
|
806
825
|
MCPServerTemplate(
|
|
807
826
|
id="confluence",
|
|
808
827
|
name="confluence",
|