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
23fn text_property() -> serde_json::Value {
24 serde_json::json!({
25 "type": "boolean",
26 "description": "Human-readable output, as the CLI prints it (default: JSON)"
27 })
28}
29
30fn evaluate_input_schema() -> serde_json::Value {
31 serde_json::json!({
32 "type": "object",
33 "properties": {
34 "spec": {
35 "type": "string",
36 "description": "Spec set id, e.g. pricing"
37 },
38 "repository": {
39 "type": "string",
40 "description": "Optional repository qualifier (e.g. lemma, @org/repo). Omit for the default repository."
41 },
42 "rules": {
43 "description": "Optional: one rule name (string) or several (string array). Omit for all rules.",
44 "oneOf": [
45 { "type": "string" },
46 { "type": "array", "items": { "type": "string" }, "minItems": 1 }
47 ]
48 },
49 "data": {
50 "type": "object",
51 "description": "Optional input bindings. Strings, booleans, single-unit maps ({\"eur\": \"84\"}), arrays. A list (Show type kind `list`) is one array that replaces all its rows: items of a value list are literals ([\"89 eur\", \"120 eur\"]), rows of an instance list are objects keyed by the row spec's data names ([{\"price\": \"2\", \"qty\": \"3\"}]), a nested list inside a row is again an array. A missing row field such as `lines.2.qty` is bound by sending the whole `lines` array again, never as a dotted key. JSON numbers are rejected; pass decimals as strings. Partial is fine.",
52 "additionalProperties": true
53 },
54 "effective": {
55 "type": "string",
56 "description": "Optional: evaluate at a specific effective datetime (e.g. '2026', '2026-03', '2026-03-04', '2026-03-04T10:30:00Z')"
57 }
58 },
59 "required": ["spec"]
60 })
61}
62
63pub fn list_tools() -> Vec<ToolDefinition> {
64 vec![
65 ToolDefinition {
66 name: "evaluate",
67 description: "Evaluate rules; returns ASCII explanation trees (always on) plus a Missing data block when inputs are unbound. Pass `rules` to target one or more rules; omit for all. User phrasing “ask Lemma” means this tool. For human intake: call guide (default = evaluate guide; not topic full), then specs, show once, evaluate. Missing data lists unbound input keys only — look up type/help/suggest on the Show already fetched. 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.",
68 input_schema: evaluate_input_schema(),
69 },
70 ToolDefinition {
71 name: "specs",
72 description: "Local specs with their uses targets (JSON: name, effective_from, effective_to, dependencies; `text: true`: as `lemma specs` prints them). Pass `all: true` for every loaded repository including lemma and lemma_deps (`lemma specs --all`). 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. When the workspace is empty and write mode is enabled, use add_spec to load Lemma source.",
73 input_schema: serde_json::json!({
74 "type": "object",
75 "properties": {
76 "text": text_property(),
77 "all": {
78 "type": "boolean",
79 "description": "Every loaded repository (lemma stdlib, lemma_deps), without nested uses in the text view (default: local specs and their uses)"
80 }
81 }
82 }),
83 },
84 ToolDefinition {
85 name: "show",
86 description: "Return a spec's interface. Default: a JSON Schema (Draft 2020-12) for the spec's evaluation data: `properties` per input key (type, pattern, enum, description = help, default = spec value, examples = suggestion; x-lemma-type with units, x-lemma-path, x-lemma-needed-by-rules empty = reuse-only), `required`, `$defs` row schemas of instance lists, and this spec's rule graph under `x-lemma-rules` (local plus reachable imports as ShowRule: type, path, branches, depends_on_rules). `text: true` returns the `lemma show` view instead. Call once after specs. Static catalog — not a required-input list, not a questionnaire, not something to re-call between evaluate/ask turns. Human intake: call guide (default = evaluate guide); bind only missing_data keys, not reuse-only slots.",
87 input_schema: serde_json::json!({
88 "type": "object",
89 "properties": {
90 "spec": {
91 "type": "string",
92 "description": "Spec set id, e.g. pricing"
93 },
94 "repository": {
95 "type": "string",
96 "description": "Optional repository qualifier (e.g. lemma, @org/repo). Omit for the default repository."
97 },
98 "effective": {
99 "type": "string",
100 "description": "Optional: show at a specific effective datetime"
101 },
102 "text": text_property()
103 },
104 "required": ["spec"]
105 }),
106 },
107 ToolDefinition {
108 name: "source",
109 description: "Return formatted Lemma source. Pass `repository` (e.g. `lemma` for embedded units stdlib) for the whole repo, or `spec` for a spec. After add_spec / update_spec, call this and paste the result in chat for user verify; do not present the draft you authored.",
110 input_schema: serde_json::json!({
111 "type": "object",
112 "properties": {
113 "repository": {
114 "type": "string",
115 "description": "Repository qualifier (e.g. lemma). Alone: whole repository. With `spec`: that repo's spec."
116 },
117 "spec": {
118 "type": "string",
119 "description": "Spec set id (default repository when repository omitted)"
120 },
121 "effective": {
122 "type": "string",
123 "description": "Optional: get source at a specific effective datetime"
124 }
125 }
126 }),
127 },
128 ToolDefinition {
129 name: "check",
130 description: "Validate Lemma sources (does not load). On success returns JSON quality recommendations array (empty if none); `text: true` returns one recommendation per line, or `No recommendations.`. 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.",
131 input_schema: serde_json::json!({
132 "type": "object",
133 "properties": {
134 "sources": {
135 "type": "array",
136 "items": {
137 "type": "array",
138 "items": { "type": "string" },
139 "minItems": 2,
140 "maxItems": 2
141 },
142 "description": "Array of [label, code] pairs. Label is the source path (e.g. 'pricing.lemma') or a dependency identifier (e.g. '@org/repo')."
143 },
144 "text": text_property()
145 },
146 "required": ["sources"]
147 }),
148 },
149 ToolDefinition {
150 name: "guide",
151 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.",
152 input_schema: serde_json::json!({
153 "type": "object",
154 "properties": {
155 "topic": {
156 "type": "string",
157 "enum": ["method", "syntax", "data", "rules", "units", "veto", "composition", "anti_patterns", "evaluate", "full"],
158 "description": "Optional. Omit for evaluate guide (do not write specs). Use full when authoring; method then data also fine."
159 }
160 }
161 }),
162 },
163 ]
164}
165
166pub fn list_resources() -> Vec<ResourceDefinition> {
167 let mut resources = vec![ResourceDefinition {
168 uri: "lemma://guide".to_string(),
169 name: "Lemma evaluate guide".to_string(),
170 mime_type: "text/plain",
171 description: "Default evaluate guide for CS intake. Same as guide tool with no topic. Use lemma://guide/full only when authoring.".to_string(),
172 }];
173 for topic in GuideTopic::ALL {
174 resources.push(ResourceDefinition {
175 uri: format!("lemma://guide/{}", topic.as_str()),
176 name: format!("Lemma guide: {}", topic.as_str()),
177 mime_type: "text/plain",
178 description: format!("Guide section '{}'", topic.as_str()),
179 });
180 }
181 for example in EXAMPLE_RESOURCES {
182 resources.push(ResourceDefinition {
183 uri: format!("lemma://examples/{}", example.path),
184 name: example.path.to_string(),
185 mime_type: "text/plain",
186 description: format!("Example Lemma source: {}", example.path),
187 });
188 }
189 resources
190}
191
192pub fn read_resource(uri: &str) -> Result<&'static str, ResourceError> {
193 if uri == "lemma://guide" {
194 return Ok(EVALUATE_GUIDE);
195 }
196 if let Some(topic_name) = uri.strip_prefix("lemma://guide/") {
197 let topic = GuideTopic::parse(topic_name).ok_or_else(|| {
198 ResourceError::UnknownUri(format!(
199 "Unknown guide topic '{topic_name}'. Valid: {}",
200 GuideTopic::VALID_LIST
201 ))
202 })?;
203 return Ok(topic.section_text());
204 }
205 if let Some(path) = uri.strip_prefix("lemma://examples/") {
206 return example_by_path(path)
207 .ok_or_else(|| ResourceError::UnknownUri(format!("Unknown example resource: {uri}")));
208 }
209 Err(ResourceError::UnknownUri(format!(
210 "Unknown resource URI: {uri}. Use resources/list."
211 )))
212}