@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.
- package/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""HTTP/SSE controller for the built-in Channel MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import urllib.parse
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from http.server import BaseHTTPRequestHandler
|
|
9
|
+
from threading import Condition, Lock
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class ChannelMcpSessionStore:
|
|
15
|
+
states: dict[str, dict[str, Any]]
|
|
16
|
+
lock: Lock
|
|
17
|
+
|
|
18
|
+
def create(self, session: str, last_id: int) -> None:
|
|
19
|
+
with self.lock:
|
|
20
|
+
self.states[session] = {
|
|
21
|
+
"created_at": time.time(),
|
|
22
|
+
"last_id": last_id,
|
|
23
|
+
"initialized": False,
|
|
24
|
+
"outbox": [],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def stream_state(self, session: str) -> tuple[int, bool] | None:
|
|
28
|
+
with self.lock:
|
|
29
|
+
state = self.states.get(session)
|
|
30
|
+
if not state:
|
|
31
|
+
return None
|
|
32
|
+
return int(state.get("last_id") or 0), bool(state.get("initialized"))
|
|
33
|
+
|
|
34
|
+
def set_last_id(self, session: str, last_id: int) -> None:
|
|
35
|
+
with self.lock:
|
|
36
|
+
if state := self.states.get(session):
|
|
37
|
+
state["last_id"] = last_id
|
|
38
|
+
|
|
39
|
+
def touch(self, session: str) -> None:
|
|
40
|
+
with self.lock:
|
|
41
|
+
if session and session in self.states:
|
|
42
|
+
self.states[session]["last_seen_at"] = time.time()
|
|
43
|
+
|
|
44
|
+
def initialize(self, session: str) -> None:
|
|
45
|
+
with self.lock:
|
|
46
|
+
if session and session in self.states:
|
|
47
|
+
self.states[session]["initialized"] = True
|
|
48
|
+
|
|
49
|
+
def remove(self, session: str) -> None:
|
|
50
|
+
with self.lock:
|
|
51
|
+
self.states.pop(session, None)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class ChannelMcpStreamServices:
|
|
56
|
+
new_session_id: Callable[[], str]
|
|
57
|
+
start_last_id: Callable[[BaseHTTPRequestHandler], int]
|
|
58
|
+
send_headers: Callable[[BaseHTTPRequestHandler], None]
|
|
59
|
+
write_event: Callable[..., None]
|
|
60
|
+
take_outbox: Callable[[str], list[dict[str, Any]]]
|
|
61
|
+
read_messages: Callable[[int, str | None, str | None, int], list[dict[str, Any]]]
|
|
62
|
+
project_notifications: Callable[[list[dict[str, Any]], str], tuple[int, list[tuple[int, dict[str, Any]]]]]
|
|
63
|
+
update_cursor: Callable[[int], None]
|
|
64
|
+
condition: Condition
|
|
65
|
+
log: Callable[[str, str], None]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class ChannelMcpRpcServices:
|
|
70
|
+
initialize_response: Callable[[Any, str], dict[str, Any]]
|
|
71
|
+
tool_schemas: Callable[[], list[dict[str, Any]]]
|
|
72
|
+
tool_call_response: Callable[[Any, dict[str, Any]], dict[str, Any]]
|
|
73
|
+
enqueue: Callable[[str, dict[str, Any]], bool]
|
|
74
|
+
write_json: Callable[..., None]
|
|
75
|
+
write_accepted: Callable[[BaseHTTPRequestHandler], None]
|
|
76
|
+
log: Callable[[str, str], None]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class ChannelMcpHttpController:
|
|
81
|
+
store: ChannelMcpSessionStore
|
|
82
|
+
stream: ChannelMcpStreamServices
|
|
83
|
+
rpc: ChannelMcpRpcServices
|
|
84
|
+
|
|
85
|
+
def get(self, handler: BaseHTTPRequestHandler, path: str) -> bool:
|
|
86
|
+
if path == "/ca/mcp/health":
|
|
87
|
+
self.rpc.write_json(
|
|
88
|
+
handler,
|
|
89
|
+
{
|
|
90
|
+
"ok": True,
|
|
91
|
+
"name": "ciel-runtime-router",
|
|
92
|
+
"streamable_http": "/ca/mcp",
|
|
93
|
+
"sse": "/ca/mcp/sse",
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
return True
|
|
97
|
+
if path != "/ca/mcp/sse":
|
|
98
|
+
return False
|
|
99
|
+
session = self.stream.new_session_id()
|
|
100
|
+
last_id = self.stream.start_last_id(handler)
|
|
101
|
+
self.store.create(session, last_id)
|
|
102
|
+
self.stream.log("INFO", f"channel_mcp_session_started session={session} last_id={last_id}")
|
|
103
|
+
started_at = time.time()
|
|
104
|
+
close_reason = "finished"
|
|
105
|
+
self.stream.send_headers(handler)
|
|
106
|
+
self.stream.write_event(
|
|
107
|
+
handler,
|
|
108
|
+
"endpoint",
|
|
109
|
+
f"/ca/mcp/messages?sessionId={urllib.parse.quote(session)}",
|
|
110
|
+
)
|
|
111
|
+
try:
|
|
112
|
+
while True:
|
|
113
|
+
current = self.store.stream_state(session)
|
|
114
|
+
if current is None:
|
|
115
|
+
close_reason = "session_missing"
|
|
116
|
+
return True
|
|
117
|
+
last_id, initialized = current
|
|
118
|
+
if outbox := self.stream.take_outbox(session):
|
|
119
|
+
for payload in outbox:
|
|
120
|
+
self.stream.write_event(handler, "message", payload)
|
|
121
|
+
self.stream.log("INFO", f"channel_mcp_rpc_flushed session={session} count={len(outbox)}")
|
|
122
|
+
continue
|
|
123
|
+
if not initialized:
|
|
124
|
+
handler.wfile.write(b": waiting-for-initialize\n\n")
|
|
125
|
+
handler.wfile.flush()
|
|
126
|
+
with self.stream.condition:
|
|
127
|
+
self.stream.condition.wait(timeout=1.0)
|
|
128
|
+
continue
|
|
129
|
+
messages = self.stream.read_messages(last_id, None, None, 100)
|
|
130
|
+
if messages:
|
|
131
|
+
delivered_id, events = self.stream.project_notifications(messages, session)
|
|
132
|
+
last_id = max(last_id, delivered_id)
|
|
133
|
+
for event_id, notification in events:
|
|
134
|
+
self.stream.write_event(handler, "message", notification, event_id)
|
|
135
|
+
self.stream.log(
|
|
136
|
+
"INFO",
|
|
137
|
+
f"channel_mcp_notification_written session={session} message_id={event_id}",
|
|
138
|
+
)
|
|
139
|
+
self.stream.update_cursor(last_id)
|
|
140
|
+
self.store.set_last_id(session, last_id)
|
|
141
|
+
continue
|
|
142
|
+
handler.wfile.write(b": keepalive\n\n")
|
|
143
|
+
handler.wfile.flush()
|
|
144
|
+
with self.stream.condition:
|
|
145
|
+
self.stream.condition.wait(timeout=5.0)
|
|
146
|
+
except (BrokenPipeError, ConnectionError, ConnectionResetError) as error:
|
|
147
|
+
close_reason = type(error).__name__
|
|
148
|
+
return True
|
|
149
|
+
except Exception as error:
|
|
150
|
+
close_reason = type(error).__name__
|
|
151
|
+
self.stream.log(
|
|
152
|
+
"ERROR",
|
|
153
|
+
f"channel_mcp_session_failed session={session} error={type(error).__name__}: {error}",
|
|
154
|
+
)
|
|
155
|
+
return True
|
|
156
|
+
finally:
|
|
157
|
+
age = time.time() - started_at
|
|
158
|
+
self.stream.log(
|
|
159
|
+
"INFO",
|
|
160
|
+
f"channel_mcp_session_closed session={session} reason={close_reason} age={age:.1f}s",
|
|
161
|
+
)
|
|
162
|
+
self.store.remove(session)
|
|
163
|
+
|
|
164
|
+
def post(
|
|
165
|
+
self,
|
|
166
|
+
handler: BaseHTTPRequestHandler,
|
|
167
|
+
path: str,
|
|
168
|
+
body: dict[str, Any],
|
|
169
|
+
) -> bool:
|
|
170
|
+
if path == "/ca/mcp":
|
|
171
|
+
response = self._rpc_response(body)
|
|
172
|
+
if response is None:
|
|
173
|
+
self.rpc.write_accepted(handler)
|
|
174
|
+
else:
|
|
175
|
+
self.rpc.write_json(handler, response)
|
|
176
|
+
self.rpc.log(
|
|
177
|
+
"INFO",
|
|
178
|
+
"channel_mcp_streamable_http method=%s request_id=%s"
|
|
179
|
+
% (str(body.get("method") or ""), body.get("id")),
|
|
180
|
+
)
|
|
181
|
+
return True
|
|
182
|
+
if path != "/ca/mcp/messages":
|
|
183
|
+
return False
|
|
184
|
+
query = urllib.parse.parse_qs(
|
|
185
|
+
urllib.parse.urlparse(handler.path).query,
|
|
186
|
+
keep_blank_values=True,
|
|
187
|
+
)
|
|
188
|
+
session = (query.get("sessionId") or query.get("session") or [""])[0]
|
|
189
|
+
self.store.touch(session)
|
|
190
|
+
method = str(body.get("method") or "")
|
|
191
|
+
request_id = body.get("id")
|
|
192
|
+
if method == "initialize":
|
|
193
|
+
self.store.initialize(session)
|
|
194
|
+
response = self._rpc_response(body)
|
|
195
|
+
if method == "initialize":
|
|
196
|
+
params = body.get("params") if isinstance(body.get("params"), dict) else {}
|
|
197
|
+
protocol = str(params.get("protocolVersion") or "2024-11-05")
|
|
198
|
+
self.rpc.log("INFO", f"channel_mcp_initialized session={session or '-'} protocol={protocol}")
|
|
199
|
+
if response is not None:
|
|
200
|
+
if not self.rpc.enqueue(session, response):
|
|
201
|
+
self.rpc.log("WARN", f"channel_mcp_rpc_enqueue_failed session={session or '-'} method={method}")
|
|
202
|
+
self.rpc.write_json(
|
|
203
|
+
handler,
|
|
204
|
+
{
|
|
205
|
+
"jsonrpc": "2.0",
|
|
206
|
+
"id": request_id,
|
|
207
|
+
"error": {"code": -32000, "message": "MCP SSE session is not connected"},
|
|
208
|
+
},
|
|
209
|
+
404,
|
|
210
|
+
)
|
|
211
|
+
return True
|
|
212
|
+
self.rpc.log(
|
|
213
|
+
"INFO",
|
|
214
|
+
f"channel_mcp_rpc_queued session={session or '-'} method={method} request_id={request_id}",
|
|
215
|
+
)
|
|
216
|
+
self.rpc.write_accepted(handler)
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
def _rpc_response(self, body: dict[str, Any]) -> dict[str, Any] | None:
|
|
220
|
+
method = str(body.get("method") or "")
|
|
221
|
+
request_id = body.get("id")
|
|
222
|
+
if method == "initialize":
|
|
223
|
+
params = body.get("params") if isinstance(body.get("params"), dict) else {}
|
|
224
|
+
protocol = str(params.get("protocolVersion") or "2024-11-05")
|
|
225
|
+
return self.rpc.initialize_response(request_id, protocol)
|
|
226
|
+
if method == "tools/list":
|
|
227
|
+
return {
|
|
228
|
+
"jsonrpc": "2.0",
|
|
229
|
+
"id": request_id,
|
|
230
|
+
"result": {"tools": self.rpc.tool_schemas()},
|
|
231
|
+
}
|
|
232
|
+
if method == "tools/call":
|
|
233
|
+
params = body.get("params") if isinstance(body.get("params"), dict) else {}
|
|
234
|
+
return self.rpc.tool_call_response(request_id, params)
|
|
235
|
+
if method == "ping":
|
|
236
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": {}}
|
|
237
|
+
if request_id is not None:
|
|
238
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": {}}
|
|
239
|
+
return None
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Ownership repository and lifecycle for MCP channel notification streams."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChannelProxyOwnershipRepository:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
proxy_config: Path,
|
|
16
|
+
disable_notifications: Callable[[dict[str, Any]], bool],
|
|
17
|
+
log: Callable[[str, str], None],
|
|
18
|
+
) -> None:
|
|
19
|
+
self.proxy_config = proxy_config
|
|
20
|
+
self.disable_notifications = disable_notifications
|
|
21
|
+
self.log = log
|
|
22
|
+
|
|
23
|
+
def owned_names(self) -> set[str]:
|
|
24
|
+
try:
|
|
25
|
+
data = json.loads(self.proxy_config.read_text(encoding="utf-8"))
|
|
26
|
+
except FileNotFoundError:
|
|
27
|
+
return set()
|
|
28
|
+
except (OSError, UnicodeError, json.JSONDecodeError, TypeError) as exc:
|
|
29
|
+
self.log(
|
|
30
|
+
"WARN",
|
|
31
|
+
f"proxy_owned_channel_config_read_failed path={self.proxy_config} "
|
|
32
|
+
f"error={type(exc).__name__}: {exc}",
|
|
33
|
+
)
|
|
34
|
+
return set()
|
|
35
|
+
servers = data.get("mcpServers") if isinstance(data, dict) else None
|
|
36
|
+
if not isinstance(servers, dict):
|
|
37
|
+
return set()
|
|
38
|
+
owned: set[str] = set()
|
|
39
|
+
for name, entry in servers.items():
|
|
40
|
+
if not isinstance(entry, dict) or not isinstance(entry.get("args"), list):
|
|
41
|
+
continue
|
|
42
|
+
arguments = [str(argument) for argument in entry["args"]]
|
|
43
|
+
if "mcp-proxy" not in arguments or "--server-name" not in arguments:
|
|
44
|
+
continue
|
|
45
|
+
try:
|
|
46
|
+
wrapped_name = arguments[arguments.index("--server-name") + 1].strip()
|
|
47
|
+
except IndexError:
|
|
48
|
+
wrapped_name = str(name).strip()
|
|
49
|
+
if self.server_config_disables_notifications(arguments):
|
|
50
|
+
continue
|
|
51
|
+
if wrapped_name:
|
|
52
|
+
owned.add(wrapped_name)
|
|
53
|
+
return owned
|
|
54
|
+
|
|
55
|
+
def server_config_disables_notifications(self, arguments: list[str]) -> bool:
|
|
56
|
+
try:
|
|
57
|
+
path = Path(arguments[arguments.index("--server-config") + 1])
|
|
58
|
+
except (ValueError, IndexError):
|
|
59
|
+
return False
|
|
60
|
+
try:
|
|
61
|
+
server = json.loads(path.read_text(encoding="utf-8"))
|
|
62
|
+
except (OSError, UnicodeError, json.JSONDecodeError, TypeError) as exc:
|
|
63
|
+
self.log(
|
|
64
|
+
"WARN",
|
|
65
|
+
f"proxy_server_config_read_failed path={path} "
|
|
66
|
+
f"error={type(exc).__name__}: {exc}",
|
|
67
|
+
)
|
|
68
|
+
return False
|
|
69
|
+
return (
|
|
70
|
+
self.disable_notifications(server) if isinstance(server, dict) else False
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class ChannelRouterLifecyclePorts:
|
|
76
|
+
delivery_enabled: Callable[..., bool]
|
|
77
|
+
launch_specs: Callable[[dict[str, Any], list[str]], list[str]]
|
|
78
|
+
server_names: Callable[[Iterable[str]], list[str]]
|
|
79
|
+
owned_names: Callable[[], set[str]]
|
|
80
|
+
public_name: Callable[[str], str]
|
|
81
|
+
ensure_probe: Callable[..., bool]
|
|
82
|
+
source_paths: Callable[[Iterable[str]], list[Path]]
|
|
83
|
+
auto_start: Callable[..., list[dict[str, Any]]]
|
|
84
|
+
log: Callable[[str, str], None]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ChannelRouterLifecycle:
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
native_router_names: frozenset[str],
|
|
91
|
+
ports: ChannelRouterLifecyclePorts,
|
|
92
|
+
) -> None:
|
|
93
|
+
self.native_router_names = native_router_names
|
|
94
|
+
self.ports = ports
|
|
95
|
+
|
|
96
|
+
def managed_names(self, config: dict[str, Any]) -> list[str]:
|
|
97
|
+
names = self.ports.server_names(self.ports.launch_specs(config, []))
|
|
98
|
+
names = [name for name in names if name.lower() not in self.native_router_names]
|
|
99
|
+
owned = self.ports.owned_names()
|
|
100
|
+
if not owned:
|
|
101
|
+
return names
|
|
102
|
+
owned_public = {self.ports.public_name(name) for name in owned}
|
|
103
|
+
kept = [
|
|
104
|
+
name for name in names if self.ports.public_name(name) not in owned_public
|
|
105
|
+
]
|
|
106
|
+
if len(kept) != len(names):
|
|
107
|
+
dropped = sorted(set(names) - set(kept))
|
|
108
|
+
self.ports.log(
|
|
109
|
+
"INFO",
|
|
110
|
+
"router_channel_sse_skipped_proxy_owned "
|
|
111
|
+
f"servers={','.join(dropped) or '-'}",
|
|
112
|
+
)
|
|
113
|
+
return kept
|
|
114
|
+
|
|
115
|
+
def start(self, config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
116
|
+
if not self.ports.delivery_enabled(True, [], config):
|
|
117
|
+
return []
|
|
118
|
+
names = self.managed_names(config)
|
|
119
|
+
if not names:
|
|
120
|
+
self.ports.log(
|
|
121
|
+
"INFO", "router_channel_sse_skipped reason=no_external_channel_specs"
|
|
122
|
+
)
|
|
123
|
+
return []
|
|
124
|
+
source_paths: list[Path] = []
|
|
125
|
+
try:
|
|
126
|
+
self.ports.ensure_probe(config, [])
|
|
127
|
+
source_paths = self.ports.source_paths(
|
|
128
|
+
[f"server:{name}" for name in names]
|
|
129
|
+
)
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
self.ports.log(
|
|
132
|
+
"WARN",
|
|
133
|
+
f"router_channel_probe_cache_failed "
|
|
134
|
+
f"error={type(exc).__name__}: {exc}",
|
|
135
|
+
)
|
|
136
|
+
started = self.ports.auto_start(
|
|
137
|
+
[],
|
|
138
|
+
extra_config_paths=source_paths,
|
|
139
|
+
allowed_server_names=list(names),
|
|
140
|
+
)
|
|
141
|
+
self.ports.log(
|
|
142
|
+
"INFO",
|
|
143
|
+
f"router_channel_sse_started count={len(started)} servers="
|
|
144
|
+
+ (
|
|
145
|
+
",".join(str(item.get("name") or "") for item in started) or "-"
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
return started
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Built-in channel MCP tool catalog and application service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class ChannelMcpToolServices:
|
|
12
|
+
queue_compact: Callable[[str, str], dict[str, Any]]
|
|
13
|
+
append_message: Callable[[dict[str, Any]], dict[str, Any]]
|
|
14
|
+
store_file_path: Callable[[Any, str | None, str | None], dict[str, Any]]
|
|
15
|
+
store_file_upload: Callable[[dict[str, Any]], dict[str, Any]]
|
|
16
|
+
file_message_text: Callable[[str, list[dict[str, Any]]], str]
|
|
17
|
+
handle_llm_options: Callable[[str, str], tuple[list[str], bool]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def channel_mcp_tool_schemas() -> list[dict[str, Any]]:
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
"name": "compact_session",
|
|
24
|
+
"description": (
|
|
25
|
+
"Queue Claude Code's /compact slash command for the active Ciel Runtime-launched session. "
|
|
26
|
+
"Use this when the conversation context is too large and the session should compact itself."
|
|
27
|
+
),
|
|
28
|
+
"inputSchema": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"reason": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Optional short reason shown in Ciel Runtime logs.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "send_message",
|
|
40
|
+
"description": (
|
|
41
|
+
"Send a reply or status message to a Ciel Runtime channel. "
|
|
42
|
+
"Use this to answer messages delivered through the Ciel Runtime channel inbox, "
|
|
43
|
+
"including /ca/web/chat browser sessions."
|
|
44
|
+
),
|
|
45
|
+
"inputSchema": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"channel": {"type": "string", "description": "Destination channel id from the incoming message."},
|
|
49
|
+
"message": {"type": "string", "description": "Message body to send."},
|
|
50
|
+
"recipients": {
|
|
51
|
+
"description": "Recipient id, 'all', or an array of recipients. Use 'web' for /ca/web/chat replies."
|
|
52
|
+
},
|
|
53
|
+
"thread_id": {"type": "string", "description": "Thread/conversation id to continue."},
|
|
54
|
+
"parent_id": {"description": "Optional parent message id."},
|
|
55
|
+
"delivery": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": {"type": "string"},
|
|
58
|
+
"description": "Delivery targets. Use ['web'] for browser-only replies.",
|
|
59
|
+
},
|
|
60
|
+
"kind": {"type": "string", "description": "Optional message kind, for example 'reply' or 'status'."},
|
|
61
|
+
},
|
|
62
|
+
"required": ["channel", "message"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "send_file",
|
|
67
|
+
"description": (
|
|
68
|
+
"Send a file attachment to a Ciel Runtime channel. "
|
|
69
|
+
"Use this to return files to /ca/web/chat browser sessions. "
|
|
70
|
+
"Provide either path for an existing local file, or content with encoding='text' or encoding='base64'."
|
|
71
|
+
),
|
|
72
|
+
"inputSchema": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"channel": {"type": "string", "description": "Destination channel id from the incoming message."},
|
|
76
|
+
"path": {"type": "string", "description": "Optional local file path to attach."},
|
|
77
|
+
"content": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Optional inline file content when path is not used.",
|
|
80
|
+
},
|
|
81
|
+
"encoding": {"type": "string", "description": "Inline content encoding: text or base64."},
|
|
82
|
+
"name": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "Display filename. Defaults to the source path basename or file.txt.",
|
|
85
|
+
},
|
|
86
|
+
"content_type": {"type": "string", "description": "Optional MIME type."},
|
|
87
|
+
"message": {"type": "string", "description": "Optional message body to show with the file link."},
|
|
88
|
+
"recipients": {
|
|
89
|
+
"description": "Recipient id, 'all', or an array of recipients. Use 'web' for /ca/web/chat replies."
|
|
90
|
+
},
|
|
91
|
+
"thread_id": {"type": "string", "description": "Thread/conversation id to continue."},
|
|
92
|
+
"parent_id": {"description": "Optional parent message id."},
|
|
93
|
+
"delivery": {
|
|
94
|
+
"type": "array",
|
|
95
|
+
"items": {"type": "string"},
|
|
96
|
+
"description": "Delivery targets. Use ['web'] for browser-only replies.",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
"required": ["channel"],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "llm_options",
|
|
104
|
+
"description": (
|
|
105
|
+
"Show, apply, or restore ciel-runtime live LLM option presets for the current routed session. "
|
|
106
|
+
"Use action='list' to show keyboard-selectable slash commands, action='apply' with preset, "
|
|
107
|
+
"or action='restore' to return to the captured original options."
|
|
108
|
+
),
|
|
109
|
+
"inputSchema": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"action": {"type": "string", "description": "One of status, list, apply, or restore."},
|
|
113
|
+
"preset": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"description": (
|
|
116
|
+
"Preset id or alias when action is apply, for example balanced, long-context-256k, "
|
|
117
|
+
"long-context-300k, long-context-512k, or million-context-1m."
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def channel_mcp_tool_response(request_id: Any, text: str, is_error: bool = False) -> dict[str, Any]:
|
|
127
|
+
return {
|
|
128
|
+
"jsonrpc": "2.0",
|
|
129
|
+
"id": request_id,
|
|
130
|
+
"result": {"content": [{"type": "text", "text": text}], "isError": bool(is_error)},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def dispatch_channel_mcp_tool(
|
|
135
|
+
request_id: Any,
|
|
136
|
+
params: dict[str, Any],
|
|
137
|
+
services: ChannelMcpToolServices,
|
|
138
|
+
) -> dict[str, Any]:
|
|
139
|
+
name = str(params.get("name") or "")
|
|
140
|
+
args = params.get("arguments") if isinstance(params.get("arguments"), dict) else {}
|
|
141
|
+
if name == "compact_session":
|
|
142
|
+
request = services.queue_compact("ciel-runtime-router-tool", str(args.get("reason") or ""))
|
|
143
|
+
return _json_response(
|
|
144
|
+
request_id,
|
|
145
|
+
{
|
|
146
|
+
"ok": True,
|
|
147
|
+
"queued": True,
|
|
148
|
+
"command": request.get("command"),
|
|
149
|
+
"request_id": request.get("id"),
|
|
150
|
+
"expires_at": request.get("expires_at"),
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
if name == "send_message":
|
|
154
|
+
return _send_message(request_id, args, services)
|
|
155
|
+
if name == "send_file":
|
|
156
|
+
return _send_file(request_id, args, services)
|
|
157
|
+
if name == "llm_options":
|
|
158
|
+
lines, changed = services.handle_llm_options(
|
|
159
|
+
str(args.get("action") or "status"),
|
|
160
|
+
str(args.get("preset") or ""),
|
|
161
|
+
)
|
|
162
|
+
return _json_response(request_id, {"ok": True, "changed": changed, "lines": lines})
|
|
163
|
+
return channel_mcp_tool_response(request_id, f"Unknown ciel-runtime-router tool: {name}", True)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _send_message(
|
|
167
|
+
request_id: Any,
|
|
168
|
+
args: dict[str, Any],
|
|
169
|
+
services: ChannelMcpToolServices,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
channel = str(args.get("channel") or "").strip()
|
|
172
|
+
message = str(args.get("message") or args.get("text") or "").strip()
|
|
173
|
+
if not channel or not message:
|
|
174
|
+
return channel_mcp_tool_response(request_id, "send_message requires channel and message.", True)
|
|
175
|
+
saved = services.append_message(_message_payload(args, channel, message, "reply"))
|
|
176
|
+
return _json_response(request_id, {"ok": True, "message": saved})
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _send_file(
|
|
180
|
+
request_id: Any,
|
|
181
|
+
args: dict[str, Any],
|
|
182
|
+
services: ChannelMcpToolServices,
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
channel = str(args.get("channel") or "").strip()
|
|
185
|
+
if not channel:
|
|
186
|
+
return channel_mcp_tool_response(request_id, "send_file requires channel.", True)
|
|
187
|
+
try:
|
|
188
|
+
upload = _store_file(args, services)
|
|
189
|
+
except (FileNotFoundError, OverflowError, ValueError) as exc:
|
|
190
|
+
return channel_mcp_tool_response(request_id, str(exc), True)
|
|
191
|
+
uploads = [upload]
|
|
192
|
+
message = services.file_message_text(str(args.get("message") or ""), uploads)
|
|
193
|
+
payload = _message_payload(args, channel, message, "file")
|
|
194
|
+
payload["meta"] = {"attachments": uploads, **payload["meta"]}
|
|
195
|
+
saved = services.append_message(payload)
|
|
196
|
+
return _json_response(request_id, {"ok": True, "file": upload, "message": saved})
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _store_file(args: dict[str, Any], services: ChannelMcpToolServices) -> dict[str, Any]:
|
|
200
|
+
if args.get("path"):
|
|
201
|
+
return services.store_file_path(
|
|
202
|
+
args.get("path"),
|
|
203
|
+
str(args.get("name") or "").strip() or None,
|
|
204
|
+
str(args.get("content_type") or args.get("mime_type") or "").strip() or None,
|
|
205
|
+
)
|
|
206
|
+
return services.store_file_upload(
|
|
207
|
+
{
|
|
208
|
+
"name": str(args.get("name") or "file.txt"),
|
|
209
|
+
"encoding": str(args.get("encoding") or "text"),
|
|
210
|
+
"content": args.get("content", ""),
|
|
211
|
+
"content_type": str(args.get("content_type") or args.get("mime_type") or "text/plain"),
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _message_payload(
|
|
217
|
+
args: dict[str, Any],
|
|
218
|
+
channel: str,
|
|
219
|
+
message: str,
|
|
220
|
+
default_kind: str,
|
|
221
|
+
) -> dict[str, Any]:
|
|
222
|
+
meta = args.get("meta") if isinstance(args.get("meta"), dict) else {}
|
|
223
|
+
return {
|
|
224
|
+
"channel": channel,
|
|
225
|
+
"sender_id": args.get("sender_id") or "claude-code",
|
|
226
|
+
"recipients": args.get("recipients", args.get("recipient_id", "web")),
|
|
227
|
+
"thread_id": args.get("thread_id"),
|
|
228
|
+
"parent_id": args.get("parent_id"),
|
|
229
|
+
"kind": args.get("kind") or default_kind,
|
|
230
|
+
"message": message,
|
|
231
|
+
"delivery": args.get("delivery", ["web"]),
|
|
232
|
+
"meta": {"source": "ciel-runtime-router-tool", **meta},
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _json_response(request_id: Any, payload: dict[str, Any]) -> dict[str, Any]:
|
|
237
|
+
return channel_mcp_tool_response(
|
|
238
|
+
request_id,
|
|
239
|
+
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
|
|
240
|
+
)
|