Skip to main content

lemma/mcp/
catalog.rs

1use serde::Serialize;
2
3use crate::documentation::{example_by_path, GuideTopic, EVALUATE_GUIDE, EXAMPLE_RESOURCES};
4use crate::mcp::error::ResourceError;
5
6#[derive(Debug, Clone, Serialize)]
7pub struct ToolDefinition {
8    pub name: &'static str,
9    pub description: &'static str,
10    #[serde(rename = "inputSchema")]
11    pub input_schema: serde_json::Value,
12}
13
14#[derive(Debug, Clone, Serialize)]
15pub struct ResourceDefinition {
16    pub uri: String,
17    pub name: String,
18    #[serde(rename = "mimeType")]
19    pub mime_type: &'static str,
20    pub description: String,
21}
22
23pub fn list_tools() -> Vec<ToolDefinition> {
24    vec![
25        ToolDefinition {
26            name: "evaluate",
27            description: "Evaluate rules. Pass `rule` to target one rule; omit for all. For human intake: call guide (default = evaluate guide; not topic full), then list, show once, evaluate. missing_data lines include name, type, and help. Primary loop: after each user turn, bind every field that utterance decides (entailments), re-evaluate; ask at most one open topic-question when something remains. Never ask the user what the policy means. Never dispose interpretation as truth; use “should” when a judgment call cannot be answered. When the rule answers, present details+answer in domain language for user verify (no tooling jargon to the user) before treating as done. No questionnaire dumps. No re-call show between asks. Do not dump every show data field into evaluate. Returns display values, unit maps, reasoning, and missing_data when inputs are still needed.",
28            input_schema: serde_json::json!({
29                "type": "object",
30                "properties": {
31                    "spec": {
32                        "type": "string",
33                        "description": "Spec set id, e.g. pricing"
34                    },
35                    "rule": {
36                        "type": "string",
37                        "description": "Optional: name of a specific rule to evaluate. Omit to evaluate all rules."
38                    },
39                    "data": {
40                        "type": "array",
41                        "items": { "type": "string" },
42                        "description": "Optional data values as 'name=value' (e.g. ['price=100', 'measure=5']). Partial is fine.",
43                        "default": []
44                    },
45                    "effective": {
46                        "type": "string",
47                        "description": "Optional: evaluate at a specific effective datetime (e.g. '2026', '2026-03', '2026-03-04', '2026-03-04T10:30:00Z')"
48                    }
49                },
50                "required": ["spec"]
51            }),
52        },
53        ToolDefinition {
54            name: "list",
55            description: "List loaded specs by repository (name, effective_from, effective_to). Call this first when you do not already know the exact spec name. Do not invent or guess spec names. For human intake call guide (default evaluate guide), then show once and evaluate.",
56            input_schema: serde_json::json!({
57                "type": "object",
58                "properties": {}
59            }),
60        },
61        ToolDefinition {
62            name: "show",
63            description: "Return JSON Show for a spec: data catalog (types, constraints, suggestions, units, help) and rule output types. Call once after list. Static interface — not a required-input list, not a questionnaire, not something to re-call between evaluate/ask turns. Human intake: call guide (default = evaluate guide).",
64            input_schema: serde_json::json!({
65                "type": "object",
66                "properties": {
67                    "spec": {
68                        "type": "string",
69                        "description": "Spec set id, e.g. pricing"
70                    },
71                    "effective": {
72                        "type": "string",
73                        "description": "Optional: show at a specific effective datetime"
74                    }
75                },
76                "required": ["spec"]
77            }),
78        },
79        ToolDefinition {
80            name: "source",
81            description: "Return formatted Lemma source. Pass `repository` (e.g. `lemma` for embedded units stdlib) for the whole repo, or `spec` for a workspace spec. After add_spec / update_spec, call this and paste the result in chat for user verify; do not present the draft you authored.",
82            input_schema: serde_json::json!({
83                "type": "object",
84                "properties": {
85                    "repository": {
86                        "type": "string",
87                        "description": "Repository qualifier (e.g. lemma). When set, returns formatted source for the entire repository."
88                    },
89                    "spec": {
90                        "type": "string",
91                        "description": "Workspace spec set id (when repository is omitted)"
92                    },
93                    "effective": {
94                        "type": "string",
95                        "description": "Optional: get source at a specific effective datetime"
96                    }
97                }
98            }),
99        },
100        ToolDefinition {
101            name: "check",
102            description: "Validate Lemma sources (does not load). On success confirms syntax is valid. Call add_spec to load after check passes. On failure returns structured diagnostics (kind, message, suggestion, source line/column). Sources resolve cross-file `uses` within the batch. A leading `@` label loads as a dependency. Lemma has no `#` or `//` comments; commentary is valid only as a docstring immediately after the `spec` line. Before drafting new specs, call guide with topic full (or method then data). Finish Interrogate first: do not call check or add_spec in the same turn as the first policy questions; wait for answers or an explicit acceptance that the source already states the gaps.",
103            input_schema: serde_json::json!({
104                "type": "object",
105                "properties": {
106                    "sources": {
107                        "type": "array",
108                        "items": {
109                            "type": "array",
110                            "items": { "type": "string" },
111                            "minItems": 2,
112                            "maxItems": 2
113                        },
114                        "description": "Array of [label, code] pairs. Label is the source path (e.g. 'pricing.lemma') or a dependency identifier (e.g. '@org/repo')."
115                    }
116                },
117                "required": ["sources"]
118            }),
119        },
120        ToolDefinition {
121            name: "guide",
122            description: "Return a Lemma guide. Omit topic for the evaluate guide (CS intake with loaded specs; default). That guide forbids writing or redesigning specs. Authoring: pass topic full (complete authoring guide), or method then data. Other authoring sections: syntax, rules, units, veto, composition, anti_patterns; topic evaluate is the same as the default.",
123            input_schema: serde_json::json!({
124                "type": "object",
125                "properties": {
126                    "topic": {
127                        "type": "string",
128                        "enum": ["method", "syntax", "data", "rules", "units", "veto", "composition", "anti_patterns", "evaluate", "full"],
129                        "description": "Optional. Omit for evaluate guide (do not write specs). Use full when authoring; method then data also fine."
130                    }
131                }
132            }),
133        },
134    ]
135}
136
137pub fn list_resources() -> Vec<ResourceDefinition> {
138    let mut resources = vec![ResourceDefinition {
139        uri: "lemma://guide".to_string(),
140        name: "Lemma evaluate guide".to_string(),
141        mime_type: "text/plain",
142        description: "Default evaluate guide for CS intake. Same as guide tool with no topic. Use lemma://guide/full only when authoring.".to_string(),
143    }];
144    for topic in GuideTopic::ALL {
145        resources.push(ResourceDefinition {
146            uri: format!("lemma://guide/{}", topic.as_str()),
147            name: format!("Lemma guide: {}", topic.as_str()),
148            mime_type: "text/plain",
149            description: format!("Guide section '{}'", topic.as_str()),
150        });
151    }
152    for example in EXAMPLE_RESOURCES {
153        resources.push(ResourceDefinition {
154            uri: format!("lemma://examples/{}", example.path),
155            name: example.path.to_string(),
156            mime_type: "text/plain",
157            description: format!("Example Lemma source: {}", example.path),
158        });
159    }
160    resources
161}
162
163pub fn read_resource(uri: &str) -> Result<&'static str, ResourceError> {
164    if uri == "lemma://guide" {
165        return Ok(EVALUATE_GUIDE);
166    }
167    if let Some(topic_name) = uri.strip_prefix("lemma://guide/") {
168        let topic = GuideTopic::parse(topic_name).ok_or_else(|| {
169            ResourceError::UnknownUri(format!(
170                "Unknown guide topic '{topic_name}'. Valid: {}",
171                GuideTopic::VALID_LIST
172            ))
173        })?;
174        return Ok(topic.section_text());
175    }
176    if let Some(path) = uri.strip_prefix("lemma://examples/") {
177        return example_by_path(path)
178            .ok_or_else(|| ResourceError::UnknownUri(format!("Unknown example resource: {uri}")));
179    }
180    Err(ResourceError::UnknownUri(format!(
181        "Unknown resource URI: {uri}. Use resources/list."
182    )))
183}