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
calc_terminal/mcp.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT MCP — Model Context Protocol client (v0.7.8 BUG 5).
|
|
3
|
+
|
|
4
|
+
Replaces the old "not wired up yet" placeholder with a real backend:
|
|
5
|
+
|
|
6
|
+
- Local MCP servers: spawned as child processes, JSON-RPC 2.0 over
|
|
7
|
+
stdio (the standard local transport — e.g. a local `npx`/python
|
|
8
|
+
MCP server binary).
|
|
9
|
+
- Remote MCP servers: JSON-RPC 2.0 over HTTP POST (one request per
|
|
10
|
+
call; SSE streaming transports are reported honestly as
|
|
11
|
+
unsupported rather than faked).
|
|
12
|
+
|
|
13
|
+
Every server reports: status (Connected/Disconnected/Testing), the MCP
|
|
14
|
+
protocol version it negotiated, measured latency (ms), and the tool
|
|
15
|
+
list it advertises (tools/list). `call_tool` executes a tool and
|
|
16
|
+
returns the text content of the response.
|
|
17
|
+
|
|
18
|
+
Persistence: ~/.cct_mcp_servers.json, a plain list of server records.
|
|
19
|
+
This module is UI-agnostic (no Textual imports) — the ui/mcp_panel.py
|
|
20
|
+
screen renders it.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import subprocess
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
MCP_FILE = os.path.join(os.path.expanduser("~"), ".cct_mcp_servers.json")
|
|
32
|
+
_PROTOCOL_VERSION = "2024-11-05"
|
|
33
|
+
_REQUEST_TIMEOUT = 20 # seconds, for both transports
|
|
34
|
+
|
|
35
|
+
_lock = threading.Lock()
|
|
36
|
+
_session_id = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _next_id():
|
|
40
|
+
global _session_id
|
|
41
|
+
with _lock:
|
|
42
|
+
_session_id += 1
|
|
43
|
+
return _session_id
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Persistence
|
|
48
|
+
|
|
49
|
+
def load_servers() -> list[dict]:
|
|
50
|
+
"""Load saved MCP server records. Never raises."""
|
|
51
|
+
try:
|
|
52
|
+
if os.path.exists(MCP_FILE):
|
|
53
|
+
with open(MCP_FILE, "r", encoding="utf-8") as f:
|
|
54
|
+
data = json.load(f)
|
|
55
|
+
if isinstance(data, list):
|
|
56
|
+
out = []
|
|
57
|
+
for entry in data:
|
|
58
|
+
if not isinstance(entry, dict):
|
|
59
|
+
continue
|
|
60
|
+
rec = {
|
|
61
|
+
"id": entry.get("id", ""),
|
|
62
|
+
"name": entry.get("name", ""),
|
|
63
|
+
"kind": entry.get("kind", "remote"), # "local" | "remote"
|
|
64
|
+
"command": entry.get("command", ""), # local: argv
|
|
65
|
+
"url": entry.get("url", ""), # remote: endpoint
|
|
66
|
+
"headers": entry.get("headers", {}),
|
|
67
|
+
"enabled": bool(entry.get("enabled", True)),
|
|
68
|
+
"status": "disconnected",
|
|
69
|
+
"version": entry.get("version", ""),
|
|
70
|
+
"latency_ms": entry.get("latency_ms"),
|
|
71
|
+
"tools": entry.get("tools", []),
|
|
72
|
+
"last_connected": entry.get("last_connected", 0),
|
|
73
|
+
}
|
|
74
|
+
out.append(rec)
|
|
75
|
+
return out
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def save_servers(servers: list[dict]) -> bool:
|
|
82
|
+
"""Persist server records. Returns True on success."""
|
|
83
|
+
try:
|
|
84
|
+
clean = []
|
|
85
|
+
for s in servers or []:
|
|
86
|
+
clean.append({
|
|
87
|
+
"id": s.get("id", ""),
|
|
88
|
+
"name": s.get("name", ""),
|
|
89
|
+
"kind": s.get("kind", "remote"),
|
|
90
|
+
"command": s.get("command", ""),
|
|
91
|
+
"url": s.get("url", ""),
|
|
92
|
+
"headers": s.get("headers", {}),
|
|
93
|
+
"enabled": bool(s.get("enabled", True)),
|
|
94
|
+
"version": s.get("version", ""),
|
|
95
|
+
"latency_ms": s.get("latency_ms"),
|
|
96
|
+
"tools": s.get("tools", []),
|
|
97
|
+
"last_connected": s.get("last_connected", 0),
|
|
98
|
+
})
|
|
99
|
+
with open(MCP_FILE, "w", encoding="utf-8") as f:
|
|
100
|
+
json.dump(clean, f, indent=2)
|
|
101
|
+
return True
|
|
102
|
+
except Exception:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def add_server(entry: dict) -> str:
|
|
107
|
+
"""Add (or update by id) a server record and persist. Returns the id."""
|
|
108
|
+
servers = load_servers()
|
|
109
|
+
sid = entry.get("id") or entry.get("name") or f"mcp-{int(time.time())}"
|
|
110
|
+
entry["id"] = sid
|
|
111
|
+
servers = [s for s in servers if s.get("id") != sid]
|
|
112
|
+
servers.append(entry)
|
|
113
|
+
save_servers(servers)
|
|
114
|
+
return sid
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def remove_server(server_id: str):
|
|
118
|
+
save_servers([s for s in load_servers() if s.get("id") != server_id])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# JSON-RPC helpers
|
|
123
|
+
|
|
124
|
+
def _rpc_request(req_id: int, method: str, params: dict) -> str:
|
|
125
|
+
return json.dumps({
|
|
126
|
+
"jsonrpc": "2.0",
|
|
127
|
+
"id": req_id,
|
|
128
|
+
"method": method,
|
|
129
|
+
"params": params,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _initialize_params() -> dict:
|
|
134
|
+
return {
|
|
135
|
+
"protocolVersion": _PROTOCOL_VERSION,
|
|
136
|
+
"capabilities": {},
|
|
137
|
+
"clientInfo": {"name": "cct", "version": "0.7.8"},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class _LocalTransport:
|
|
142
|
+
"""Child-process stdio transport with request/response correlation."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, argv: list[str]):
|
|
145
|
+
self._proc = subprocess.Popen(
|
|
146
|
+
argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
147
|
+
stderr=subprocess.DEVNULL, text=True, encoding="utf-8",
|
|
148
|
+
errors="replace", bufsize=1, creationflags=getattr(
|
|
149
|
+
subprocess, "CREATE_NO_WINDOW", 0))
|
|
150
|
+
self._read_lock = threading.Lock()
|
|
151
|
+
|
|
152
|
+
def read_response(self, req_id: int, timeout: float = _REQUEST_TIMEOUT):
|
|
153
|
+
deadline = time.time() + timeout
|
|
154
|
+
buf = ""
|
|
155
|
+
with self._read_lock:
|
|
156
|
+
while time.time() < deadline:
|
|
157
|
+
line = self._proc.stdout.readline()
|
|
158
|
+
if not line:
|
|
159
|
+
raise ConnectionError("MCP server closed its stdout")
|
|
160
|
+
buf += line
|
|
161
|
+
stripped = line.strip()
|
|
162
|
+
if not stripped:
|
|
163
|
+
continue
|
|
164
|
+
try:
|
|
165
|
+
msg = json.loads(stripped)
|
|
166
|
+
except Exception:
|
|
167
|
+
continue
|
|
168
|
+
if msg.get("id") == req_id:
|
|
169
|
+
return msg
|
|
170
|
+
raise TimeoutError(f"MCP request {req_id} timed out")
|
|
171
|
+
|
|
172
|
+
def send(self, payload: str):
|
|
173
|
+
self._proc.stdin.write(payload + "\n")
|
|
174
|
+
self._proc.stdin.flush()
|
|
175
|
+
|
|
176
|
+
def close(self):
|
|
177
|
+
try:
|
|
178
|
+
self._proc.stdin.close()
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
try:
|
|
182
|
+
self._proc.terminate()
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class _RemoteTransport:
|
|
188
|
+
"""HTTP POST JSON-RPC transport for remote MCP endpoints."""
|
|
189
|
+
|
|
190
|
+
def __init__(self, url: str, headers: dict | None = None):
|
|
191
|
+
self.url = url
|
|
192
|
+
self.headers = {"Content-Type": "application/json"}
|
|
193
|
+
for k, v in (headers or {}).items():
|
|
194
|
+
self.headers[k] = v
|
|
195
|
+
|
|
196
|
+
def read_response(self, req_id: int, timeout: float = _REQUEST_TIMEOUT):
|
|
197
|
+
raise NotImplementedError # response comes from send() directly
|
|
198
|
+
|
|
199
|
+
def send(self, payload: str):
|
|
200
|
+
import requests
|
|
201
|
+
resp = requests.post(self.url, data=payload.encode("utf-8"),
|
|
202
|
+
headers=self.headers, timeout=_REQUEST_TIMEOUT)
|
|
203
|
+
resp.raise_for_status()
|
|
204
|
+
return resp.json()
|
|
205
|
+
|
|
206
|
+
def close(self):
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Session object — one live connection to one server
|
|
212
|
+
|
|
213
|
+
class McpSession:
|
|
214
|
+
"""A live, single-request-at-a-time connection to an MCP server.
|
|
215
|
+
Tracks status/version/latency/tools exactly as the panel shows them."""
|
|
216
|
+
|
|
217
|
+
def __init__(self, server: dict):
|
|
218
|
+
self.server = dict(server)
|
|
219
|
+
self.status = "disconnected"
|
|
220
|
+
self.version = server.get("version", "")
|
|
221
|
+
self.latency_ms = None
|
|
222
|
+
self.tools = list(server.get("tools", []))
|
|
223
|
+
self.error = ""
|
|
224
|
+
self._transport = None
|
|
225
|
+
self._lock = threading.Lock()
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def name(self):
|
|
229
|
+
return self.server.get("name") or self.server.get("id") or "MCP"
|
|
230
|
+
|
|
231
|
+
def _open_transport(self):
|
|
232
|
+
kind = self.server.get("kind", "remote")
|
|
233
|
+
if kind == "local":
|
|
234
|
+
argv = self.server.get("command", "")
|
|
235
|
+
if isinstance(argv, str):
|
|
236
|
+
argv = argv.split()
|
|
237
|
+
if not argv:
|
|
238
|
+
raise ValueError("Local MCP server needs a command.")
|
|
239
|
+
return _LocalTransport(argv)
|
|
240
|
+
url = self.server.get("url", "")
|
|
241
|
+
if not url:
|
|
242
|
+
raise ValueError("Remote MCP server needs a URL.")
|
|
243
|
+
return _RemoteTransport(url, self.server.get("headers") or {})
|
|
244
|
+
|
|
245
|
+
def connect(self) -> tuple[bool, str]:
|
|
246
|
+
"""Initialize the session: handshake, then tools/list. Returns
|
|
247
|
+
(ok, message). Latency covers the initialize round trip."""
|
|
248
|
+
with self._lock:
|
|
249
|
+
start = time.time()
|
|
250
|
+
try:
|
|
251
|
+
transport = self._open_transport()
|
|
252
|
+
req = _next_id()
|
|
253
|
+
payload = _rpc_request(req, "initialize", _initialize_params())
|
|
254
|
+
if isinstance(transport, _RemoteTransport):
|
|
255
|
+
resp = transport.send(payload)
|
|
256
|
+
else:
|
|
257
|
+
transport.send(payload)
|
|
258
|
+
resp = transport.read_response(req)
|
|
259
|
+
self._transport = transport
|
|
260
|
+
except Exception as e:
|
|
261
|
+
self.status = "disconnected"
|
|
262
|
+
self.error = str(e)
|
|
263
|
+
self.latency_ms = None
|
|
264
|
+
return False, f"Could not connect: {e}"
|
|
265
|
+
try:
|
|
266
|
+
if isinstance(resp, dict):
|
|
267
|
+
if "error" in resp:
|
|
268
|
+
raise ConnectionError(resp["error"].get("message", "handshake error"))
|
|
269
|
+
self.version = (resp.get("result", {}) or {}).get(
|
|
270
|
+
"protocolVersion", "")
|
|
271
|
+
# fire-and-forget initialized notification
|
|
272
|
+
try:
|
|
273
|
+
notify = _rpc_request(_next_id(), "notifications/initialized", {})
|
|
274
|
+
if isinstance(transport, _RemoteTransport):
|
|
275
|
+
transport.send(notify)
|
|
276
|
+
else:
|
|
277
|
+
transport.send(notify)
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
self.latency_ms = int((time.time() - start) * 1000)
|
|
281
|
+
self.status = "connected"
|
|
282
|
+
self._load_tools()
|
|
283
|
+
return True, f"Connected \u2014 {len(self.tools)} tool(s) available."
|
|
284
|
+
except Exception as e:
|
|
285
|
+
self.status = "disconnected"
|
|
286
|
+
self.error = str(e)
|
|
287
|
+
return False, f"Handshake failed: {e}"
|
|
288
|
+
|
|
289
|
+
def _load_tools(self):
|
|
290
|
+
req = _next_id()
|
|
291
|
+
payload = _rpc_request(req, "tools/list", {})
|
|
292
|
+
try:
|
|
293
|
+
if isinstance(self._transport, _RemoteTransport):
|
|
294
|
+
resp = self._transport.send(payload)
|
|
295
|
+
else:
|
|
296
|
+
self._transport.send(payload)
|
|
297
|
+
resp = self._transport.read_response(req)
|
|
298
|
+
result = (resp or {}).get("result") or {}
|
|
299
|
+
tools = result.get("tools") or []
|
|
300
|
+
self.tools = [t.get("name", "?") for t in tools if isinstance(t, dict)]
|
|
301
|
+
except Exception:
|
|
302
|
+
self.tools = list(self.server.get("tools", []))
|
|
303
|
+
|
|
304
|
+
def call_tool(self, name: str, arguments: dict | None = None) -> str:
|
|
305
|
+
"""Execute a tool and return the text content of its result.
|
|
306
|
+
Raises on transport errors; returns the content string otherwise."""
|
|
307
|
+
if self.status != "connected" or self._transport is None:
|
|
308
|
+
raise ConnectionError(f"MCP server '{self.name}' is not connected.")
|
|
309
|
+
req = _next_id()
|
|
310
|
+
payload = _rpc_request(req, "tools/call", {
|
|
311
|
+
"name": name, "arguments": arguments or {},
|
|
312
|
+
})
|
|
313
|
+
with self._lock:
|
|
314
|
+
if isinstance(self._transport, _RemoteTransport):
|
|
315
|
+
resp = self._transport.send(payload)
|
|
316
|
+
else:
|
|
317
|
+
self._transport.send(payload)
|
|
318
|
+
resp = self._transport.read_response(req)
|
|
319
|
+
if "error" in (resp or {}):
|
|
320
|
+
raise RuntimeError(resp["error"].get("message", "tool call failed"))
|
|
321
|
+
result = resp.get("result") or {}
|
|
322
|
+
if result.get("isError"):
|
|
323
|
+
raise RuntimeError("MCP tool reported an error")
|
|
324
|
+
parts = []
|
|
325
|
+
for item in result.get("content") or []:
|
|
326
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
327
|
+
parts.append(item.get("text", ""))
|
|
328
|
+
return "\n".join(parts) or "(no text content)"
|
|
329
|
+
|
|
330
|
+
def close(self):
|
|
331
|
+
try:
|
|
332
|
+
if self._transport is not None:
|
|
333
|
+
self._transport.close()
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
self._transport = None
|
|
337
|
+
self.status = "disconnected"
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
# Manager-level API (what the panel and the app call)
|
|
342
|
+
|
|
343
|
+
_sessions: dict[str, McpSession] = {}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def get_session(server: dict) -> McpSession:
|
|
347
|
+
"""Return the live session for a server record, creating one if
|
|
348
|
+
needed. Sessions survive panel open/close; close_session() frees."""
|
|
349
|
+
sid = server.get("id")
|
|
350
|
+
with _lock:
|
|
351
|
+
s = _sessions.get(sid)
|
|
352
|
+
if s is None:
|
|
353
|
+
s = McpSession(server)
|
|
354
|
+
_sessions[sid] = s
|
|
355
|
+
else:
|
|
356
|
+
s.server = dict(server)
|
|
357
|
+
return s
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def connect_server(server: dict) -> tuple[bool, str]:
|
|
361
|
+
session = get_session(server)
|
|
362
|
+
return session.connect()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def disconnect_server(server_id: str):
|
|
366
|
+
with _lock:
|
|
367
|
+
s = _sessions.pop(server_id, None)
|
|
368
|
+
if s is not None:
|
|
369
|
+
s.close()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def server_status(server: dict) -> dict:
|
|
373
|
+
"""Current live status of a server record (panel-friendly dict)."""
|
|
374
|
+
session = get_session(server)
|
|
375
|
+
return {
|
|
376
|
+
"status": session.status,
|
|
377
|
+
"version": session.version,
|
|
378
|
+
"latency_ms": session.latency_ms,
|
|
379
|
+
"tools": list(session.tools),
|
|
380
|
+
"error": session.error,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def call_server_tool(server: dict, name: str, arguments: dict | None = None) -> str:
|
|
385
|
+
session = get_session(server)
|
|
386
|
+
return session.call_tool(name, arguments)
|