@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0

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 (287) hide show
  1. package/README.md +132 -0
  2. package/ciel-runtime-menu.py +56 -6
  3. package/ciel_runtime.py +9379 -35909
  4. package/ciel_runtime_support/advisor_client.py +193 -0
  5. package/ciel_runtime_support/advisor_policy.py +320 -0
  6. package/ciel_runtime_support/advisor_refinement.py +160 -0
  7. package/ciel_runtime_support/advisor_request_builder.py +261 -0
  8. package/ciel_runtime_support/agy_installer.py +169 -0
  9. package/ciel_runtime_support/agy_mcp_restore.py +182 -0
  10. package/ciel_runtime_support/anthropic_model_policy.py +186 -0
  11. package/ciel_runtime_support/anthropic_response_writer.py +255 -0
  12. package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
  13. package/ciel_runtime_support/api_key_cooldown.py +159 -0
  14. package/ciel_runtime_support/architecture.py +488 -1
  15. package/ciel_runtime_support/architecture_budget.py +42 -0
  16. package/ciel_runtime_support/channel_backlog.py +90 -0
  17. package/ciel_runtime_support/channel_cli.py +119 -0
  18. package/ciel_runtime_support/channel_compact_injection.py +82 -0
  19. package/ciel_runtime_support/channel_compact_poll.py +67 -0
  20. package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
  21. package/ciel_runtime_support/channel_config_service.py +281 -0
  22. package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
  23. package/ciel_runtime_support/channel_connection_registry.py +128 -0
  24. package/ciel_runtime_support/channel_connection_worker.py +284 -0
  25. package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
  26. package/ciel_runtime_support/channel_cursor_repository.py +89 -0
  27. package/ciel_runtime_support/channel_cursor_service.py +178 -0
  28. package/ciel_runtime_support/channel_event_identity.py +212 -0
  29. package/ciel_runtime_support/channel_event_projection.py +315 -0
  30. package/ciel_runtime_support/channel_inflight.py +127 -0
  31. package/ciel_runtime_support/channel_injection.py +115 -0
  32. package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
  33. package/ciel_runtime_support/channel_launch_policy.py +180 -0
  34. package/ciel_runtime_support/channel_llm_context.py +156 -0
  35. package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
  36. package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
  37. package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
  38. package/ciel_runtime_support/channel_mcp_tools.py +240 -0
  39. package/ciel_runtime_support/channel_mcp_transport.py +394 -0
  40. package/ciel_runtime_support/channel_message_dedupe.py +65 -0
  41. package/ciel_runtime_support/channel_message_policy.py +256 -0
  42. package/ciel_runtime_support/channel_message_prompt.py +305 -0
  43. package/ciel_runtime_support/channel_message_repository.py +234 -0
  44. package/ciel_runtime_support/channel_notification_projection.py +217 -0
  45. package/ciel_runtime_support/channel_panel.py +162 -0
  46. package/ciel_runtime_support/channel_pending_injection.py +209 -0
  47. package/ciel_runtime_support/channel_pending_poll.py +109 -0
  48. package/ciel_runtime_support/channel_probe_cache.py +433 -0
  49. package/ciel_runtime_support/channel_probe_report.py +101 -0
  50. package/ciel_runtime_support/channel_runtime_environment.py +181 -0
  51. package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
  52. package/ciel_runtime_support/channel_session_repository.py +90 -0
  53. package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
  54. package/ciel_runtime_support/channel_terminal_input.py +277 -0
  55. package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
  56. package/ciel_runtime_support/channel_tool_context.py +166 -0
  57. package/ciel_runtime_support/channel_transcript.py +414 -0
  58. package/ciel_runtime_support/channel_transcript_repository.py +96 -0
  59. package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
  60. package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
  61. package/ciel_runtime_support/chat_files.py +138 -0
  62. package/ciel_runtime_support/chat_http_controller.py +235 -0
  63. package/ciel_runtime_support/claude_environment.py +375 -0
  64. package/ciel_runtime_support/claude_router.py +247 -193
  65. package/ciel_runtime_support/cli_dispatch.py +792 -0
  66. package/ciel_runtime_support/cli_parser.py +165 -0
  67. package/ciel_runtime_support/cli_usage.py +100 -0
  68. package/ciel_runtime_support/codex_app_server.py +20 -5
  69. package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
  70. package/ciel_runtime_support/codex_cli.py +42 -6
  71. package/ciel_runtime_support/codex_config.py +323 -0
  72. package/ciel_runtime_support/codex_launch_configuration.py +240 -0
  73. package/ciel_runtime_support/codex_launch_policy.py +66 -0
  74. package/ciel_runtime_support/codex_mcp_integration.py +195 -0
  75. package/ciel_runtime_support/codex_mcp_restore.py +304 -0
  76. package/ciel_runtime_support/codex_model_catalog.py +133 -0
  77. package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
  78. package/ciel_runtime_support/codex_router.py +147 -1
  79. package/ciel_runtime_support/codex_session_repository.py +115 -0
  80. package/ciel_runtime_support/codex_session_selection.py +114 -0
  81. package/ciel_runtime_support/command_asset_installer.py +103 -0
  82. package/ciel_runtime_support/compatibility_probe.py +295 -0
  83. package/ciel_runtime_support/compatibility_protocol.py +251 -0
  84. package/ciel_runtime_support/compatibility_runtime.py +166 -0
  85. package/ciel_runtime_support/compatibility_test.py +370 -0
  86. package/ciel_runtime_support/config_migrations.py +307 -0
  87. package/ciel_runtime_support/config_repository.py +175 -0
  88. package/ciel_runtime_support/config_value_codec.py +64 -0
  89. package/ciel_runtime_support/configuration_cli.py +374 -0
  90. package/ciel_runtime_support/context_compaction.py +280 -0
  91. package/ciel_runtime_support/context_setup.py +208 -0
  92. package/ciel_runtime_support/context_summary_policy.py +392 -0
  93. package/ciel_runtime_support/credential_cli.py +104 -0
  94. package/ciel_runtime_support/credential_management.py +261 -0
  95. package/ciel_runtime_support/credentials.py +269 -0
  96. package/ciel_runtime_support/executable_discovery.py +141 -0
  97. package/ciel_runtime_support/github_copilot_oauth.py +335 -0
  98. package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
  99. package/ciel_runtime_support/header_forwarding.py +73 -0
  100. package/ciel_runtime_support/headless_config.py +221 -0
  101. package/ciel_runtime_support/http_response.py +129 -0
  102. package/ciel_runtime_support/install_diagnostics.py +149 -0
  103. package/ciel_runtime_support/kimi_identity.py +123 -0
  104. package/ciel_runtime_support/launch_diagnostics.py +204 -0
  105. package/ciel_runtime_support/launch_state.py +127 -0
  106. package/ciel_runtime_support/live_api_key_controller.py +58 -0
  107. package/ciel_runtime_support/llm_config_http.py +148 -0
  108. package/ciel_runtime_support/llm_option_config.py +259 -0
  109. package/ciel_runtime_support/llm_presentation_data.py +447 -0
  110. package/ciel_runtime_support/llm_presets.py +773 -0
  111. package/ciel_runtime_support/lm_studio_runtime.py +401 -0
  112. package/ciel_runtime_support/managed_mcp_config.py +144 -0
  113. package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
  114. package/ciel_runtime_support/managed_service_cleanup.py +89 -0
  115. package/ciel_runtime_support/mcp_config_reader.py +230 -0
  116. package/ciel_runtime_support/mcp_http_proxy.py +607 -0
  117. package/ciel_runtime_support/mcp_inventory.py +59 -0
  118. package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
  119. package/ciel_runtime_support/mcp_probe_codec.py +136 -0
  120. package/ciel_runtime_support/mcp_probe_transport.py +328 -0
  121. package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
  122. package/ciel_runtime_support/mcp_proxy_config.py +107 -0
  123. package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
  124. package/ciel_runtime_support/mcp_proxy_process.py +560 -0
  125. package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
  126. package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
  127. package/ciel_runtime_support/mcp_transport.py +146 -0
  128. package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
  129. package/ciel_runtime_support/model_catalog_projection.py +61 -0
  130. package/ciel_runtime_support/model_context_hints.py +109 -0
  131. package/ciel_runtime_support/model_panel.py +147 -0
  132. package/ciel_runtime_support/model_registry_repository.py +231 -0
  133. package/ciel_runtime_support/npm_runtime.py +191 -0
  134. package/ciel_runtime_support/ollama_catalog.py +462 -0
  135. package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
  136. package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
  137. package/ciel_runtime_support/ollama_context_sync.py +87 -0
  138. package/ciel_runtime_support/ollama_forwarding.py +449 -0
  139. package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
  140. package/ciel_runtime_support/openai_chat_router.py +64 -0
  141. package/ciel_runtime_support/openai_forwarding.py +194 -0
  142. package/ciel_runtime_support/openai_responses_router.py +291 -0
  143. package/ciel_runtime_support/openai_responses_stream.py +135 -0
  144. package/ciel_runtime_support/output_budget.py +89 -0
  145. package/ciel_runtime_support/package_lifecycle.py +217 -0
  146. package/ciel_runtime_support/plan_artifact_controller.py +104 -0
  147. package/ciel_runtime_support/prelaunch.py +959 -0
  148. package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
  149. package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
  150. package/ciel_runtime_support/prelaunch_terminal.py +764 -0
  151. package/ciel_runtime_support/process_control.py +708 -0
  152. package/ciel_runtime_support/prompt_compaction.py +322 -0
  153. package/ciel_runtime_support/prompt_injection.py +176 -0
  154. package/ciel_runtime_support/protocols/__init__.py +24 -0
  155. package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
  156. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
  157. package/ciel_runtime_support/protocols/chat_projection.py +315 -0
  158. package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
  159. package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
  160. package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
  161. package/ciel_runtime_support/protocols/ollama_response.py +231 -0
  162. package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
  163. package/ciel_runtime_support/protocols/openai_responses.py +271 -0
  164. package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
  165. package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
  166. package/ciel_runtime_support/provider_adapters.py +160 -0
  167. package/ciel_runtime_support/provider_catalog_sources.py +316 -0
  168. package/ciel_runtime_support/provider_choice.py +201 -0
  169. package/ciel_runtime_support/provider_compatibility.py +165 -0
  170. package/ciel_runtime_support/provider_config_mutations.py +361 -0
  171. package/ciel_runtime_support/provider_configuration_service.py +162 -0
  172. package/ciel_runtime_support/provider_context.py +308 -0
  173. package/ciel_runtime_support/provider_contract_projection.py +73 -0
  174. package/ciel_runtime_support/provider_descriptor.py +82 -0
  175. package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
  176. package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
  177. package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
  178. package/ciel_runtime_support/provider_limits.py +457 -0
  179. package/ciel_runtime_support/provider_model_identity.py +139 -0
  180. package/ciel_runtime_support/provider_model_selection.py +431 -0
  181. package/ciel_runtime_support/provider_model_specs.py +142 -0
  182. package/ciel_runtime_support/provider_models.py +263 -0
  183. package/ciel_runtime_support/provider_network.py +176 -0
  184. package/ciel_runtime_support/provider_option_cli.py +238 -0
  185. package/ciel_runtime_support/provider_option_panel.py +275 -0
  186. package/ciel_runtime_support/provider_option_status.py +192 -0
  187. package/ciel_runtime_support/provider_policy.py +101 -0
  188. package/ciel_runtime_support/provider_query_policy.py +67 -0
  189. package/ciel_runtime_support/provider_readiness.py +112 -0
  190. package/ciel_runtime_support/provider_request_access.py +131 -0
  191. package/ciel_runtime_support/provider_request_builder.py +250 -0
  192. package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
  193. package/ciel_runtime_support/provider_runtime_info.py +113 -0
  194. package/ciel_runtime_support/provider_runtime_modes.py +150 -0
  195. package/ciel_runtime_support/provider_sampling_policy.py +46 -0
  196. package/ciel_runtime_support/provider_status.py +145 -0
  197. package/ciel_runtime_support/provider_timeout_policy.py +184 -0
  198. package/ciel_runtime_support/provider_tool_policy.py +145 -0
  199. package/ciel_runtime_support/providers/__init__.py +65 -0
  200. package/ciel_runtime_support/providers/anthropic.py +160 -0
  201. package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
  202. package/ciel_runtime_support/providers/base.py +232 -0
  203. package/ciel_runtime_support/providers/catalog.py +326 -0
  204. package/ciel_runtime_support/providers/cloud.py +194 -0
  205. package/ciel_runtime_support/providers/constants.py +54 -0
  206. package/ciel_runtime_support/providers/deepseek.py +115 -0
  207. package/ciel_runtime_support/providers/fireworks.py +158 -0
  208. package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
  209. package/ciel_runtime_support/providers/kimi.py +297 -0
  210. package/ciel_runtime_support/providers/lm_studio.py +76 -0
  211. package/ciel_runtime_support/providers/meta.py +257 -0
  212. package/ciel_runtime_support/providers/native.py +194 -0
  213. package/ciel_runtime_support/providers/nim.py +69 -0
  214. package/ciel_runtime_support/providers/nvidia.py +158 -0
  215. package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
  216. package/ciel_runtime_support/providers/ollama.py +181 -0
  217. package/ciel_runtime_support/providers/ollama_context.py +195 -0
  218. package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
  219. package/ciel_runtime_support/providers/opencode.py +220 -0
  220. package/ciel_runtime_support/providers/opencode_go.py +37 -0
  221. package/ciel_runtime_support/providers/openrouter.py +57 -0
  222. package/ciel_runtime_support/providers/vllm.py +65 -0
  223. package/ciel_runtime_support/providers/zai.py +119 -0
  224. package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
  225. package/ciel_runtime_support/rate_limit_policy.py +117 -0
  226. package/ciel_runtime_support/rate_limit_repository.py +154 -0
  227. package/ciel_runtime_support/registry.py +46 -0
  228. package/ciel_runtime_support/request_shortcuts.py +253 -0
  229. package/ciel_runtime_support/request_trace.py +323 -0
  230. package/ciel_runtime_support/response_collection.py +209 -0
  231. package/ciel_runtime_support/router_access.py +238 -0
  232. package/ciel_runtime_support/router_client_lifecycle.py +366 -0
  233. package/ciel_runtime_support/router_health_policy.py +101 -0
  234. package/ciel_runtime_support/router_http.py +513 -0
  235. package/ciel_runtime_support/router_process_lifecycle.py +401 -0
  236. package/ciel_runtime_support/router_rate_limit_service.py +285 -0
  237. package/ciel_runtime_support/router_server_runtime.py +103 -0
  238. package/ciel_runtime_support/router_shortcuts.py +201 -0
  239. package/ciel_runtime_support/routing_fallback.py +73 -0
  240. package/ciel_runtime_support/runtime_activity_repository.py +143 -0
  241. package/ciel_runtime_support/runtime_adapters.py +104 -0
  242. package/ciel_runtime_support/runtime_command_factory.py +73 -0
  243. package/ciel_runtime_support/runtime_compatibility.py +50 -0
  244. package/ciel_runtime_support/runtime_constants.py +178 -0
  245. package/ciel_runtime_support/runtime_launch.py +1602 -0
  246. package/ciel_runtime_support/runtime_llm_options.py +312 -0
  247. package/ciel_runtime_support/runtime_logging.py +161 -0
  248. package/ciel_runtime_support/runtime_paths.py +157 -0
  249. package/ciel_runtime_support/runtime_restart.py +84 -0
  250. package/ciel_runtime_support/runtime_upgrade.py +149 -0
  251. package/ciel_runtime_support/secure_json_repository.py +55 -0
  252. package/ciel_runtime_support/session_import.py +356 -0
  253. package/ciel_runtime_support/settings_repository.py +8 -0
  254. package/ciel_runtime_support/slash_command_assets.py +211 -0
  255. package/ciel_runtime_support/sse_stream.py +57 -0
  256. package/ciel_runtime_support/sse_trace.py +225 -0
  257. package/ciel_runtime_support/statusline_script.py +593 -0
  258. package/ciel_runtime_support/statusline_settings.py +53 -0
  259. package/ciel_runtime_support/stream_chunk_policy.py +18 -0
  260. package/ciel_runtime_support/streaming_anthropic.py +1955 -0
  261. package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
  262. package/ciel_runtime_support/terminal_platform_io.py +127 -0
  263. package/ciel_runtime_support/timeout_profile.py +196 -0
  264. package/ciel_runtime_support/tool_dialects.py +85 -0
  265. package/ciel_runtime_support/tool_exposure_policy.py +63 -0
  266. package/ciel_runtime_support/tool_guard_hooks.py +218 -0
  267. package/ciel_runtime_support/tool_request_projection.py +96 -0
  268. package/ciel_runtime_support/tool_schema.py +483 -0
  269. package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
  270. package/ciel_runtime_support/ui_text.py +266 -0
  271. package/ciel_runtime_support/upstream_error_policy.py +104 -0
  272. package/ciel_runtime_support/upstream_retry.py +419 -0
  273. package/ciel_runtime_support/upstream_stream_io.py +106 -0
  274. package/ciel_runtime_support/usage_events.py +96 -0
  275. package/ciel_runtime_support/visible_stream_filters.py +130 -0
  276. package/ciel_runtime_support/web_endpoints.py +447 -0
  277. package/ciel_runtime_support/web_ui.py +915 -0
  278. package/ciel_runtime_support/web_ui_controller.py +189 -0
  279. package/ciel_runtime_support/windows_console_input.py +137 -0
  280. package/ciel_runtime_support/windows_console_mode.py +112 -0
  281. package/docs/Architecture.md +54 -0
  282. package/docs/Configuration.md +17 -0
  283. package/docs/Module-Map.md +1093 -26
  284. package/docs/Providers.md +46 -1
  285. package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
  286. package/npm-bin/run-ciel-runtime.js +22 -2
  287. package/package.json +9 -2
@@ -0,0 +1,560 @@
1
+ """MCP proxy stdio framing and process transport adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Callable
14
+
15
+ from .mcp_proxy_codec import _mcp_proxy_error_response
16
+
17
+
18
+ _MCP_PROXY_IO_LOCK = threading.Lock()
19
+
20
+
21
+ def _mcp_proxy_observe_stdout_line(
22
+ server_name: str,
23
+ line: bytes,
24
+ observe_json_message: Callable[..., Any],
25
+ ) -> None:
26
+ try:
27
+ text = line.decode("utf-8", errors="replace").strip()
28
+ if not text or not text.startswith("{"):
29
+ return
30
+ payload = json.loads(text)
31
+ except Exception:
32
+ return
33
+ observe_json_message(server_name, payload)
34
+
35
+
36
+ def _mcp_proxy_header_end(buffer: bytes) -> tuple[int, int] | None:
37
+ crlf = buffer.find(b"\r\n\r\n")
38
+ lf = buffer.find(b"\n\n")
39
+ candidates: list[tuple[int, int]] = []
40
+ if crlf >= 0:
41
+ candidates.append((crlf, 4))
42
+ if lf >= 0:
43
+ candidates.append((lf, 2))
44
+ return min(candidates, key=lambda item: item[0]) if candidates else None
45
+
46
+
47
+ def _mcp_proxy_frame_header(buffer: bytes) -> tuple[int, int, int] | None:
48
+ header = _mcp_proxy_header_end(buffer)
49
+ if not header:
50
+ return None
51
+ header_end, delimiter_len = header
52
+ length = _mcp_proxy_content_length(buffer[:header_end])
53
+ if length is None:
54
+ return None
55
+ return header_end, delimiter_len, length
56
+
57
+
58
+ def _mcp_proxy_content_length(header_bytes: bytes) -> int | None:
59
+ try:
60
+ header_text = header_bytes.decode("ascii", errors="replace")
61
+ except Exception:
62
+ return None
63
+ for line in re.split(r"\r?\n", header_text):
64
+ name, sep, value = line.partition(":")
65
+ if sep and name.strip().lower() == "content-length":
66
+ try:
67
+ length = int(value.strip())
68
+ except Exception:
69
+ return None
70
+ return length if length >= 0 else None
71
+ return None
72
+
73
+
74
+ class _McpStdoutObserver:
75
+ def __init__(self, server_name: str, observe_json_message: Callable[..., Any]) -> None:
76
+ self.server_name = server_name
77
+ self._observe_json_message = observe_json_message
78
+ self.buffer = bytearray()
79
+
80
+ def feed(self, chunk: bytes) -> None:
81
+ if not chunk:
82
+ return
83
+ self.buffer.extend(chunk)
84
+ self._drain()
85
+
86
+ def _drop_until_candidate(self) -> bool:
87
+ data = bytes(self.buffer)
88
+ if not data:
89
+ return False
90
+ stripped = data.lstrip()
91
+ if len(stripped) != len(data):
92
+ del self.buffer[: len(data) - len(stripped)]
93
+ data = stripped
94
+ if _mcp_proxy_frame_header(data) or data.startswith(b"{"):
95
+ return True
96
+ lowered = data.lower()
97
+ content_idx = lowered.find(b"content-length:")
98
+ json_idx = data.find(b"{")
99
+ candidates = [idx for idx in (content_idx, json_idx) if idx >= 0]
100
+ newline_idx = data.find(b"\n")
101
+ if candidates:
102
+ keep_from = min(candidates)
103
+ if newline_idx >= 0 and newline_idx < keep_from:
104
+ del self.buffer[: newline_idx + 1]
105
+ elif keep_from > 0:
106
+ del self.buffer[:keep_from]
107
+ return True
108
+ if newline_idx >= 0:
109
+ del self.buffer[: newline_idx + 1]
110
+ return True
111
+ if len(self.buffer) > 1024 * 1024:
112
+ del self.buffer[:-4096]
113
+ return False
114
+
115
+ def _drain(self) -> None:
116
+ while self.buffer:
117
+ if not self._drop_until_candidate():
118
+ return
119
+ data = bytes(self.buffer)
120
+ frame = _mcp_proxy_frame_header(data)
121
+ if frame:
122
+ header_end, delimiter_len, length = frame
123
+ body_start = header_end + delimiter_len
124
+ body_end = body_start + length
125
+ if len(data) < body_end:
126
+ return
127
+ body = data[body_start:body_end]
128
+ del self.buffer[:body_end]
129
+ try:
130
+ payload = json.loads(body.decode("utf-8", errors="replace"))
131
+ except Exception:
132
+ continue
133
+ self._observe_json_message(self.server_name, payload)
134
+ continue
135
+ if data.startswith(b"{"):
136
+ newline_idx = data.find(b"\n")
137
+ if newline_idx < 0:
138
+ return
139
+ line = data[:newline_idx]
140
+ del self.buffer[: newline_idx + 1]
141
+ _mcp_proxy_observe_stdout_line(self.server_name, line, self._observe_json_message)
142
+ continue
143
+ return
144
+
145
+
146
+ def _mcp_proxy_forward_stdin(proc: subprocess.Popen[bytes], *, log: Callable[..., Any]) -> None:
147
+ try:
148
+ stdin_fd = sys.stdin.fileno()
149
+ while True:
150
+ chunk = os.read(stdin_fd, 65536)
151
+ if not chunk:
152
+ break
153
+ if proc.stdin:
154
+ proc.stdin.write(chunk)
155
+ proc.stdin.flush()
156
+ except Exception as exc:
157
+ log("WARN", f"mcp_proxy_stdin_forward_failed error={type(exc).__name__}: {exc}")
158
+ finally:
159
+ try:
160
+ if proc.stdin:
161
+ proc.stdin.close()
162
+ except Exception as exc:
163
+ log("WARN", f"mcp_proxy_stdin_close_failed error={type(exc).__name__}: {exc}")
164
+
165
+
166
+ def _mcp_proxy_stdio_mode(server: dict[str, Any]) -> str:
167
+ mode = str(server.get("ciel_runtime_stdio") or server.get("stdio_mode") or "").strip().lower()
168
+ if mode in ("jsonl", "json-lines", "json_lines", "newline-json", "line-json"):
169
+ return "jsonl"
170
+ return "framed"
171
+
172
+
173
+ def _mcp_proxy_write_proc_jsonl(proc: subprocess.Popen[bytes], body: bytes) -> None:
174
+ line = body.strip()
175
+ if not line or not proc.stdin:
176
+ return
177
+ proc.stdin.write(line + b"\n")
178
+ proc.stdin.flush()
179
+
180
+
181
+ def _mcp_proxy_drain_stdin_jsonl_buffer(proc: subprocess.Popen[bytes], buffer: bytearray, *, final: bool = False) -> None:
182
+ while buffer:
183
+ data = bytes(buffer)
184
+ stripped = data.lstrip()
185
+ if len(stripped) != len(data):
186
+ del buffer[: len(data) - len(stripped)]
187
+ data = stripped
188
+ frame = _mcp_proxy_frame_header(data)
189
+ if frame:
190
+ header_end, delimiter_len, length = frame
191
+ body_start = header_end + delimiter_len
192
+ body_end = body_start + length
193
+ if len(data) < body_end:
194
+ return
195
+ body = data[body_start:body_end]
196
+ del buffer[:body_end]
197
+ _mcp_proxy_write_proc_jsonl(proc, body)
198
+ continue
199
+ if data.startswith(b"{"):
200
+ newline_idx = data.find(b"\n")
201
+ if newline_idx >= 0:
202
+ line = data[:newline_idx]
203
+ del buffer[: newline_idx + 1]
204
+ _mcp_proxy_write_proc_jsonl(proc, line)
205
+ continue
206
+ if final:
207
+ del buffer[:]
208
+ _mcp_proxy_write_proc_jsonl(proc, data)
209
+ return
210
+ lowered = data.lower()
211
+ content_idx = lowered.find(b"content-length:")
212
+ json_idx = data.find(b"{")
213
+ candidates = [idx for idx in (content_idx, json_idx) if idx >= 0]
214
+ newline_idx = data.find(b"\n")
215
+ if candidates:
216
+ keep_from = min(candidates)
217
+ if keep_from > 0:
218
+ del buffer[:keep_from]
219
+ continue
220
+ if newline_idx >= 0:
221
+ del buffer[: newline_idx + 1]
222
+ continue
223
+ if len(buffer) > 1024 * 1024:
224
+ del buffer[:-4096]
225
+ return
226
+
227
+
228
+ def _mcp_proxy_forward_stdin_jsonl(proc: subprocess.Popen[bytes], *, log: Callable[..., Any]) -> None:
229
+ buffer = bytearray()
230
+ try:
231
+ stdin_fd = sys.stdin.fileno()
232
+ while True:
233
+ chunk = os.read(stdin_fd, 65536)
234
+ if not chunk:
235
+ break
236
+ buffer.extend(chunk)
237
+ _mcp_proxy_drain_stdin_jsonl_buffer(proc, buffer)
238
+ _mcp_proxy_drain_stdin_jsonl_buffer(proc, buffer, final=True)
239
+ except Exception as exc:
240
+ log("WARN", f"mcp_proxy_jsonl_stdin_forward_failed error={type(exc).__name__}: {exc}")
241
+ finally:
242
+ try:
243
+ if proc.stdin:
244
+ proc.stdin.close()
245
+ except Exception as exc:
246
+ log("WARN", f"mcp_proxy_jsonl_stdin_close_failed error={type(exc).__name__}: {exc}")
247
+
248
+
249
+ def _mcp_proxy_write_stdout_frame(body: bytes) -> None:
250
+ with _MCP_PROXY_IO_LOCK:
251
+ sys.stdout.buffer.write(b"Content-Length: " + str(len(body)).encode("ascii") + b"\r\n\r\n" + body)
252
+ sys.stdout.buffer.flush()
253
+
254
+
255
+ # Framing the Streamable HTTP proxy uses when replying to the MCP client on
256
+ # stdout. Claude Code's stdio MCP client speaks newline-delimited JSON (JSONL);
257
+ # replying with LSP-style Content-Length frames makes Claude Code fail to
258
+ # connect ("Failed to connect"). The stdio proxy paths are unaffected -- they
259
+ # keep using _mcp_proxy_write_stdout_frame directly. Default JSONL (Claude
260
+ # Code's format); switched to "framed" if the client actually sends frames.
261
+ _MCP_PROXY_HTTP_CLIENT_FRAMING = "jsonl"
262
+
263
+
264
+ def _mcp_proxy_set_http_client_framing(mode: str) -> None:
265
+ global _MCP_PROXY_HTTP_CLIENT_FRAMING
266
+ if mode in ("jsonl", "framed"):
267
+ _MCP_PROXY_HTTP_CLIENT_FRAMING = mode
268
+
269
+
270
+ def _mcp_proxy_write_client_message(body: bytes) -> None:
271
+ with _MCP_PROXY_IO_LOCK:
272
+ if _MCP_PROXY_HTTP_CLIENT_FRAMING == "framed":
273
+ sys.stdout.buffer.write(b"Content-Length: " + str(len(body)).encode("ascii") + b"\r\n\r\n" + body)
274
+ else:
275
+ sys.stdout.buffer.write(body.strip() + b"\n")
276
+ sys.stdout.buffer.flush()
277
+
278
+
279
+ def _mcp_proxy_write_json_response(payload: dict[str, Any]) -> None:
280
+ _mcp_proxy_write_client_message(json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))
281
+
282
+
283
+ def _mcp_proxy_drain_input_messages(buffer: bytearray, *, final: bool = False) -> list[dict[str, Any]]:
284
+ messages: list[dict[str, Any]] = []
285
+ while buffer:
286
+ data = bytes(buffer)
287
+ stripped = data.lstrip()
288
+ if len(stripped) != len(data):
289
+ del buffer[: len(data) - len(stripped)]
290
+ data = stripped
291
+ frame = _mcp_proxy_frame_header(data)
292
+ if frame:
293
+ # Client sent an LSP-style Content-Length frame: reply in kind.
294
+ _mcp_proxy_set_http_client_framing("framed")
295
+ header_end, delimiter_len, length = frame
296
+ body_start = header_end + delimiter_len
297
+ body_end = body_start + length
298
+ if len(data) < body_end:
299
+ return messages
300
+ body = data[body_start:body_end]
301
+ del buffer[:body_end]
302
+ elif data.startswith(b"{"):
303
+ # Client sent newline-delimited JSON (Claude Code): reply in kind.
304
+ _mcp_proxy_set_http_client_framing("jsonl")
305
+ newline_idx = data.find(b"\n")
306
+ if newline_idx < 0:
307
+ if not final:
308
+ return messages
309
+ body = bytes(data)
310
+ del buffer[:]
311
+ else:
312
+ body = data[:newline_idx]
313
+ del buffer[: newline_idx + 1]
314
+ else:
315
+ content_idx = data.lower().find(b"content-length:")
316
+ json_idx = data.find(b"{")
317
+ candidates = [idx for idx in (content_idx, json_idx) if idx >= 0]
318
+ if candidates:
319
+ keep_from = min(candidates)
320
+ if keep_from > 0:
321
+ del buffer[:keep_from]
322
+ continue
323
+ newline_idx = data.find(b"\n")
324
+ if newline_idx >= 0:
325
+ del buffer[: newline_idx + 1]
326
+ continue
327
+ if len(buffer) > 1024 * 1024:
328
+ del buffer[:-4096]
329
+ return messages
330
+ try:
331
+ payload = json.loads(body.decode("utf-8", errors="replace"))
332
+ except Exception as exc:
333
+ _mcp_proxy_write_json_response(_mcp_proxy_error_response(None, f"invalid JSON-RPC payload: {type(exc).__name__}", -32700))
334
+ continue
335
+ if isinstance(payload, dict):
336
+ messages.append(payload)
337
+ return messages
338
+
339
+
340
+ def _mcp_proxy_emit_jsonl_stdout_line(
341
+ server_name: str,
342
+ line: bytes,
343
+ *,
344
+ observe_json_message: Callable[..., Any],
345
+ log: Callable[..., Any],
346
+ ) -> None:
347
+ body = line.strip()
348
+ if not body:
349
+ return
350
+ try:
351
+ payload = json.loads(body.decode("utf-8", errors="replace"))
352
+ except Exception:
353
+ try:
354
+ sys.stderr.buffer.write(body + b"\n")
355
+ sys.stderr.buffer.flush()
356
+ except Exception as write_exc:
357
+ log(
358
+ "WARN",
359
+ f"mcp_proxy_invalid_stdout_log_failed server={server_name} "
360
+ f"error={type(write_exc).__name__}: {write_exc}",
361
+ )
362
+ return
363
+ observe_json_message(server_name, payload)
364
+ _mcp_proxy_write_stdout_frame(body)
365
+
366
+
367
+ def _mcp_proxy_forward_stdout_jsonl(
368
+ server_name: str,
369
+ proc: subprocess.Popen[bytes],
370
+ *,
371
+ observe_json_message: Callable[..., Any],
372
+ log: Callable[..., Any],
373
+ ) -> None:
374
+ if not proc.stdout:
375
+ return
376
+ buffer = bytearray()
377
+ while True:
378
+ chunk = proc.stdout.read(65536)
379
+ if not chunk:
380
+ break
381
+ buffer.extend(chunk)
382
+ while True:
383
+ newline_idx = buffer.find(b"\n")
384
+ if newline_idx < 0:
385
+ break
386
+ line = bytes(buffer[:newline_idx])
387
+ del buffer[: newline_idx + 1]
388
+ _mcp_proxy_emit_jsonl_stdout_line(server_name, line, observe_json_message=observe_json_message, log=log)
389
+ if buffer.strip():
390
+ _mcp_proxy_emit_jsonl_stdout_line(server_name, bytes(buffer), observe_json_message=observe_json_message, log=log)
391
+
392
+
393
+ def _mcp_proxy_forward_stderr(proc: subprocess.Popen[bytes], *, log: Callable[..., Any]) -> None:
394
+ try:
395
+ if not proc.stderr:
396
+ return
397
+ while True:
398
+ chunk = proc.stderr.read(4096)
399
+ if not chunk:
400
+ break
401
+ sys.stderr.buffer.write(chunk)
402
+ sys.stderr.buffer.flush()
403
+ except Exception as exc:
404
+ log("WARN", f"mcp_proxy_stderr_forward_failed error={type(exc).__name__}: {exc}")
405
+
406
+
407
+ def _mcp_proxy_streamable_http_request(
408
+ endpoint: str,
409
+ headers: dict[str, str],
410
+ payload: dict[str, Any],
411
+ timeout: float,
412
+ protocol_version: str,
413
+ session_id: str | None,
414
+ *,
415
+ post_json: Callable[..., Any],
416
+ ) -> tuple[Any, str | None]:
417
+ return post_json(endpoint, headers, payload, timeout, protocol_version, session_id)
418
+
419
+
420
+ @dataclass(frozen=True, slots=True)
421
+ class McpStdioConfigPorts:
422
+ read: Callable[[Path], Any]
423
+ is_stdio: Callable[[dict[str, Any]], bool]
424
+ resolve_process: Callable[[str, list[str]], tuple[str, list[str]]]
425
+ environment: Callable[[], dict[str, str]]
426
+
427
+
428
+ @dataclass(frozen=True, slots=True)
429
+ class McpStdioTransportPorts:
430
+ popen: Callable[..., Any]
431
+ stdio_mode: Callable[[dict[str, Any]], str]
432
+ forward_stdin: Callable[[Any], None]
433
+ forward_stdin_jsonl: Callable[[Any], None]
434
+ forward_stdout_jsonl: Callable[[str, Any], None]
435
+ forward_stderr: Callable[[Any], None]
436
+ observer: Callable[[str], Any]
437
+
438
+
439
+ @dataclass(frozen=True, slots=True)
440
+ class McpStdioEffects:
441
+ log: Callable[[str, str], None]
442
+ error: Callable[[str], None]
443
+ start_thread: Callable[[Callable[..., Any], tuple[Any, ...], str], None]
444
+ write_stdout: Callable[[bytes], None]
445
+ flush_stdout: Callable[[], None]
446
+
447
+
448
+ @dataclass(frozen=True, slots=True)
449
+ class McpStdioProxyService:
450
+ config: McpStdioConfigPorts
451
+ transport: McpStdioTransportPorts
452
+ effects: McpStdioEffects
453
+
454
+ def run(self, server_name: str, server_config_path: Path) -> int:
455
+ try:
456
+ server = self.config.read(server_config_path)
457
+ except Exception as exc:
458
+ self.effects.log(
459
+ "ERROR",
460
+ f"mcp_proxy_config_read_failed server={server_name} "
461
+ f"error={type(exc).__name__}: {exc}",
462
+ )
463
+ self.effects.error(
464
+ "ciel-runtime mcp-proxy: cannot read server config: "
465
+ f"{type(exc).__name__}: {exc}"
466
+ )
467
+ return 2
468
+ if not isinstance(server, dict) or not self.config.is_stdio(server):
469
+ self.effects.log(
470
+ "ERROR", f"mcp_proxy_invalid_config server={server_name}"
471
+ )
472
+ self.effects.error(
473
+ "ciel-runtime mcp-proxy: server config is not a stdio MCP server"
474
+ )
475
+ return 2
476
+
477
+ command = str(server.get("command") or "").strip()
478
+ args = (
479
+ [str(item) for item in server.get("args", [])]
480
+ if isinstance(server.get("args"), list)
481
+ else []
482
+ )
483
+ command, args = self.config.resolve_process(command, args)
484
+ env = self.config.environment()
485
+ raw_env = server.get("env")
486
+ if isinstance(raw_env, dict):
487
+ env.update(
488
+ {str(key): str(value) for key, value in raw_env.items() if str(key)}
489
+ )
490
+ cwd_value = server.get("cwd") or server.get("workingDirectory")
491
+ cwd = str(cwd_value) if cwd_value else None
492
+ try:
493
+ proc = self.transport.popen(
494
+ [command, *args],
495
+ stdin=subprocess.PIPE,
496
+ stdout=subprocess.PIPE,
497
+ stderr=subprocess.PIPE,
498
+ cwd=cwd,
499
+ env=env,
500
+ bufsize=0,
501
+ )
502
+ except Exception as exc:
503
+ self.effects.log(
504
+ "ERROR",
505
+ f"mcp_proxy_start_failed server={server_name} command={command} "
506
+ f"error={type(exc).__name__}: {exc}",
507
+ )
508
+ self.effects.error(
509
+ f"ciel-runtime mcp-proxy: failed to start {command}: "
510
+ f"{type(exc).__name__}: {exc}"
511
+ )
512
+ return 127
513
+
514
+ stdio_mode = self.transport.stdio_mode(server)
515
+ self.effects.log(
516
+ "INFO",
517
+ f"mcp_proxy_started server={server_name} command={command} "
518
+ f"stdio={stdio_mode}",
519
+ )
520
+ stdin_target = (
521
+ self.transport.forward_stdin_jsonl
522
+ if stdio_mode == "jsonl"
523
+ else self.transport.forward_stdin
524
+ )
525
+ self.effects.start_thread(
526
+ stdin_target, (proc,), f"mcp-proxy-stdin-{server_name}"
527
+ )
528
+ self.effects.start_thread(
529
+ self.transport.forward_stderr,
530
+ (proc,),
531
+ f"mcp-proxy-stderr-{server_name}",
532
+ )
533
+ try:
534
+ if stdio_mode == "jsonl":
535
+ self.transport.forward_stdout_jsonl(server_name, proc)
536
+ elif proc.stdout:
537
+ observer = self.transport.observer(server_name)
538
+ while True:
539
+ chunk = proc.stdout.read(65536)
540
+ if not chunk:
541
+ break
542
+ observer.feed(chunk)
543
+ self.effects.write_stdout(chunk)
544
+ self.effects.flush_stdout()
545
+ rc = proc.wait()
546
+ self.effects.log(
547
+ "INFO" if rc == 0 else "WARN",
548
+ f"mcp_proxy_exited server={server_name} rc={rc}",
549
+ )
550
+ return rc
551
+ finally:
552
+ if proc.poll() is None:
553
+ try:
554
+ proc.terminate()
555
+ except Exception as exc:
556
+ self.effects.log(
557
+ "WARN",
558
+ f"mcp_stdio_proxy_terminate_failed server={server_name} "
559
+ f"error={type(exc).__name__}: {exc}",
560
+ )