From d986442e8e4bc2d716c9d63159a1cfa7b1e6ed76 Mon Sep 17 00:00:00 2001 From: main Date: Sun, 22 Mar 2026 22:20:17 -0400 Subject: Bootstrap consultative Claude Code MCP --- crates/phone-opus/src/mcp/catalog.rs | 107 +++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 crates/phone-opus/src/mcp/catalog.rs (limited to 'crates/phone-opus/src/mcp/catalog.rs') diff --git a/crates/phone-opus/src/mcp/catalog.rs b/crates/phone-opus/src/mcp/catalog.rs new file mode 100644 index 0000000..a7e7cf6 --- /dev/null +++ b/crates/phone-opus/src/mcp/catalog.rs @@ -0,0 +1,107 @@ +use libmcp::ReplayContract; +use serde_json::{Value, json}; + +use crate::mcp::output::with_common_presentation; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DispatchTarget { + Host, + Worker, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct ToolSpec { + pub(crate) name: &'static str, + pub(crate) description: &'static str, + pub(crate) dispatch: DispatchTarget, + pub(crate) replay: ReplayContract, +} + +impl ToolSpec { + fn annotation_json(self) -> Value { + json!({ + "title": self.name, + "readOnlyHint": true, + "destructiveHint": false, + "phoneOpus": { + "dispatch": match self.dispatch { + DispatchTarget::Host => "host", + DispatchTarget::Worker => "worker", + }, + "replayContract": match self.replay { + ReplayContract::Convergent => "convergent", + ReplayContract::ProbeRequired => "probe_required", + ReplayContract::NeverReplay => "never_replay", + }, + } + }) + } +} + +const TOOL_SPECS: &[ToolSpec] = &[ + ToolSpec { + name: "consult", + description: "Run a blocking consult against the system Claude Code install using a read-only built-in toolset and return the response.", + dispatch: DispatchTarget::Worker, + replay: ReplayContract::NeverReplay, + }, + ToolSpec { + name: "health_snapshot", + description: "Read host lifecycle, worker generation, rollout state, and latest fault. Defaults to render=porcelain; use render=json for structured output.", + dispatch: DispatchTarget::Host, + replay: ReplayContract::Convergent, + }, + ToolSpec { + name: "telemetry_snapshot", + description: "Read aggregate request and recovery telemetry for this session. Defaults to render=porcelain; use render=json for structured output.", + dispatch: DispatchTarget::Host, + replay: ReplayContract::Convergent, + }, +]; + +pub(crate) fn tool_spec(name: &str) -> Option { + TOOL_SPECS.iter().copied().find(|spec| spec.name == name) +} + +pub(crate) fn tool_definitions() -> Vec { + TOOL_SPECS + .iter() + .map(|spec| { + json!({ + "name": spec.name, + "description": spec.description, + "inputSchema": tool_schema(spec.name), + "annotations": spec.annotation_json(), + }) + }) + .collect() +} + +fn tool_schema(name: &str) -> Value { + match name { + "consult" => with_common_presentation(json!({ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Prompt to send to Claude Code." + }, + "cwd": { + "type": "string", + "description": "Optional working directory for the Claude Code session. Relative paths resolve against the MCP host working directory." + }, + "max_turns": { + "type": "integer", + "minimum": 1, + "description": "Optional maximum number of Claude agent turns before stopping." + } + }, + "required": ["prompt"] + })), + "health_snapshot" | "telemetry_snapshot" => with_common_presentation(json!({ + "type": "object", + "properties": {} + })), + _ => Value::Null, + } +} -- cgit v1.2.3