@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,345 @@
1
+ """JSON-RPC response and MCP tool-result codec without transport ownership."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import json
7
+ import os
8
+ import re
9
+ from typing import Any, Callable
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class McpProxyCodecPolicy:
14
+ default_tool_result_max_chars: int
15
+ item_text_chars: int
16
+ positive_env_int: Callable[..., Any]
17
+ router_log: Callable[..., Any]
18
+ tool_leaf_name: Callable[..., Any]
19
+ truncate_for_prompt: Callable[..., Any]
20
+
21
+
22
+ def _mcp_proxy_error_response(request_id: Any, message: str, code: int = -32000) -> dict[str, Any]:
23
+ return {
24
+ "jsonrpc": "2.0",
25
+ "id": request_id,
26
+ "error": {"code": code, "message": str(message)},
27
+ }
28
+
29
+
30
+ def _mcp_proxy_tool_call_name(payload: dict[str, Any]) -> str:
31
+ if str(payload.get("method") or "") != "tools/call":
32
+ return ""
33
+ params = payload.get("params") if isinstance(payload.get("params"), dict) else {}
34
+ return str(params.get("name") or "").strip()
35
+
36
+
37
+ def _mcp_proxy_tool_call_arguments(payload: dict[str, Any]) -> dict[str, Any]:
38
+ params = payload.get("params") if isinstance(payload.get("params"), dict) else {}
39
+ arguments = params.get("arguments") if isinstance(params.get("arguments"), dict) else {}
40
+ return arguments
41
+
42
+
43
+ def _mcp_proxy_tool_is_notification_wait(tool_name: str) -> bool:
44
+ normalized = re.sub(r"[^a-z0-9_]+", "_", str(tool_name or "").strip().lower()).strip("_")
45
+ if not normalized:
46
+ return False
47
+ waits = normalized.startswith(("wait_", "watch_")) or normalized in {"wait", "watch"}
48
+ if not waits:
49
+ return False
50
+ return any(
51
+ term in normalized
52
+ for term in (
53
+ "notification",
54
+ "notifications",
55
+ "message",
56
+ "messages",
57
+ "event",
58
+ "events",
59
+ "response",
60
+ "responses",
61
+ "inbox",
62
+ "mailbox",
63
+ "channel",
64
+ "channels",
65
+ )
66
+ )
67
+
68
+
69
+ def _mcp_proxy_wait_timeout_seconds(arguments: dict[str, Any]) -> float:
70
+ def _float_env(name: str, default: float) -> float:
71
+ try:
72
+ return float(str(os.environ.get(name, default)).strip())
73
+ except Exception:
74
+ return default
75
+
76
+ default_timeout = max(0.0, min(60.0, _float_env("CIEL_RUNTIME_MCP_WAIT_DEFAULT_SECONDS", 10.0)))
77
+ max_timeout = max(1.0, min(120.0, _float_env("CIEL_RUNTIME_MCP_WAIT_MAX_SECONDS", 30.0)))
78
+ value: Any = None
79
+ scale = 1.0
80
+ for key in ("timeout_ms", "wait_ms", "poll_ms"):
81
+ if key in arguments:
82
+ value = arguments.get(key)
83
+ scale = 0.001
84
+ break
85
+ if value is None:
86
+ for key in ("timeout_seconds", "wait_seconds"):
87
+ if key in arguments:
88
+ value = arguments.get(key)
89
+ scale = 1.0
90
+ break
91
+ if value is None and "timeout" in arguments:
92
+ value = arguments.get("timeout")
93
+ scale = 0.001 if isinstance(value, (int, float)) and float(value) > 1000 else 1.0
94
+ if value is None:
95
+ return min(default_timeout, max_timeout)
96
+ try:
97
+ timeout = float(value) * scale
98
+ except Exception:
99
+ timeout = default_timeout
100
+ return max(0.0, min(max_timeout, timeout))
101
+
102
+
103
+ def _mcp_proxy_notification_wait_response(
104
+ request_id: Any,
105
+ server_name: str,
106
+ notifications: list[dict[str, Any]],
107
+ *,
108
+ timed_out: bool,
109
+ ) -> dict[str, Any]:
110
+ status = "timeout" if timed_out and not notifications else "ok"
111
+ body = {
112
+ "status": status,
113
+ "source": server_name,
114
+ "count": len(notifications),
115
+ "notifications": notifications,
116
+ }
117
+ return {
118
+ "jsonrpc": "2.0",
119
+ "id": request_id,
120
+ "result": {
121
+ "content": [
122
+ {
123
+ "type": "text",
124
+ "text": json.dumps(body, ensure_ascii=False, separators=(",", ":"), default=str),
125
+ }
126
+ ],
127
+ "isError": False,
128
+ },
129
+ }
130
+
131
+
132
+ def _mcp_proxy_tool_result_max_chars(*, policy: McpProxyCodecPolicy) -> int:
133
+ positive_env_int = policy.positive_env_int
134
+ MCP_PROXY_TOOL_RESULT_MAX_CHARS_DEFAULT = policy.default_tool_result_max_chars
135
+ return max(
136
+ 4000,
137
+ min(200000, positive_env_int("CIEL_RUNTIME_MCP_TOOL_RESULT_MAX_CHARS", MCP_PROXY_TOOL_RESULT_MAX_CHARS_DEFAULT)),
138
+ )
139
+
140
+
141
+ def _mcp_proxy_tool_result_is_message_read(
142
+ tool_name: str, *, policy: McpProxyCodecPolicy
143
+ ) -> bool:
144
+ _mcp_tool_leaf_name = policy.tool_leaf_name
145
+ normalized = re.sub(r"[^a-z0-9_]+", "_", _mcp_tool_leaf_name(tool_name)).strip("_")
146
+ if not normalized:
147
+ return False
148
+ if not normalized.startswith(("get_", "list_", "read_", "search_", "fetch_")):
149
+ return False
150
+ return any(term in normalized for term in ("message", "messages", "notification", "notifications", "inbox"))
151
+
152
+
153
+ def _mcp_proxy_compact_metadata_for_tool_result(
154
+ value: Any, *, policy: McpProxyCodecPolicy
155
+ ) -> Any:
156
+ truncate_for_prompt = policy.truncate_for_prompt
157
+ if not isinstance(value, dict):
158
+ return value
159
+ out: dict[str, Any] = {}
160
+ for key in (
161
+ "id",
162
+ "message_id",
163
+ "source_message_id",
164
+ "room_id",
165
+ "channel",
166
+ "thread_id",
167
+ "sender_id",
168
+ "sender_name",
169
+ "author_name",
170
+ "kind",
171
+ "type",
172
+ "created_at",
173
+ "updated_at",
174
+ "stream_id",
175
+ "seq",
176
+ "sequence",
177
+ ):
178
+ if key in value and value.get(key) is not None:
179
+ item = value.get(key)
180
+ out[key] = truncate_for_prompt(item, 1000) if isinstance(item, str) else item
181
+ if "snapshot" in value and isinstance(value.get("snapshot"), dict):
182
+ out["snapshot_keys"] = sorted(str(key) for key in value["snapshot"].keys())[:40]
183
+ remaining_keys = sorted(str(key) for key in value.keys() if key not in out and key != "snapshot")
184
+ if remaining_keys:
185
+ out["keys"] = remaining_keys[:80]
186
+ return out or {"keys": sorted(str(key) for key in value.keys())[:80]}
187
+
188
+
189
+ def _mcp_proxy_compact_message_item_for_tool_result(
190
+ item: Any, text_limit: int, *, policy: McpProxyCodecPolicy
191
+ ) -> Any:
192
+ truncate_for_prompt = policy.truncate_for_prompt
193
+ if not isinstance(item, dict):
194
+ if isinstance(item, str):
195
+ return truncate_for_prompt(item, text_limit)
196
+ return item
197
+ out: dict[str, Any] = {}
198
+ scalar_keys = (
199
+ "id",
200
+ "message_id",
201
+ "room_id",
202
+ "channel",
203
+ "thread_id",
204
+ "parent_id",
205
+ "sender_type",
206
+ "sender_id",
207
+ "sender_name",
208
+ "author_name",
209
+ "kind",
210
+ "type",
211
+ "created_at",
212
+ "updated_at",
213
+ "timestamp",
214
+ "stream_id",
215
+ "seq",
216
+ "sequence",
217
+ )
218
+ text_keys = ("content", "message", "text", "body", "summary")
219
+ for key in scalar_keys:
220
+ if key in item and item.get(key) is not None:
221
+ value = item.get(key)
222
+ out[key] = truncate_for_prompt(value, 1000) if isinstance(value, str) else value
223
+ for key in text_keys:
224
+ if key in item and item.get(key) is not None:
225
+ value = item.get(key)
226
+ if isinstance(value, str):
227
+ out[key] = truncate_for_prompt(value, text_limit)
228
+ else:
229
+ out[key] = value
230
+ metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else None
231
+ if metadata:
232
+ out["metadata"] = _mcp_proxy_compact_metadata_for_tool_result(metadata, policy=policy)
233
+ for key, value in item.items():
234
+ if key in out or key == "metadata":
235
+ continue
236
+ if isinstance(value, (str, int, float, bool)) or value is None:
237
+ out[key] = truncate_for_prompt(value, 1000) if isinstance(value, str) else value
238
+ if len(out) >= 30:
239
+ break
240
+ return out
241
+
242
+
243
+ def _mcp_proxy_compact_message_json_for_tool_result(
244
+ parsed: Any, original_chars: int, max_chars: int, *, policy: McpProxyCodecPolicy
245
+ ) -> str:
246
+ MCP_PROXY_TOOL_RESULT_ITEM_TEXT_CHARS = policy.item_text_chars
247
+ truncate_for_prompt = policy.truncate_for_prompt
248
+ item_limit = max(1000, min(MCP_PROXY_TOOL_RESULT_ITEM_TEXT_CHARS, max_chars // 3))
249
+ if isinstance(parsed, dict):
250
+ out: dict[str, Any] = {}
251
+ for key, value in parsed.items():
252
+ if isinstance(value, (str, int, float, bool)) or value is None:
253
+ out[key] = truncate_for_prompt(value, 1000) if isinstance(value, str) else value
254
+ for key in ("messages", "data", "items", "results", "notifications"):
255
+ value = parsed.get(key)
256
+ if not isinstance(value, list):
257
+ continue
258
+ kept_items = value[:20]
259
+ out[key] = [_mcp_proxy_compact_message_item_for_tool_result(item, item_limit, policy=policy) for item in kept_items]
260
+ if len(value) > len(kept_items):
261
+ out[f"{key}_omitted"] = len(value) - len(kept_items)
262
+ break
263
+ else:
264
+ out["data"] = _mcp_proxy_compact_message_item_for_tool_result(parsed, item_limit, policy=policy)
265
+ elif isinstance(parsed, list):
266
+ kept_items = parsed[:20]
267
+ out = {"data": [_mcp_proxy_compact_message_item_for_tool_result(item, item_limit, policy=policy) for item in kept_items]}
268
+ if len(parsed) > len(kept_items):
269
+ out["data_omitted"] = len(parsed) - len(kept_items)
270
+ else:
271
+ return truncate_for_prompt(str(parsed), max_chars)
272
+ out["ciel_runtime_compacted"] = True
273
+ out["ciel_runtime_original_chars"] = original_chars
274
+ out["ciel_runtime_note"] = (
275
+ "Large MCP message-read result compacted before returning to Claude Code. "
276
+ "Message text is preserved up to a bounded size; bulky metadata is summarized."
277
+ )
278
+ text = json.dumps(out, ensure_ascii=False, indent=2, default=str)
279
+ return truncate_for_prompt(text, max_chars)
280
+
281
+
282
+ def _mcp_proxy_compact_tool_result_text(
283
+ tool_name: str, text: str, max_chars: int, *, policy: McpProxyCodecPolicy
284
+ ) -> str:
285
+ truncate_for_prompt = policy.truncate_for_prompt
286
+ original = str(text or "")
287
+ if len(original) <= max_chars:
288
+ return original
289
+ if not _mcp_proxy_tool_result_is_message_read(tool_name, policy=policy):
290
+ return original
291
+ try:
292
+ parsed = json.loads(original)
293
+ except Exception:
294
+ prefix = (
295
+ f"[ciel-runtime compacted MCP tool result: tool={tool_name or '-'} "
296
+ f"original_chars={len(original)} max_chars={max_chars}]\n"
297
+ )
298
+ return prefix + truncate_for_prompt(original, max(1000, max_chars - len(prefix)))
299
+ return _mcp_proxy_compact_message_json_for_tool_result(parsed, len(original), max_chars, policy=policy)
300
+
301
+
302
+ def compact_tool_result_response(
303
+ server_name: str, tool_name: str, payload: dict[str, Any], *, policy: McpProxyCodecPolicy
304
+ ) -> dict[str, Any]:
305
+ router_log = policy.router_log
306
+ if not isinstance(payload, dict) or not _mcp_proxy_tool_result_is_message_read(tool_name, policy=policy):
307
+ return payload
308
+ result = payload.get("result") if isinstance(payload.get("result"), dict) else None
309
+ if not isinstance(result, dict) or result.get("isError") or result.get("is_error"):
310
+ return payload
311
+ content = result.get("content")
312
+ if not isinstance(content, list):
313
+ return payload
314
+ max_chars = _mcp_proxy_tool_result_max_chars(policy=policy)
315
+ original_chars = 0
316
+ changed = False
317
+ new_content: list[Any] = []
318
+ for block in content:
319
+ if not isinstance(block, dict) or block.get("type") != "text":
320
+ new_content.append(block)
321
+ continue
322
+ text = str(block.get("text") or "")
323
+ original_chars += len(text)
324
+ compacted = _mcp_proxy_compact_tool_result_text(tool_name, text, max_chars, policy=policy)
325
+ if compacted != text:
326
+ changed = True
327
+ new_block = dict(block)
328
+ new_block["text"] = compacted
329
+ new_content.append(new_block)
330
+ else:
331
+ new_content.append(block)
332
+ if not changed:
333
+ return payload
334
+ out = dict(payload)
335
+ out_result = dict(result)
336
+ out_result["content"] = new_content
337
+ out["result"] = out_result
338
+ compacted_chars = sum(len(str(block.get("text") or "")) for block in new_content if isinstance(block, dict) and block.get("type") == "text")
339
+ router_log(
340
+ "INFO",
341
+ f"mcp_proxy_tool_result_compacted server={server_name} tool={tool_name or '-'} "
342
+ f"original_chars={original_chars} compacted_chars={compacted_chars}",
343
+ )
344
+ return out
345
+
@@ -0,0 +1,107 @@
1
+ """Configuration service materializing MCP servers through local proxy commands."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ import sys
7
+ from typing import Any, Callable
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class McpProxyConfigPaths:
12
+ output: Path
13
+ server_directory: Path
14
+ entrypoint: Path
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class McpProxyConfigPorts:
19
+ config_paths: Callable[[list[str], Path, Path | None], list[Path]]
20
+ read_servers: Callable[[Path, Path], list[tuple[str, dict[str, Any]]]]
21
+ is_streamable_http: Callable[[dict[str, Any]], bool]
22
+ force_proxy: Callable[[dict[str, Any]], bool]
23
+ is_stdio: Callable[[dict[str, Any]], bool]
24
+ safe_name: Callable[[str], str]
25
+ save_json: Callable[[Path, dict[str, Any], str], None]
26
+ log: Callable[[str, str], None]
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class McpProxyConfigService:
31
+ paths: McpProxyConfigPaths
32
+ ports: McpProxyConfigPorts
33
+
34
+ def write(
35
+ self,
36
+ passthrough: list[str],
37
+ *,
38
+ extra_config_paths: list[Path | str] | None = None,
39
+ force_proxy_server_names: set[str] | None = None,
40
+ disable_proxy_notification_stream_names: set[str] | None = None,
41
+ cwd: Path | None = None,
42
+ home: Path | None = None,
43
+ ) -> Path | None:
44
+ working_directory = cwd or Path.cwd()
45
+ forced_names = set(force_proxy_server_names or ())
46
+ disabled_stream_names = set(disable_proxy_notification_stream_names or ())
47
+ extra = [Path(item).expanduser() for item in (extra_config_paths or [])]
48
+ config_paths = [*extra, *self.ports.config_paths(passthrough, working_directory, home)]
49
+ servers: dict[str, Any] = {}
50
+ for path in config_paths:
51
+ if not path.exists() or not path.is_file():
52
+ continue
53
+ for name, server in self.ports.read_servers(path, working_directory):
54
+ if name in servers:
55
+ self.ports.log(
56
+ "INFO",
57
+ f"mcp_proxy_config_duplicate_overwritten server={name} source={path}",
58
+ )
59
+ streamable = self.ports.is_streamable_http(server)
60
+ forced_streamable = streamable and (
61
+ name in forced_names or self.ports.force_proxy(server)
62
+ )
63
+ if self.ports.is_stdio(server) or forced_streamable:
64
+ servers[name] = self._proxy_server(
65
+ name,
66
+ server,
67
+ disable_notification_stream=streamable and name in disabled_stream_names,
68
+ )
69
+ else:
70
+ servers[name] = server
71
+ if not servers:
72
+ return None
73
+ self.ports.save_json(
74
+ self.paths.output,
75
+ {"mcpServers": servers},
76
+ "mcp_proxy_config",
77
+ )
78
+ self.ports.log("INFO", f"mcp_proxy_config_written servers={','.join(sorted(servers))}")
79
+ return self.paths.output
80
+
81
+ def _proxy_server(
82
+ self,
83
+ name: str,
84
+ server: dict[str, Any],
85
+ *,
86
+ disable_notification_stream: bool,
87
+ ) -> dict[str, Any]:
88
+ self.paths.server_directory.mkdir(parents=True, exist_ok=True)
89
+ server_path = self.paths.server_directory / f"{self.ports.safe_name(name)}.json"
90
+ saved_server = dict(server)
91
+ if disable_notification_stream:
92
+ saved_server["ciel_runtime_disable_notification_stream"] = True
93
+ self.ports.save_json(server_path, saved_server, f"mcp_proxy_server:{name}")
94
+ return {
95
+ "command": sys.executable,
96
+ "args": [
97
+ str(self.paths.entrypoint),
98
+ "mcp-proxy",
99
+ "--server-name",
100
+ name,
101
+ "--server-config",
102
+ str(server_path),
103
+ ],
104
+ }
105
+
106
+
107
+ __all__ = ["McpProxyConfigPaths", "McpProxyConfigPorts", "McpProxyConfigService"]