swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/phone-opus/src/mcp/catalog.rs
blob: a7e7cf6e177883a2fc9723d277917821e76e772f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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,
    }
}