Skip to main content

lilo_rm_core/
tool_contracts.rs

1use std::collections::BTreeMap;
2use std::sync::OnceLock;
3
4use serde::Deserialize;
5use serde_json::{Value, json};
6
7static CONTRACT_REGISTRY: OnceLock<ToolRegistry> = OnceLock::new();
8const TOOLS_TOML: &str = include_str!("../tools.toml");
9
10#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
11pub struct ToolRegistry {
12    pub tools: BTreeMap<String, ToolContract>,
13}
14
15#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
16pub struct ToolContract {
17    pub cli_name: String,
18    pub cli_about: String,
19    pub mcp_description: String,
20    pub args_type: String,
21    pub response_type: String,
22    pub response_description: String,
23    #[serde(default)]
24    pub params: Vec<ToolParam>,
25    #[serde(default)]
26    pub outputs: Vec<ToolOutput>,
27}
28
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
30pub struct ToolParam {
31    pub name: String,
32    pub kind: SchemaKind,
33    pub required: bool,
34    pub mcp_description: String,
35    #[serde(default)]
36    pub format: Option<String>,
37    #[serde(default)]
38    pub items_kind: Option<SchemaKind>,
39    #[serde(default)]
40    pub items_format: Option<String>,
41    #[serde(default)]
42    pub cli_flag: Option<String>,
43    #[serde(default)]
44    pub cli_help: Option<String>,
45}
46
47#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
48pub struct ToolOutput {
49    pub name: String,
50    pub kind: SchemaKind,
51    pub description: String,
52    #[serde(default)]
53    pub items_kind: Option<SchemaKind>,
54}
55
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
57#[serde(rename_all = "snake_case")]
58pub enum SchemaKind {
59    Array,
60    Boolean,
61    Integer,
62    Object,
63    String,
64}
65
66pub fn contract_registry() -> &'static ToolRegistry {
67    CONTRACT_REGISTRY.get_or_init(|| {
68        toml::from_str(TOOLS_TOML).expect("tools.toml must parse as runtime tool contracts")
69    })
70}
71
72impl ToolRegistry {
73    pub fn tool_list_value(&self) -> Value {
74        json!({
75            "tools": self
76                .tools
77                .iter()
78                .map(|(name, contract)| contract.tool_entry_value(name))
79                .collect::<Vec<_>>()
80        })
81    }
82
83    pub fn admin_tools_markdown(&self) -> String {
84        let mut lines = vec![
85            "## Admin MCP Tools".to_owned(),
86            String::new(),
87            "| Tool | Purpose |".to_owned(),
88            "| --- | --- |".to_owned(),
89        ];
90        for (name, contract) in &self.tools {
91            lines.push(format!("| `{name}` | {} |", contract.mcp_description));
92        }
93        lines.push(String::new());
94        lines.join("\n")
95    }
96}
97
98impl ToolContract {
99    pub fn tool_entry_value(&self, name: &str) -> Value {
100        let mut entry = json!({
101            "name": name,
102            "description": self.mcp_description,
103            "inputSchema": self.input_schema_value()
104        });
105        if !self.outputs.is_empty() {
106            entry["outputSchema"] = self.output_schema_value();
107        }
108        entry
109    }
110
111    pub fn input_schema_value(&self) -> Value {
112        let mut properties = serde_json::Map::new();
113        let mut required = Vec::new();
114        for param in &self.params {
115            properties.insert(param.name.clone(), param.schema_value());
116            if param.required {
117                required.push(Value::String(param.name.clone()));
118            }
119        }
120        json!({
121            "type": "object",
122            "properties": properties,
123            "required": required,
124            "additionalProperties": false
125        })
126    }
127
128    pub fn output_schema_value(&self) -> Value {
129        let mut properties = serde_json::Map::new();
130        for output in &self.outputs {
131            properties.insert(output.name.clone(), output.schema_value());
132        }
133        json!({
134            "type": "object",
135            "description": self.response_description,
136            "properties": properties,
137            "additionalProperties": false
138        })
139    }
140}
141
142impl ToolParam {
143    fn schema_value(&self) -> Value {
144        let mut schema = kind_schema(
145            &self.kind,
146            self.format.as_deref(),
147            self.items_kind.as_ref(),
148            self.items_format.as_deref(),
149        );
150        schema["description"] = Value::String(self.mcp_description.clone());
151        schema
152    }
153}
154
155impl ToolOutput {
156    fn schema_value(&self) -> Value {
157        let mut schema = kind_schema(&self.kind, None, self.items_kind.as_ref(), None);
158        schema["description"] = Value::String(self.description.clone());
159        schema
160    }
161}
162
163fn kind_schema(
164    kind: &SchemaKind,
165    format: Option<&str>,
166    items_kind: Option<&SchemaKind>,
167    items_format: Option<&str>,
168) -> Value {
169    let mut schema = json!({ "type": kind.as_json_type() });
170    if let Some(format) = format {
171        schema["format"] = Value::String(format.to_owned());
172    }
173    if let (SchemaKind::Array, Some(items_kind)) = (kind, items_kind) {
174        let mut items = json!({ "type": items_kind.as_json_type() });
175        if let Some(items_format) = items_format {
176            items["format"] = Value::String(items_format.to_owned());
177        }
178        schema["items"] = items;
179    }
180    schema
181}
182
183impl SchemaKind {
184    fn as_json_type(&self) -> &'static str {
185        match self {
186            Self::Array => "array",
187            Self::Boolean => "boolean",
188            Self::Integer => "integer",
189            Self::Object => "object",
190            Self::String => "string",
191        }
192    }
193}