cct-cli 0.7.9.0__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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — McpServersPanel (v0.7.8.1 full redesign).
|
|
3
|
+
|
|
4
|
+
Professional management screen for the Model Context Protocol (MCP)
|
|
5
|
+
client in calc_terminal/mcp.py:
|
|
6
|
+
|
|
7
|
+
- Card rows list every saved server with a live connection status:
|
|
8
|
+
🟢 Connected / 🔴 Offline / 🟡 Connecting, plus transport, version,
|
|
9
|
+
latency, tools exposed and last sync time.
|
|
10
|
+
- Minimal action set: one primary "+ Add Server"; a contextual row
|
|
11
|
+
(Edit / Connect·Disconnect / Delete) appears only when servers
|
|
12
|
+
exist and acts on the selected card; Save / Cancel close the page.
|
|
13
|
+
- Connect/Disconnect drives calc_terminal.mcp sessions in a
|
|
14
|
+
background thread so the UI never blocks on a hanging server.
|
|
15
|
+
- The Add/Edit popup is centered with a fade + slide animation and
|
|
16
|
+
offers HTTP / STDIO transports, URL, headers, authentication, and a
|
|
17
|
+
live Test Connection button.
|
|
18
|
+
|
|
19
|
+
The app routes the Main Menu's "MCP Servers" item to this screen.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
print("This is a library file and is not meant to be run directly.")
|
|
24
|
+
import sys
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
import threading
|
|
28
|
+
import time
|
|
29
|
+
|
|
30
|
+
TEXTUAL_AVAILABLE = True
|
|
31
|
+
try:
|
|
32
|
+
from textual.screen import Screen
|
|
33
|
+
from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
34
|
+
from textual.widgets import Static, Button, Input
|
|
35
|
+
from textual.binding import Binding
|
|
36
|
+
except Exception:
|
|
37
|
+
TEXTUAL_AVAILABLE = False
|
|
38
|
+
|
|
39
|
+
if TEXTUAL_AVAILABLE:
|
|
40
|
+
from .. import mcp
|
|
41
|
+
from . import theme_css
|
|
42
|
+
|
|
43
|
+
_STATUS_GLYPH = {
|
|
44
|
+
"connected": "\U0001f7e2", # 🟢
|
|
45
|
+
"connecting": "\U0001f7e1", # 🟡
|
|
46
|
+
"disconnected": "\U0001f534", # 🔴
|
|
47
|
+
"failed": "\U0001f534", # 🔴
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_STATUS_LABEL = {
|
|
51
|
+
"connected": "Connected",
|
|
52
|
+
"connecting": "Connecting",
|
|
53
|
+
"disconnected": "Offline",
|
|
54
|
+
"failed": "Offline",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def _status_tone(status_key):
|
|
58
|
+
return {
|
|
59
|
+
"connected": "success",
|
|
60
|
+
"connecting": "warning",
|
|
61
|
+
"disconnected": "text-faint",
|
|
62
|
+
"failed": "error",
|
|
63
|
+
}.get(status_key, "text-faint")
|
|
64
|
+
|
|
65
|
+
def _last_sync_text(server):
|
|
66
|
+
ts = server.get("last_connected") or 0
|
|
67
|
+
if not ts:
|
|
68
|
+
return "never"
|
|
69
|
+
return time.strftime("%Y-%m-%d %H:%M", time.localtime(ts))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _ServerCard(Static):
|
|
73
|
+
"""One server card: name + live status on line 1, transport ·
|
|
74
|
+
version · latency · tools · last sync on line 2."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, index, server, selected=False):
|
|
77
|
+
self.index = index
|
|
78
|
+
self.server = server
|
|
79
|
+
super().__init__("", classes="mcp-card" + (" mcp-card-sel" if selected else ""))
|
|
80
|
+
self._redraw()
|
|
81
|
+
|
|
82
|
+
def _redraw(self):
|
|
83
|
+
name = self.server.get("name") or self.server.get("id") or "?"
|
|
84
|
+
status_key = self.server.get("status") or "disconnected"
|
|
85
|
+
glyph = _STATUS_GLYPH.get(status_key, "\U0001f534")
|
|
86
|
+
tone = theme_css.current_hex(_status_tone(status_key))
|
|
87
|
+
label = _STATUS_LABEL.get(status_key, "Offline")
|
|
88
|
+
kind = "local" if self.server.get("kind") == "local" else "remote"
|
|
89
|
+
version = self.server.get("version") or ""
|
|
90
|
+
latency = self.server.get("latency_ms")
|
|
91
|
+
lat_txt = f"{latency}ms" if latency is not None else "\u2014"
|
|
92
|
+
tools = self.server.get("tools") or []
|
|
93
|
+
tools_txt = f"{len(tools)} tools"
|
|
94
|
+
sync_txt = _last_sync_text(self.server)
|
|
95
|
+
sel = " \u25b8" if "mcp-card-sel" in self.classes else ""
|
|
96
|
+
line1 = (f" {glyph} [{theme_css.current_hex('text')} b]{name}[/] "
|
|
97
|
+
f"[{tone}]{label}[/] [{theme_css.current_hex('text-faint')}]"
|
|
98
|
+
f"({kind})[/]{sel}")
|
|
99
|
+
line2 = (f" [{theme_css.current_hex('text-faint')}]"
|
|
100
|
+
f"transport {kind} \u00b7 v{version if version else '-'}"
|
|
101
|
+
f" \u00b7 {lat_txt} \u00b7 {tools_txt}"
|
|
102
|
+
f" \u00b7 sync {sync_txt}[/]")
|
|
103
|
+
self.update(f"{line1}\n{line2}")
|
|
104
|
+
|
|
105
|
+
def set_selected(self, selected):
|
|
106
|
+
self.set_class(selected, "mcp-card-sel")
|
|
107
|
+
self._redraw()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class McpServersPanel(Screen):
|
|
111
|
+
"""Manage MCP servers: add, edit, remove, connect, disconnect,
|
|
112
|
+
persist."""
|
|
113
|
+
|
|
114
|
+
CSS = """
|
|
115
|
+
McpServersPanel { align: center middle; background: $app-background 70%; }
|
|
116
|
+
#mcp-box {
|
|
117
|
+
width: 92; max-width: 100%; height: auto; max-height: 92%;
|
|
118
|
+
background: $surface; border: round $border; padding: 0;
|
|
119
|
+
opacity: 0; offset-y: 1;
|
|
120
|
+
transition: opacity 150ms, offset 180ms;
|
|
121
|
+
}
|
|
122
|
+
#mcp-box.open { opacity: 1; offset-y: 0; }
|
|
123
|
+
#mcp-titlebar {
|
|
124
|
+
height: 3; padding: 1 2 0 2;
|
|
125
|
+
border-bottom: solid $border;
|
|
126
|
+
}
|
|
127
|
+
#mcp-title { text-style: bold; width: 1fr; }
|
|
128
|
+
#mcp-subtitle { color: $text-faint; height: 2; padding: 0 2; }
|
|
129
|
+
#mcp-list { height: auto; min-height: 4; padding: 1 2; overflow-y: auto; scrollbar-gutter: stable; }
|
|
130
|
+
.mcp-card {
|
|
131
|
+
height: 3; margin-bottom: 1;
|
|
132
|
+
background: $app-background; border: round $border;
|
|
133
|
+
padding: 0 1;
|
|
134
|
+
}
|
|
135
|
+
.mcp-card-sel { border: round $border-active; }
|
|
136
|
+
.mcp-empty { color: $text-faint; padding: 2 1; }
|
|
137
|
+
#mcp-add-row { height: 4; padding: 0 2 1 2; }
|
|
138
|
+
#mcp-add-row Button { margin-right: 1; }
|
|
139
|
+
#mcp-ctx-actions { height: 4; padding: 0 2 1 2; }
|
|
140
|
+
#mcp-ctx-actions Button { margin-right: 1; width: 1fr; min-width: 10; }
|
|
141
|
+
#mcp-actions { height: 5; padding: 0 2 1 2; border-top: solid $border; }
|
|
142
|
+
#mcp-actions Button { margin-right: 1; }
|
|
143
|
+
#mcp-box.cct-compact #mcp-subtitle { display: none; }
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
BINDINGS = [
|
|
147
|
+
Binding("escape", "cancel", "Cancel"),
|
|
148
|
+
Binding("down", "move_down_sel", "Next row"),
|
|
149
|
+
Binding("up", "move_up_sel", "Previous row"),
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
def __init__(self):
|
|
153
|
+
super().__init__()
|
|
154
|
+
self._servers = mcp.load_servers()
|
|
155
|
+
self._selected = 0
|
|
156
|
+
|
|
157
|
+
def compose(self):
|
|
158
|
+
with Vertical(id="mcp-box"):
|
|
159
|
+
with Horizontal(id="mcp-titlebar"):
|
|
160
|
+
yield Static("\U0001f310 MCP Servers", id="mcp-title")
|
|
161
|
+
yield Button("\u2715", id="mcp-close", classes="cct-popup-close")
|
|
162
|
+
yield Static(
|
|
163
|
+
"Model Context Protocol servers used by CAT.",
|
|
164
|
+
id="mcp-subtitle")
|
|
165
|
+
with ScrollableContainer(id="mcp-list"):
|
|
166
|
+
yield from self._rows()
|
|
167
|
+
with Horizontal(id="mcp-add-row"):
|
|
168
|
+
yield Button("+ Add Server", id="mcp-add", variant="primary",
|
|
169
|
+
classes="cct-btn")
|
|
170
|
+
with Horizontal(id="mcp-ctx-actions"):
|
|
171
|
+
yield Button("Edit", id="mcp-edit", classes="cct-btn")
|
|
172
|
+
yield Button("Connect", id="mcp-connect", classes="cct-btn")
|
|
173
|
+
yield Button("Delete", id="mcp-remove", classes="cct-btn")
|
|
174
|
+
with Horizontal(id="mcp-actions"):
|
|
175
|
+
yield Button("Save", id="mcp-save", variant="primary", classes="cct-btn")
|
|
176
|
+
yield Button("Cancel", id="mcp-cancel", classes="cct-btn")
|
|
177
|
+
|
|
178
|
+
def _rows(self):
|
|
179
|
+
if not self._servers:
|
|
180
|
+
yield Static("No MCP servers configured.", classes="mcp-empty")
|
|
181
|
+
yield Static("Add an MCP server to expose its tools to CAT.",
|
|
182
|
+
classes="mcp-empty")
|
|
183
|
+
return
|
|
184
|
+
for i, server in enumerate(self._servers):
|
|
185
|
+
yield _ServerCard(i, server, selected=(i == self._selected))
|
|
186
|
+
|
|
187
|
+
def _rebuild_list(self):
|
|
188
|
+
try:
|
|
189
|
+
self.query_one("#mcp-list", ScrollableContainer).remove_children()
|
|
190
|
+
for row in self._rows():
|
|
191
|
+
self.query_one("#mcp-list", ScrollableContainer).mount(row)
|
|
192
|
+
self._refresh_actions()
|
|
193
|
+
self._fit()
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
def _refresh_actions(self):
|
|
198
|
+
"""Contextual actions row appears only when servers exist;
|
|
199
|
+
the Connect button flips to Disconnect for a connected
|
|
200
|
+
server. Only what's relevant is ever on screen."""
|
|
201
|
+
has = bool(self._servers)
|
|
202
|
+
try:
|
|
203
|
+
self.query_one("#mcp-ctx-actions").display = has
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
if not has:
|
|
207
|
+
return
|
|
208
|
+
server = self._current()
|
|
209
|
+
connected = bool(server and server.get("status") == "connected")
|
|
210
|
+
try:
|
|
211
|
+
btn = self.query_one("#mcp-connect", Button)
|
|
212
|
+
btn.label = "Disconnect" if connected else "Connect"
|
|
213
|
+
btn.variant = "error" if connected else "default"
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
def _fit(self):
|
|
218
|
+
"""Re-fit after content changes: content_size is only valid
|
|
219
|
+
after the next layout pass, so always defer one frame."""
|
|
220
|
+
try:
|
|
221
|
+
self.call_after_refresh(
|
|
222
|
+
lambda: theme_css.fit_dialog(self, "mcp-box", "mcp-list"))
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
def _refresh_status(self):
|
|
227
|
+
"""Legacy no-op kept for callers outside this class."""
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
def on_mount(self):
|
|
231
|
+
self.call_after_refresh(lambda: self.query_one("#mcp-box").add_class("open"))
|
|
232
|
+
self.call_after_refresh(self._fit)
|
|
233
|
+
self._refresh_actions()
|
|
234
|
+
|
|
235
|
+
def on_resize(self, event):
|
|
236
|
+
self._fit()
|
|
237
|
+
|
|
238
|
+
def on_click(self, event):
|
|
239
|
+
widget = getattr(event, "widget", None)
|
|
240
|
+
if isinstance(widget, _ServerCard):
|
|
241
|
+
if self._servers and widget.index < len(self._servers):
|
|
242
|
+
self._selected = widget.index
|
|
243
|
+
self._rebuild_list()
|
|
244
|
+
return
|
|
245
|
+
if widget in (self, None):
|
|
246
|
+
self.dismiss(None)
|
|
247
|
+
|
|
248
|
+
def on_key(self, event):
|
|
249
|
+
if event.key == "escape":
|
|
250
|
+
self.dismiss(None)
|
|
251
|
+
|
|
252
|
+
def action_cancel(self):
|
|
253
|
+
self.dismiss(None)
|
|
254
|
+
|
|
255
|
+
def action_move_down_sel(self):
|
|
256
|
+
if self._servers:
|
|
257
|
+
self._selected = min(self._selected + 1, len(self._servers) - 1)
|
|
258
|
+
self._rebuild_list()
|
|
259
|
+
|
|
260
|
+
def action_move_up_sel(self):
|
|
261
|
+
if self._servers:
|
|
262
|
+
self._selected = max(self._selected - 1, 0)
|
|
263
|
+
self._rebuild_list()
|
|
264
|
+
def _current(self):
|
|
265
|
+
if not self._servers:
|
|
266
|
+
return None
|
|
267
|
+
return self._servers[min(self._selected, len(self._servers) - 1)]
|
|
268
|
+
|
|
269
|
+
def on_button_pressed(self, event):
|
|
270
|
+
eid = event.button.id
|
|
271
|
+
if eid in ("mcp-cancel", "mcp-close"):
|
|
272
|
+
self.dismiss(None)
|
|
273
|
+
elif eid == "mcp-add":
|
|
274
|
+
self.app.push_screen(_AddMcpForm(), self._on_saved)
|
|
275
|
+
elif eid == "mcp-edit":
|
|
276
|
+
server = self._current()
|
|
277
|
+
if server:
|
|
278
|
+
self.app.push_screen(_AddMcpForm(server), self._on_saved)
|
|
279
|
+
elif eid == "mcp-remove":
|
|
280
|
+
server = self._current()
|
|
281
|
+
if server:
|
|
282
|
+
mcp.remove_server(server.get("id", ""))
|
|
283
|
+
self._servers = mcp.load_servers()
|
|
284
|
+
self._selected = min(self._selected, max(0, len(self._servers) - 1))
|
|
285
|
+
self._rebuild_list()
|
|
286
|
+
elif eid == "mcp-connect":
|
|
287
|
+
self._toggle_connect()
|
|
288
|
+
elif eid == "mcp-save":
|
|
289
|
+
mcp.save_servers(self._servers)
|
|
290
|
+
try:
|
|
291
|
+
self.app._system_note(
|
|
292
|
+
f"\u2713 MCP servers saved \u2014 {len(self._servers)} configured.")
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
self.dismiss(None)
|
|
296
|
+
|
|
297
|
+
def _toggle_connect(self):
|
|
298
|
+
"""One contextual Connect/Disconnect action: disconnects a
|
|
299
|
+
connected server, connects a disconnected one."""
|
|
300
|
+
server = self._current()
|
|
301
|
+
if not server:
|
|
302
|
+
return
|
|
303
|
+
if server.get("status") == "connected":
|
|
304
|
+
mcp.disconnect_server(server.get("id", ""))
|
|
305
|
+
server["status"] = "disconnected"
|
|
306
|
+
server["version"] = ""
|
|
307
|
+
server["latency_ms"] = None
|
|
308
|
+
server["tools"] = []
|
|
309
|
+
self._rebuild_list()
|
|
310
|
+
return
|
|
311
|
+
server["status"] = "connecting"
|
|
312
|
+
self._rebuild_list()
|
|
313
|
+
|
|
314
|
+
def _run():
|
|
315
|
+
ok, msg = mcp.connect_server(server)
|
|
316
|
+
live = mcp.server_status(server)
|
|
317
|
+
server["status"] = "connected" if ok else "failed"
|
|
318
|
+
server["version"] = live["version"]
|
|
319
|
+
server["latency_ms"] = live["latency_ms"]
|
|
320
|
+
server["tools"] = list(live["tools"])
|
|
321
|
+
if ok:
|
|
322
|
+
server["last_connected"] = int(time.time())
|
|
323
|
+
self.app.call_from_thread(self._rebuild_list)
|
|
324
|
+
try:
|
|
325
|
+
if self.app._system_note:
|
|
326
|
+
self.app._system_note(f"MCP '{server.get('name', '?')}': {msg}")
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
threading.Thread(target=_run, daemon=True).start()
|
|
331
|
+
|
|
332
|
+
def _on_saved(self, server):
|
|
333
|
+
if not server:
|
|
334
|
+
return
|
|
335
|
+
mcp.add_server(server)
|
|
336
|
+
self._servers = mcp.load_servers()
|
|
337
|
+
self._selected = len(self._servers) - 1
|
|
338
|
+
self._rebuild_list()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class _AddMcpForm(Screen):
|
|
342
|
+
"""Add/Edit MCP server popup (v0.7.8.1 redesign): centered,
|
|
343
|
+
animated, equal-width buttons, HTTP / STDIO toggle, URL,
|
|
344
|
+
headers, authentication, and a live Test Connection button.
|
|
345
|
+
Pass an existing server record to edit it in place."""
|
|
346
|
+
|
|
347
|
+
CSS = """
|
|
348
|
+
_AddMcpForm { align: center middle; background: $app-background 70%; }
|
|
349
|
+
#amf-box {
|
|
350
|
+
width: 66; height: auto; max-height: 92%; background: $surface;
|
|
351
|
+
border: round $border; padding: 0;
|
|
352
|
+
opacity: 0; offset-y: 1;
|
|
353
|
+
transition: opacity 150ms, offset 180ms;
|
|
354
|
+
}
|
|
355
|
+
#amf-box.open { opacity: 1; offset-y: 0; }
|
|
356
|
+
#amf-titlebar { height: 3; padding: 1 2 0 2; border-bottom: solid $border; }
|
|
357
|
+
#amf-title { text-style: bold; width: 1fr; }
|
|
358
|
+
#amf-body { padding: 1 2; overflow-y: auto; scrollbar-gutter: stable; }
|
|
359
|
+
#amf-kind { height: 4; padding: 1 2 0 2; }
|
|
360
|
+
#amf-kind Button { margin-right: 1; }
|
|
361
|
+
#amf-status { color: $text-faint; height: 1; padding-top: 1; }
|
|
362
|
+
#amf-actions { height: 5; padding: 0 2 1 2; border-top: solid $border; }
|
|
363
|
+
#amf-actions Button { margin-right: 1; width: 1fr; min-width: 10; }
|
|
364
|
+
.amf-hint { color: $text-faint; padding: 0 2 1 2; }
|
|
365
|
+
#amf-box.cct-compact .amf-hint { display: none; }
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
BINDINGS = [Binding("escape", "cancel", "Cancel")]
|
|
369
|
+
|
|
370
|
+
def __init__(self, server=None):
|
|
371
|
+
super().__init__()
|
|
372
|
+
self._server = server or {}
|
|
373
|
+
self._kind = self._server.get("kind", "remote")
|
|
374
|
+
self._tested_ok = False
|
|
375
|
+
|
|
376
|
+
def compose(self):
|
|
377
|
+
with Vertical(id="amf-box"):
|
|
378
|
+
with Horizontal(id="amf-titlebar"):
|
|
379
|
+
yield Static("Add MCP Server" if not self._server else "Edit MCP Server",
|
|
380
|
+
id="amf-title")
|
|
381
|
+
yield Button("\u2715", id="amf-close", classes="cct-popup-close")
|
|
382
|
+
with Vertical(id="amf-body"):
|
|
383
|
+
yield Static("Server Name", classes="cct-field-label")
|
|
384
|
+
yield Input(placeholder="My Tool Server", id="amf-name",
|
|
385
|
+
value=self._server.get("name", ""))
|
|
386
|
+
yield Static("Connection Type", classes="cct-field-label")
|
|
387
|
+
with Horizontal(id="amf-kind"):
|
|
388
|
+
yield Button("HTTP", id="amf-kind-remote",
|
|
389
|
+
variant="primary" if self._kind == "remote" else "default",
|
|
390
|
+
classes="cct-btn cct-btn-sm")
|
|
391
|
+
yield Button("STDIO", id="amf-kind-local",
|
|
392
|
+
variant="primary" if self._kind == "local" else "default",
|
|
393
|
+
classes="cct-btn cct-btn-sm")
|
|
394
|
+
yield Static("Endpoint URL (HTTP only \u2014 must end in /mcp or /jsonrpc)",
|
|
395
|
+
classes="cct-field-label amf-remote-field")
|
|
396
|
+
yield Input(placeholder="http://localhost:3000/mcp", id="amf-url",
|
|
397
|
+
value=self._server.get("url", ""),
|
|
398
|
+
classes="amf-remote-field on")
|
|
399
|
+
yield Static("Command (STDIO only \u2014 e.g. npx -y server, argument per word)",
|
|
400
|
+
classes="cct-field-label amf-local-field")
|
|
401
|
+
yield Input(placeholder="npx -y my-mcp-server", id="amf-command",
|
|
402
|
+
value=self._server.get("command", ""),
|
|
403
|
+
classes="amf-local-field")
|
|
404
|
+
yield Static("Extra headers (optional, format: Key: Value)",
|
|
405
|
+
classes="cct-field-label amf-remote-field")
|
|
406
|
+
yield Input(placeholder="Authorization: Bearer abc", id="amf-header",
|
|
407
|
+
value=_headers_to_str(self._server.get("headers") or {}),
|
|
408
|
+
classes="amf-remote-field")
|
|
409
|
+
yield Static("Authentication token (optional \u2014 sent as a Bearer header)",
|
|
410
|
+
classes="cct-field-label amf-remote-field")
|
|
411
|
+
yield Input(placeholder="sk-...", id="amf-auth",
|
|
412
|
+
value=self._server.get("auth_token", ""),
|
|
413
|
+
password=True, classes="amf-remote-field")
|
|
414
|
+
yield Static("", id="amf-status")
|
|
415
|
+
with Horizontal(id="amf-actions"):
|
|
416
|
+
yield Button(" \u2699 Test Connection ", id="amf-test", classes="cct-btn")
|
|
417
|
+
yield Button("Add", id="amf-add", variant="primary", classes="cct-btn")
|
|
418
|
+
yield Button("Cancel", id="amf-cancel", classes="cct-btn")
|
|
419
|
+
yield Static("Enter to confirm \u00b7 Esc to cancel", classes="amf-hint")
|
|
420
|
+
|
|
421
|
+
def _apply_kind(self):
|
|
422
|
+
kind = self._kind
|
|
423
|
+
for name in ("command", "url", "header", "auth"):
|
|
424
|
+
try:
|
|
425
|
+
field = self.query_one(f"#amf-{name}", Input)
|
|
426
|
+
except Exception:
|
|
427
|
+
continue
|
|
428
|
+
show_field = (name == "command" and kind == "local") or \
|
|
429
|
+
(name in ("url", "header", "auth") and kind == "remote")
|
|
430
|
+
field.display = show_field
|
|
431
|
+
for cls in ("amf-remote-field", "amf-local-field"):
|
|
432
|
+
for w in self.query(f".{cls}"):
|
|
433
|
+
shown = (cls == "amf-local-field" and kind == "local") or \
|
|
434
|
+
(cls == "amf-remote-field" and kind == "remote")
|
|
435
|
+
w.display = shown
|
|
436
|
+
self.query_one("#amf-kind-remote", Button).variant = \
|
|
437
|
+
"primary" if kind == "remote" else "default"
|
|
438
|
+
self.query_one("#amf-kind-local", Button).variant = \
|
|
439
|
+
"primary" if kind == "local" else "default"
|
|
440
|
+
self._fit()
|
|
441
|
+
|
|
442
|
+
def on_mount(self):
|
|
443
|
+
self._apply_kind()
|
|
444
|
+
self.call_after_refresh(lambda: self.query_one("#amf-box").add_class("open"))
|
|
445
|
+
self.call_after_refresh(self._fit)
|
|
446
|
+
try:
|
|
447
|
+
self.query_one("#amf-name", Input).focus()
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
|
|
451
|
+
def on_resize(self, event):
|
|
452
|
+
self._fit()
|
|
453
|
+
|
|
454
|
+
def _fit(self):
|
|
455
|
+
try:
|
|
456
|
+
self.call_after_refresh(
|
|
457
|
+
lambda: theme_css.fit_dialog(self, "amf-box", "amf-body"))
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
def action_cancel(self):
|
|
462
|
+
self.dismiss(None)
|
|
463
|
+
|
|
464
|
+
def on_click(self, event):
|
|
465
|
+
wid = getattr(event.widget, "id", None) if getattr(event, "widget", None) else None
|
|
466
|
+
if wid == "amf-kind-remote":
|
|
467
|
+
self._kind = "remote"
|
|
468
|
+
self._apply_kind()
|
|
469
|
+
elif wid == "amf-kind-local":
|
|
470
|
+
self._kind = "local"
|
|
471
|
+
self._apply_kind()
|
|
472
|
+
elif getattr(event, "widget", None) in (self, None):
|
|
473
|
+
self.dismiss(None)
|
|
474
|
+
|
|
475
|
+
def on_key(self, event):
|
|
476
|
+
if event.key == "escape":
|
|
477
|
+
self.dismiss(None)
|
|
478
|
+
|
|
479
|
+
def on_button_pressed(self, event):
|
|
480
|
+
eid = event.button.id
|
|
481
|
+
if eid == "amf-add":
|
|
482
|
+
self._submit()
|
|
483
|
+
elif eid == "amf-cancel" or eid == "amf-close":
|
|
484
|
+
self.dismiss(None)
|
|
485
|
+
elif eid == "amf-test":
|
|
486
|
+
self._test_connection()
|
|
487
|
+
else:
|
|
488
|
+
self.on_click(type("e", (), {"widget": event.button})())
|
|
489
|
+
|
|
490
|
+
def on_input_submitted(self, event):
|
|
491
|
+
if event.input.id == "amf-name":
|
|
492
|
+
self._submit()
|
|
493
|
+
|
|
494
|
+
def _build_record(self):
|
|
495
|
+
server = dict(self._server)
|
|
496
|
+
name = self.query_one("#amf-name", Input).value.strip()
|
|
497
|
+
server["name"] = name
|
|
498
|
+
server["kind"] = self._kind
|
|
499
|
+
if self._kind == "local":
|
|
500
|
+
server["command"] = self.query_one("#amf-command", Input).value.strip()
|
|
501
|
+
else:
|
|
502
|
+
url = self.query_one("#amf-url", Input).value.strip()
|
|
503
|
+
if url:
|
|
504
|
+
server["url"] = url
|
|
505
|
+
header = self.query_one("#amf-header", Input).value.strip()
|
|
506
|
+
headers = {}
|
|
507
|
+
if header and ":" in header:
|
|
508
|
+
k, _, v = header.partition(":")
|
|
509
|
+
headers[k.strip()] = v.strip()
|
|
510
|
+
auth = self.query_one("#amf-auth", Input).value.strip()
|
|
511
|
+
if auth:
|
|
512
|
+
headers["Authorization"] = f"Bearer {auth}"
|
|
513
|
+
server["headers"] = headers
|
|
514
|
+
server.setdefault("enabled", True)
|
|
515
|
+
server.setdefault("status", "disconnected")
|
|
516
|
+
server.setdefault("version", "")
|
|
517
|
+
server.setdefault("latency_ms", None)
|
|
518
|
+
server.setdefault("tools", [])
|
|
519
|
+
return server
|
|
520
|
+
|
|
521
|
+
def _submit(self):
|
|
522
|
+
server = self._build_record()
|
|
523
|
+
if not server.get("name"):
|
|
524
|
+
self.query_one("#amf-status", Static).update(
|
|
525
|
+
f"[{theme_css.current_hex('error')}]\u2717 Name is required[/]")
|
|
526
|
+
return
|
|
527
|
+
if self._kind == "local" and not server.get("command"):
|
|
528
|
+
self.query_one("#amf-status", Static).update(
|
|
529
|
+
f"[{theme_css.current_hex('error')}]\u2717 Command is required for STDIO[/]")
|
|
530
|
+
return
|
|
531
|
+
if self._kind == "remote" and not str(server.get("url", "")).startswith("http"):
|
|
532
|
+
self.query_one("#amf-status", Static).update(
|
|
533
|
+
f"[{theme_css.current_hex('error')}]\u2717 A valid HTTP endpoint URL is required[/]")
|
|
534
|
+
return
|
|
535
|
+
self.dismiss(server)
|
|
536
|
+
|
|
537
|
+
def _test_connection(self):
|
|
538
|
+
server = self._build_record()
|
|
539
|
+
if not server.get("name"):
|
|
540
|
+
self.query_one("#amf-status", Static).update(
|
|
541
|
+
f"[{theme_css.current_hex('error')}]\u2717 Name is required before testing[/]")
|
|
542
|
+
return
|
|
543
|
+
status = self.query_one("#amf-status", Static)
|
|
544
|
+
status.update(
|
|
545
|
+
f"[{theme_css.current_hex('warning')}]\U0001f7e1 Testing connection\u2026[/]")
|
|
546
|
+
test_server = dict(server)
|
|
547
|
+
test_server["id"] = f"test-{int(time.time())}"
|
|
548
|
+
test_server["status"] = "connecting"
|
|
549
|
+
|
|
550
|
+
def _run():
|
|
551
|
+
try:
|
|
552
|
+
ok, msg = mcp.connect_server(test_server)
|
|
553
|
+
except Exception as e:
|
|
554
|
+
ok, msg = False, str(e)
|
|
555
|
+
self.app.call_from_thread(
|
|
556
|
+
lambda: status.update(
|
|
557
|
+
f"[{theme_css.current_hex('success' if ok else 'error')}]"
|
|
558
|
+
f"{'\u2713' if ok else '\u2717'} {msg}[/]"))
|
|
559
|
+
|
|
560
|
+
threading.Thread(target=_run, daemon=True).start()
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _headers_to_str(headers):
|
|
564
|
+
if not headers:
|
|
565
|
+
return ""
|
|
566
|
+
return ", ".join(f"{k}: {v}" for k, v in headers.items())
|
|
567
|
+
|
|
568
|
+
else:
|
|
569
|
+
McpServersPanel = None
|
|
570
|
+
_AddMcpForm = None
|