diff options
Diffstat (limited to 'crates/phone-opus/src/mcp/catalog.rs')
| -rw-r--r-- | crates/phone-opus/src/mcp/catalog.rs | 107 |
1 files changed, 107 insertions, 0 deletions
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<ToolSpec> { + TOOL_SPECS.iter().copied().find(|spec| spec.name == name) +} + +pub(crate) fn tool_definitions() -> Vec<Value> { + 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, + } +} |