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.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. 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)