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
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
"""Interactive terminal UI for browsing and installing MCP servers.
|
|
2
|
+
|
|
3
|
+
Provides a beautiful split-panel interface for browsing categories and servers
|
|
4
|
+
with live preview of server details and one-click installation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from prompt_toolkit.application import Application
|
|
14
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
15
|
+
from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
|
|
16
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
17
|
+
from prompt_toolkit.widgets import Frame
|
|
18
|
+
|
|
19
|
+
from code_puppy.messaging import emit_error, emit_info, emit_warning
|
|
20
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
21
|
+
|
|
22
|
+
from .catalog_server_installer import (
|
|
23
|
+
install_catalog_server,
|
|
24
|
+
prompt_for_server_config,
|
|
25
|
+
)
|
|
26
|
+
from .custom_server_form import run_custom_server_form
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
PAGE_SIZE = 12 # Items per page
|
|
31
|
+
|
|
32
|
+
# Special category for custom servers
|
|
33
|
+
CUSTOM_SERVER_CATEGORY = "➕ Custom Server"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MCPInstallMenu:
|
|
37
|
+
"""Interactive TUI for browsing and installing MCP servers."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, manager):
|
|
40
|
+
"""Initialize the MCP server browser menu.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
manager: MCP manager instance for server installation
|
|
44
|
+
"""
|
|
45
|
+
self.manager = manager
|
|
46
|
+
self.catalog = None
|
|
47
|
+
self.categories: List[str] = []
|
|
48
|
+
self.current_category: Optional[str] = None
|
|
49
|
+
self.current_servers: List = []
|
|
50
|
+
|
|
51
|
+
# State management
|
|
52
|
+
self.view_mode = "categories" # "categories" or "servers"
|
|
53
|
+
self.selected_category_idx = 0
|
|
54
|
+
self.selected_server_idx = 0
|
|
55
|
+
self.current_page = 0
|
|
56
|
+
self.result = None # Track installation result
|
|
57
|
+
|
|
58
|
+
# Pending server for configuration
|
|
59
|
+
self.pending_server = None
|
|
60
|
+
|
|
61
|
+
# UI controls
|
|
62
|
+
self.menu_control = None
|
|
63
|
+
self.preview_control = None
|
|
64
|
+
|
|
65
|
+
# Initialize catalog
|
|
66
|
+
self._initialize_catalog()
|
|
67
|
+
|
|
68
|
+
def _initialize_catalog(self):
|
|
69
|
+
"""Initialize the MCP server catalog with error handling."""
|
|
70
|
+
try:
|
|
71
|
+
from code_puppy.mcp_.server_registry_catalog import catalog
|
|
72
|
+
|
|
73
|
+
self.catalog = catalog
|
|
74
|
+
# Add custom server option as first category
|
|
75
|
+
self.categories = [CUSTOM_SERVER_CATEGORY] + self.catalog.list_categories()
|
|
76
|
+
if len(self.categories) <= 1: # Only custom category
|
|
77
|
+
emit_error("No categories found in server catalog")
|
|
78
|
+
except ImportError as e:
|
|
79
|
+
emit_error(f"Server catalog not available: {e}")
|
|
80
|
+
# Still allow custom servers even if catalog fails
|
|
81
|
+
self.categories = [CUSTOM_SERVER_CATEGORY]
|
|
82
|
+
except Exception as e:
|
|
83
|
+
emit_error(f"Error loading server catalog: {e}")
|
|
84
|
+
self.categories = [CUSTOM_SERVER_CATEGORY]
|
|
85
|
+
|
|
86
|
+
def _get_current_category(self) -> Optional[str]:
|
|
87
|
+
"""Get the currently selected category."""
|
|
88
|
+
if 0 <= self.selected_category_idx < len(self.categories):
|
|
89
|
+
return self.categories[self.selected_category_idx]
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def _get_current_server(self):
|
|
93
|
+
"""Get the currently selected server."""
|
|
94
|
+
if self.view_mode == "servers" and self.current_servers:
|
|
95
|
+
if 0 <= self.selected_server_idx < len(self.current_servers):
|
|
96
|
+
return self.current_servers[self.selected_server_idx]
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def _get_category_icon(self, category: str) -> str:
|
|
100
|
+
"""Get an icon for a category."""
|
|
101
|
+
if category == CUSTOM_SERVER_CATEGORY:
|
|
102
|
+
return "➕"
|
|
103
|
+
icons = {
|
|
104
|
+
"Code": "💻",
|
|
105
|
+
"Storage": "💾",
|
|
106
|
+
"Database": "🗄️",
|
|
107
|
+
"Documentation": "📝",
|
|
108
|
+
"DevOps": "🔧",
|
|
109
|
+
"Monitoring": "📊",
|
|
110
|
+
"Package Management": "📦",
|
|
111
|
+
"Communication": "💬",
|
|
112
|
+
"AI": "🤖",
|
|
113
|
+
"Search": "🔍",
|
|
114
|
+
"Development": "🛠️",
|
|
115
|
+
"Cloud": "☁️",
|
|
116
|
+
}
|
|
117
|
+
return icons.get(category, "📁")
|
|
118
|
+
|
|
119
|
+
def _is_custom_server_selected(self) -> bool:
|
|
120
|
+
"""Check if the custom server category is selected."""
|
|
121
|
+
return (
|
|
122
|
+
self.view_mode == "categories"
|
|
123
|
+
and self.selected_category_idx == 0
|
|
124
|
+
and len(self.categories) > 0
|
|
125
|
+
and self.categories[0] == CUSTOM_SERVER_CATEGORY
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _render_category_list(self) -> List:
|
|
129
|
+
"""Render the category list panel."""
|
|
130
|
+
lines = []
|
|
131
|
+
|
|
132
|
+
lines.append(("bold cyan", " 📂 CATEGORIES"))
|
|
133
|
+
lines.append(("", "\n\n"))
|
|
134
|
+
|
|
135
|
+
if not self.categories:
|
|
136
|
+
lines.append(("fg:yellow", " No categories available."))
|
|
137
|
+
lines.append(("", "\n\n"))
|
|
138
|
+
self._render_navigation_hints(lines)
|
|
139
|
+
return lines
|
|
140
|
+
|
|
141
|
+
# Show categories for current page
|
|
142
|
+
total_pages = (len(self.categories) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
143
|
+
start_idx = self.current_page * PAGE_SIZE
|
|
144
|
+
end_idx = min(start_idx + PAGE_SIZE, len(self.categories))
|
|
145
|
+
|
|
146
|
+
for i in range(start_idx, end_idx):
|
|
147
|
+
category = self.categories[i]
|
|
148
|
+
is_selected = i == self.selected_category_idx
|
|
149
|
+
icon = self._get_category_icon(category)
|
|
150
|
+
|
|
151
|
+
prefix = " > " if is_selected else " "
|
|
152
|
+
|
|
153
|
+
# Custom server category doesn't have a count
|
|
154
|
+
if category == CUSTOM_SERVER_CATEGORY:
|
|
155
|
+
label = f"{prefix}{icon} Custom Server (JSON)"
|
|
156
|
+
if is_selected:
|
|
157
|
+
lines.append(("fg:ansibrightgreen bold", label))
|
|
158
|
+
else:
|
|
159
|
+
lines.append(("fg:ansigreen", label))
|
|
160
|
+
else:
|
|
161
|
+
# Count servers in category
|
|
162
|
+
server_count = (
|
|
163
|
+
len(self.catalog.get_by_category(category)) if self.catalog else 0
|
|
164
|
+
)
|
|
165
|
+
label = f"{prefix}{icon} {category} ({server_count})"
|
|
166
|
+
if is_selected:
|
|
167
|
+
lines.append(("fg:ansibrightcyan bold", label))
|
|
168
|
+
else:
|
|
169
|
+
lines.append(("fg:ansibrightblack", label))
|
|
170
|
+
|
|
171
|
+
lines.append(("", "\n"))
|
|
172
|
+
|
|
173
|
+
lines.append(("", "\n"))
|
|
174
|
+
if total_pages > 1:
|
|
175
|
+
lines.append(
|
|
176
|
+
("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
|
|
177
|
+
)
|
|
178
|
+
lines.append(("", "\n"))
|
|
179
|
+
|
|
180
|
+
self._render_navigation_hints(lines)
|
|
181
|
+
return lines
|
|
182
|
+
|
|
183
|
+
def _render_server_list(self) -> List:
|
|
184
|
+
"""Render the server list panel."""
|
|
185
|
+
lines = []
|
|
186
|
+
|
|
187
|
+
if not self.current_category:
|
|
188
|
+
lines.append(("fg:yellow", " No category selected."))
|
|
189
|
+
lines.append(("", "\n\n"))
|
|
190
|
+
self._render_navigation_hints(lines)
|
|
191
|
+
return lines
|
|
192
|
+
|
|
193
|
+
icon = self._get_category_icon(self.current_category)
|
|
194
|
+
lines.append(("bold cyan", f" {icon} {self.current_category.upper()}"))
|
|
195
|
+
lines.append(("", "\n\n"))
|
|
196
|
+
|
|
197
|
+
if not self.current_servers:
|
|
198
|
+
lines.append(("fg:yellow", " No servers in this category."))
|
|
199
|
+
lines.append(("", "\n\n"))
|
|
200
|
+
self._render_navigation_hints(lines)
|
|
201
|
+
return lines
|
|
202
|
+
|
|
203
|
+
# Show servers for current page
|
|
204
|
+
total_pages = (len(self.current_servers) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
205
|
+
start_idx = self.current_page * PAGE_SIZE
|
|
206
|
+
end_idx = min(start_idx + PAGE_SIZE, len(self.current_servers))
|
|
207
|
+
|
|
208
|
+
for i in range(start_idx, end_idx):
|
|
209
|
+
server = self.current_servers[i]
|
|
210
|
+
is_selected = i == self.selected_server_idx
|
|
211
|
+
|
|
212
|
+
# Create indicator icons
|
|
213
|
+
icons = []
|
|
214
|
+
if server.verified:
|
|
215
|
+
icons.append("✓")
|
|
216
|
+
if server.popular:
|
|
217
|
+
icons.append("⭐")
|
|
218
|
+
|
|
219
|
+
icon_str = " ".join(icons) + " " if icons else ""
|
|
220
|
+
|
|
221
|
+
prefix = " > " if is_selected else " "
|
|
222
|
+
label = f"{prefix}{icon_str}{server.display_name}"
|
|
223
|
+
|
|
224
|
+
if is_selected:
|
|
225
|
+
lines.append(("fg:ansibrightcyan bold", label))
|
|
226
|
+
else:
|
|
227
|
+
lines.append(("fg:ansibrightblack", label))
|
|
228
|
+
|
|
229
|
+
lines.append(("", "\n"))
|
|
230
|
+
|
|
231
|
+
lines.append(("", "\n"))
|
|
232
|
+
if total_pages > 1:
|
|
233
|
+
lines.append(
|
|
234
|
+
("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
|
|
235
|
+
)
|
|
236
|
+
lines.append(("", "\n"))
|
|
237
|
+
|
|
238
|
+
self._render_navigation_hints(lines)
|
|
239
|
+
return lines
|
|
240
|
+
|
|
241
|
+
def _render_navigation_hints(self, lines: List):
|
|
242
|
+
"""Render navigation hints at the bottom of the list panel."""
|
|
243
|
+
lines.append(("", "\n"))
|
|
244
|
+
lines.append(("fg:ansibrightblack", " ↑/↓ "))
|
|
245
|
+
lines.append(("", "Navigate "))
|
|
246
|
+
lines.append(("fg:ansibrightblack", "←/→ "))
|
|
247
|
+
lines.append(("", "Page\n"))
|
|
248
|
+
if self.view_mode == "categories":
|
|
249
|
+
lines.append(("fg:green", " Enter "))
|
|
250
|
+
lines.append(("", "Browse Servers\n"))
|
|
251
|
+
else:
|
|
252
|
+
lines.append(("fg:green", " Enter "))
|
|
253
|
+
lines.append(("", "Install Server\n"))
|
|
254
|
+
lines.append(("fg:ansibrightblack", " Esc/Back "))
|
|
255
|
+
lines.append(("", "Back\n"))
|
|
256
|
+
lines.append(("fg:ansired", " Ctrl+C "))
|
|
257
|
+
lines.append(("", "Cancel"))
|
|
258
|
+
|
|
259
|
+
def _render_details(self) -> List:
|
|
260
|
+
"""Render the details panel."""
|
|
261
|
+
lines = []
|
|
262
|
+
|
|
263
|
+
lines.append(("bold cyan", " 📋 DETAILS"))
|
|
264
|
+
lines.append(("", "\n\n"))
|
|
265
|
+
|
|
266
|
+
if self.view_mode == "categories":
|
|
267
|
+
category = self._get_current_category()
|
|
268
|
+
if not category:
|
|
269
|
+
lines.append(("fg:yellow", " No category selected."))
|
|
270
|
+
return lines
|
|
271
|
+
|
|
272
|
+
# Special handling for custom server category
|
|
273
|
+
if category == CUSTOM_SERVER_CATEGORY:
|
|
274
|
+
return self._render_custom_server_details()
|
|
275
|
+
|
|
276
|
+
icon = self._get_category_icon(category)
|
|
277
|
+
lines.append(("bold", f" {icon} {category}"))
|
|
278
|
+
lines.append(("", "\n\n"))
|
|
279
|
+
|
|
280
|
+
# Show servers in this category
|
|
281
|
+
servers = self.catalog.get_by_category(category) if self.catalog else []
|
|
282
|
+
lines.append(("fg:ansibrightblack", f" {len(servers)} servers available"))
|
|
283
|
+
lines.append(("", "\n\n"))
|
|
284
|
+
|
|
285
|
+
# Show popular servers in this category
|
|
286
|
+
popular = [s for s in servers if s.popular]
|
|
287
|
+
if popular:
|
|
288
|
+
lines.append(("bold", " ⭐ Popular:"))
|
|
289
|
+
lines.append(("", "\n"))
|
|
290
|
+
for server in popular[:5]:
|
|
291
|
+
lines.append(("fg:ansibrightblack", f" • {server.display_name}"))
|
|
292
|
+
lines.append(("", "\n"))
|
|
293
|
+
|
|
294
|
+
else: # servers view
|
|
295
|
+
server = self._get_current_server()
|
|
296
|
+
if not server:
|
|
297
|
+
lines.append(("fg:yellow", " No server selected."))
|
|
298
|
+
return lines
|
|
299
|
+
|
|
300
|
+
# Server name with indicators
|
|
301
|
+
indicators = []
|
|
302
|
+
if server.verified:
|
|
303
|
+
indicators.append("✓ Verified")
|
|
304
|
+
if server.popular:
|
|
305
|
+
indicators.append("⭐ Popular")
|
|
306
|
+
|
|
307
|
+
lines.append(("bold", f" {server.display_name}"))
|
|
308
|
+
lines.append(("", "\n"))
|
|
309
|
+
|
|
310
|
+
if indicators:
|
|
311
|
+
lines.append(("fg:green", f" {' | '.join(indicators)}"))
|
|
312
|
+
lines.append(("", "\n"))
|
|
313
|
+
|
|
314
|
+
lines.append(("", "\n"))
|
|
315
|
+
|
|
316
|
+
# Description
|
|
317
|
+
lines.append(("bold", " Description:"))
|
|
318
|
+
lines.append(("", "\n"))
|
|
319
|
+
# Wrap description
|
|
320
|
+
desc = server.description or "No description available"
|
|
321
|
+
# Simple word wrap
|
|
322
|
+
words = desc.split()
|
|
323
|
+
line = " "
|
|
324
|
+
for word in words:
|
|
325
|
+
if len(line) + len(word) > 50:
|
|
326
|
+
lines.append(("fg:ansibrightblack", line))
|
|
327
|
+
lines.append(("", "\n"))
|
|
328
|
+
line = " " + word + " "
|
|
329
|
+
else:
|
|
330
|
+
line += word + " "
|
|
331
|
+
if line.strip():
|
|
332
|
+
lines.append(("fg:ansibrightblack", line))
|
|
333
|
+
lines.append(("", "\n"))
|
|
334
|
+
|
|
335
|
+
lines.append(("", "\n"))
|
|
336
|
+
|
|
337
|
+
# Type
|
|
338
|
+
lines.append(("bold", " Type:"))
|
|
339
|
+
lines.append(("", "\n"))
|
|
340
|
+
type_icons = {"stdio": "📟", "http": "🌐", "sse": "📡"}
|
|
341
|
+
type_icon = type_icons.get(server.type, "❓")
|
|
342
|
+
lines.append(("fg:ansibrightblack", f" {type_icon} {server.type}"))
|
|
343
|
+
lines.append(("", "\n\n"))
|
|
344
|
+
|
|
345
|
+
# Tags
|
|
346
|
+
if server.tags:
|
|
347
|
+
lines.append(("bold", " Tags:"))
|
|
348
|
+
lines.append(("", "\n"))
|
|
349
|
+
tag_line = " " + ", ".join(server.tags[:6])
|
|
350
|
+
lines.append(("fg:ansicyan", tag_line))
|
|
351
|
+
lines.append(("", "\n\n"))
|
|
352
|
+
|
|
353
|
+
# Requirements
|
|
354
|
+
requirements = server.get_requirements()
|
|
355
|
+
|
|
356
|
+
# Environment variables
|
|
357
|
+
env_vars = server.get_environment_vars()
|
|
358
|
+
if env_vars:
|
|
359
|
+
lines.append(("bold", " 🔑 Environment Variables:"))
|
|
360
|
+
lines.append(("", "\n"))
|
|
361
|
+
for var in env_vars:
|
|
362
|
+
# Check if already set
|
|
363
|
+
is_set = os.environ.get(var)
|
|
364
|
+
if is_set:
|
|
365
|
+
lines.append(("fg:green", f" ✓ {var}"))
|
|
366
|
+
else:
|
|
367
|
+
lines.append(("fg:yellow", f" ○ {var}"))
|
|
368
|
+
lines.append(("", "\n"))
|
|
369
|
+
lines.append(("", "\n"))
|
|
370
|
+
|
|
371
|
+
# Command line args
|
|
372
|
+
cmd_args = server.get_command_line_args()
|
|
373
|
+
if cmd_args:
|
|
374
|
+
lines.append(("bold", " ⚙️ Configuration:"))
|
|
375
|
+
lines.append(("", "\n"))
|
|
376
|
+
for arg in cmd_args:
|
|
377
|
+
name = arg.get("name", "unknown")
|
|
378
|
+
required = arg.get("required", True)
|
|
379
|
+
default = arg.get("default", "")
|
|
380
|
+
marker = "*" if required else "?"
|
|
381
|
+
default_str = f" [{default}]" if default else ""
|
|
382
|
+
lines.append(
|
|
383
|
+
("fg:ansibrightblack", f" {marker} {name}{default_str}")
|
|
384
|
+
)
|
|
385
|
+
lines.append(("", "\n"))
|
|
386
|
+
lines.append(("", "\n"))
|
|
387
|
+
|
|
388
|
+
# Required tools
|
|
389
|
+
required_tools = requirements.required_tools
|
|
390
|
+
if required_tools:
|
|
391
|
+
lines.append(("bold", " 🛠️ Required Tools:"))
|
|
392
|
+
lines.append(("", "\n"))
|
|
393
|
+
lines.append(("fg:ansibrightblack", f" {', '.join(required_tools)}"))
|
|
394
|
+
lines.append(("", "\n\n"))
|
|
395
|
+
|
|
396
|
+
# Example usage
|
|
397
|
+
if server.example_usage:
|
|
398
|
+
lines.append(("bold", " 💡 Example:"))
|
|
399
|
+
lines.append(("", "\n"))
|
|
400
|
+
lines.append(("fg:ansibrightblack", f" {server.example_usage}"))
|
|
401
|
+
lines.append(("", "\n"))
|
|
402
|
+
|
|
403
|
+
return lines
|
|
404
|
+
|
|
405
|
+
def _render_custom_server_details(self) -> List:
|
|
406
|
+
"""Render details for the custom server option."""
|
|
407
|
+
lines = []
|
|
408
|
+
|
|
409
|
+
lines.append(("bold cyan", " 📋 DETAILS"))
|
|
410
|
+
lines.append(("", "\n\n"))
|
|
411
|
+
|
|
412
|
+
lines.append(("bold green", " ➕ Add Custom MCP Server"))
|
|
413
|
+
lines.append(("", "\n\n"))
|
|
414
|
+
|
|
415
|
+
lines.append(("fg:ansibrightblack", " Add your own MCP server by providing"))
|
|
416
|
+
lines.append(("", "\n"))
|
|
417
|
+
lines.append(("fg:ansibrightblack", " a JSON configuration."))
|
|
418
|
+
lines.append(("", "\n\n"))
|
|
419
|
+
|
|
420
|
+
lines.append(("bold", " 📟 Supported Types:"))
|
|
421
|
+
lines.append(("", "\n\n"))
|
|
422
|
+
|
|
423
|
+
lines.append(("fg:ansicyan bold", " 1. stdio"))
|
|
424
|
+
lines.append(("", "\n"))
|
|
425
|
+
lines.append(("fg:ansibrightblack", " Runs a local command (npx, python,"))
|
|
426
|
+
lines.append(("", "\n"))
|
|
427
|
+
lines.append(("fg:ansibrightblack", " uvx, etc.) and communicates via"))
|
|
428
|
+
lines.append(("", "\n"))
|
|
429
|
+
lines.append(("fg:ansibrightblack", " stdin/stdout."))
|
|
430
|
+
lines.append(("", "\n\n"))
|
|
431
|
+
|
|
432
|
+
lines.append(("fg:ansicyan bold", " 2. http"))
|
|
433
|
+
lines.append(("", "\n"))
|
|
434
|
+
lines.append(("fg:ansibrightblack", " Connects to an HTTP endpoint that"))
|
|
435
|
+
lines.append(("", "\n"))
|
|
436
|
+
lines.append(("fg:ansibrightblack", " implements the MCP protocol."))
|
|
437
|
+
lines.append(("", "\n\n"))
|
|
438
|
+
|
|
439
|
+
lines.append(("fg:ansicyan bold", " 3. sse"))
|
|
440
|
+
lines.append(("", "\n"))
|
|
441
|
+
lines.append(("fg:ansibrightblack", " Connects via Server-Sent Events"))
|
|
442
|
+
lines.append(("", "\n"))
|
|
443
|
+
lines.append(("fg:ansibrightblack", " for real-time streaming."))
|
|
444
|
+
lines.append(("", "\n\n"))
|
|
445
|
+
|
|
446
|
+
lines.append(("bold", " 💡 Press Enter to configure"))
|
|
447
|
+
lines.append(("", "\n"))
|
|
448
|
+
|
|
449
|
+
return lines
|
|
450
|
+
|
|
451
|
+
def update_display(self):
|
|
452
|
+
"""Update the display based on current state."""
|
|
453
|
+
if self.view_mode == "categories":
|
|
454
|
+
self.menu_control.text = self._render_category_list()
|
|
455
|
+
else:
|
|
456
|
+
self.menu_control.text = self._render_server_list()
|
|
457
|
+
|
|
458
|
+
self.preview_control.text = self._render_details()
|
|
459
|
+
|
|
460
|
+
def _enter_category(self):
|
|
461
|
+
"""Enter the selected category to view its servers."""
|
|
462
|
+
category = self._get_current_category()
|
|
463
|
+
if not category:
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
# Handle custom server selection
|
|
467
|
+
if category == CUSTOM_SERVER_CATEGORY:
|
|
468
|
+
self.result = "pending_custom"
|
|
469
|
+
return # Signal to exit and prompt for custom config
|
|
470
|
+
|
|
471
|
+
if not self.catalog:
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
self.current_category = category
|
|
475
|
+
self.current_servers = self.catalog.get_by_category(category)
|
|
476
|
+
self.view_mode = "servers"
|
|
477
|
+
self.selected_server_idx = 0
|
|
478
|
+
self.current_page = 0
|
|
479
|
+
self.update_display()
|
|
480
|
+
|
|
481
|
+
def _go_back_to_categories(self):
|
|
482
|
+
"""Go back to categories view."""
|
|
483
|
+
self.view_mode = "categories"
|
|
484
|
+
self.current_category = None
|
|
485
|
+
self.current_servers = []
|
|
486
|
+
self.selected_server_idx = 0
|
|
487
|
+
self.current_page = 0
|
|
488
|
+
self.update_display()
|
|
489
|
+
|
|
490
|
+
def _select_current_server(self):
|
|
491
|
+
"""Select the current server for installation."""
|
|
492
|
+
server = self._get_current_server()
|
|
493
|
+
if server:
|
|
494
|
+
self.pending_server = server
|
|
495
|
+
self.result = "pending_install"
|
|
496
|
+
|
|
497
|
+
def run(self) -> bool:
|
|
498
|
+
"""Run the interactive MCP server browser (synchronous).
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
True if a server was installed, False otherwise
|
|
502
|
+
"""
|
|
503
|
+
if not self.categories:
|
|
504
|
+
emit_warning("No MCP server catalog available.")
|
|
505
|
+
return False
|
|
506
|
+
|
|
507
|
+
# Build UI
|
|
508
|
+
self.menu_control = FormattedTextControl(text="")
|
|
509
|
+
self.preview_control = FormattedTextControl(text="")
|
|
510
|
+
|
|
511
|
+
menu_window = Window(
|
|
512
|
+
content=self.menu_control, wrap_lines=True, width=Dimension(weight=35)
|
|
513
|
+
)
|
|
514
|
+
preview_window = Window(
|
|
515
|
+
content=self.preview_control, wrap_lines=True, width=Dimension(weight=65)
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
menu_frame = Frame(menu_window, width=Dimension(weight=35), title="Browse")
|
|
519
|
+
preview_frame = Frame(
|
|
520
|
+
preview_window, width=Dimension(weight=65), title="Details"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
root_container = VSplit([menu_frame, preview_frame])
|
|
524
|
+
|
|
525
|
+
# Key bindings
|
|
526
|
+
kb = KeyBindings()
|
|
527
|
+
|
|
528
|
+
@kb.add("up")
|
|
529
|
+
def _(event):
|
|
530
|
+
if self.view_mode == "categories":
|
|
531
|
+
if self.selected_category_idx > 0:
|
|
532
|
+
self.selected_category_idx -= 1
|
|
533
|
+
self.current_page = self.selected_category_idx // PAGE_SIZE
|
|
534
|
+
else: # servers view
|
|
535
|
+
if self.selected_server_idx > 0:
|
|
536
|
+
self.selected_server_idx -= 1
|
|
537
|
+
self.current_page = self.selected_server_idx // PAGE_SIZE
|
|
538
|
+
self.update_display()
|
|
539
|
+
|
|
540
|
+
@kb.add("down")
|
|
541
|
+
def _(event):
|
|
542
|
+
if self.view_mode == "categories":
|
|
543
|
+
if self.selected_category_idx < len(self.categories) - 1:
|
|
544
|
+
self.selected_category_idx += 1
|
|
545
|
+
self.current_page = self.selected_category_idx // PAGE_SIZE
|
|
546
|
+
else: # servers view
|
|
547
|
+
if self.selected_server_idx < len(self.current_servers) - 1:
|
|
548
|
+
self.selected_server_idx += 1
|
|
549
|
+
self.current_page = self.selected_server_idx // PAGE_SIZE
|
|
550
|
+
self.update_display()
|
|
551
|
+
|
|
552
|
+
@kb.add("left")
|
|
553
|
+
def _(event):
|
|
554
|
+
"""Previous page."""
|
|
555
|
+
if self.current_page > 0:
|
|
556
|
+
self.current_page -= 1
|
|
557
|
+
if self.view_mode == "categories":
|
|
558
|
+
self.selected_category_idx = self.current_page * PAGE_SIZE
|
|
559
|
+
else:
|
|
560
|
+
self.selected_server_idx = self.current_page * PAGE_SIZE
|
|
561
|
+
self.update_display()
|
|
562
|
+
|
|
563
|
+
@kb.add("right")
|
|
564
|
+
def _(event):
|
|
565
|
+
"""Next page."""
|
|
566
|
+
if self.view_mode == "categories":
|
|
567
|
+
total_items = len(self.categories)
|
|
568
|
+
else:
|
|
569
|
+
total_items = len(self.current_servers)
|
|
570
|
+
|
|
571
|
+
total_pages = (total_items + PAGE_SIZE - 1) // PAGE_SIZE
|
|
572
|
+
if self.current_page < total_pages - 1:
|
|
573
|
+
self.current_page += 1
|
|
574
|
+
if self.view_mode == "categories":
|
|
575
|
+
self.selected_category_idx = self.current_page * PAGE_SIZE
|
|
576
|
+
else:
|
|
577
|
+
self.selected_server_idx = self.current_page * PAGE_SIZE
|
|
578
|
+
self.update_display()
|
|
579
|
+
|
|
580
|
+
@kb.add("enter")
|
|
581
|
+
def _(event):
|
|
582
|
+
if self.view_mode == "categories":
|
|
583
|
+
self._enter_category()
|
|
584
|
+
# Exit if custom server was selected
|
|
585
|
+
if self.result == "pending_custom":
|
|
586
|
+
event.app.exit()
|
|
587
|
+
elif self.view_mode == "servers":
|
|
588
|
+
self._select_current_server()
|
|
589
|
+
event.app.exit()
|
|
590
|
+
|
|
591
|
+
@kb.add("escape")
|
|
592
|
+
def _(event):
|
|
593
|
+
if self.view_mode == "servers":
|
|
594
|
+
self._go_back_to_categories()
|
|
595
|
+
|
|
596
|
+
@kb.add("backspace")
|
|
597
|
+
def _(event):
|
|
598
|
+
if self.view_mode == "servers":
|
|
599
|
+
self._go_back_to_categories()
|
|
600
|
+
|
|
601
|
+
@kb.add("c-c")
|
|
602
|
+
def _(event):
|
|
603
|
+
event.app.exit()
|
|
604
|
+
|
|
605
|
+
layout = Layout(root_container)
|
|
606
|
+
app = Application(
|
|
607
|
+
layout=layout,
|
|
608
|
+
key_bindings=kb,
|
|
609
|
+
full_screen=False,
|
|
610
|
+
mouse_support=False,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
set_awaiting_user_input(True)
|
|
614
|
+
|
|
615
|
+
# Enter alternate screen buffer
|
|
616
|
+
sys.stdout.write("\033[?1049h") # Enter alternate buffer
|
|
617
|
+
sys.stdout.write("\033[2J\033[H") # Clear and home
|
|
618
|
+
sys.stdout.flush()
|
|
619
|
+
time.sleep(0.05)
|
|
620
|
+
|
|
621
|
+
try:
|
|
622
|
+
# Initial display
|
|
623
|
+
self.update_display()
|
|
624
|
+
|
|
625
|
+
# Clear the current buffer
|
|
626
|
+
sys.stdout.write("\033[2J\033[H")
|
|
627
|
+
sys.stdout.flush()
|
|
628
|
+
|
|
629
|
+
# Run application
|
|
630
|
+
app.run(in_thread=True)
|
|
631
|
+
|
|
632
|
+
finally:
|
|
633
|
+
# Exit alternate screen buffer
|
|
634
|
+
sys.stdout.write("\033[?1049l")
|
|
635
|
+
sys.stdout.flush()
|
|
636
|
+
set_awaiting_user_input(False)
|
|
637
|
+
|
|
638
|
+
# Clear exit message (unless we're about to prompt for more input)
|
|
639
|
+
if self.result not in ("pending_custom", "pending_install"):
|
|
640
|
+
emit_info("✓ Exited MCP server browser")
|
|
641
|
+
|
|
642
|
+
# Handle custom server after TUI exits
|
|
643
|
+
if self.result == "pending_custom":
|
|
644
|
+
success = run_custom_server_form(self.manager)
|
|
645
|
+
if success:
|
|
646
|
+
try:
|
|
647
|
+
from code_puppy.agent import reload_mcp_servers
|
|
648
|
+
|
|
649
|
+
reload_mcp_servers()
|
|
650
|
+
except ImportError:
|
|
651
|
+
pass
|
|
652
|
+
return success
|
|
653
|
+
|
|
654
|
+
# Handle catalog server installation after TUI exits
|
|
655
|
+
if self.result == "pending_install" and self.pending_server:
|
|
656
|
+
config = prompt_for_server_config(self.manager, self.pending_server)
|
|
657
|
+
if config:
|
|
658
|
+
success = install_catalog_server(
|
|
659
|
+
self.manager, self.pending_server, config
|
|
660
|
+
)
|
|
661
|
+
if success:
|
|
662
|
+
# Reload MCP servers
|
|
663
|
+
try:
|
|
664
|
+
from code_puppy.agent import reload_mcp_servers
|
|
665
|
+
|
|
666
|
+
reload_mcp_servers()
|
|
667
|
+
except ImportError:
|
|
668
|
+
pass
|
|
669
|
+
return success
|
|
670
|
+
return False
|
|
671
|
+
|
|
672
|
+
return False
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def run_mcp_install_menu(manager) -> bool:
|
|
676
|
+
"""Run the MCP install menu.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
manager: MCP manager instance
|
|
680
|
+
|
|
681
|
+
Returns:
|
|
682
|
+
True if a server was installed, False otherwise
|
|
683
|
+
"""
|
|
684
|
+
menu = MCPInstallMenu(manager)
|
|
685
|
+
return menu.run()
|
|
@@ -9,7 +9,7 @@ from rich.table import Table
|
|
|
9
9
|
from rich.text import Text
|
|
10
10
|
|
|
11
11
|
from code_puppy.mcp_.managed_server import ServerState
|
|
12
|
-
from code_puppy.messaging import emit_info
|
|
12
|
+
from code_puppy.messaging import emit_error, emit_info
|
|
13
13
|
|
|
14
14
|
from .base import MCPCommandBase
|
|
15
15
|
from .utils import format_state_indicator, format_uptime
|
|
@@ -91,4 +91,4 @@ class ListCommand(MCPCommandBase):
|
|
|
91
91
|
|
|
92
92
|
except Exception as e:
|
|
93
93
|
logger.error(f"Error listing MCP servers: {e}")
|
|
94
|
-
|
|
94
|
+
emit_error(f"Error listing servers: {e}", message_group=group_id)
|