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 run_input_schema() -> serde_json::Value {
24 serde_json::json!({
25 "type": "object",
26 "properties": {
27 "spec": {
28 "type": "string",
29 "description": "Spec set id, e.g. pricing"
30 },
31 "repository": {
32 "type": "string",
33 "description": "Optional repository qualifier (e.g. lemma, @org/repo). Omit for workspace."
34 },
35 "rules": {
36 "description": "Optional: one rule name (string) or several (string array). Omit for all rules.",
37 "oneOf": [
38 { "type": "string" },
39 { "type": "array", "items": { "type": "string" }, "minItems": 1 }
40 ]
41 },
42 "data": {
43 "type": "object",
44 "description": "Optional input bindings. Integers as numbers; decimals as strings; unit maps as {\"eur\": \"84\"}. Partial is fine.",
45 "additionalProperties": true
46 },
47 "effective": {
48 "type": "string",
49 "description": "Optional: evaluate at a specific effective datetime (e.g. '2026', '2026-03', '2026-03-04', '2026-03-04T10:30:00Z')"
50 }
51 },
52 "required": ["spec"]
53 })
54}
55
56pub fn list_tools() -> Vec<ToolDefinition> {
57 vec![
58 ToolDefinition {
59 name: "run",
60 description: "Evaluate rules (Engine Response JSON, same as SDK run / lemma run --json -x). Always includes explanation trees. Pass `rules` to target one or more rules; omit for all. For human intake: call guide (default = evaluate guide; not topic full), then list, show once, run. missing_data is 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-run; 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 run.",
61 input_schema: run_input_schema(),
62 },
63 ToolDefinition {
64 name: "evaluate",
65 description: "Deprecated alias of `run`. Same arguments and Response JSON. Prefer `run`.",
66 input_schema: run_input_schema(),
67 },
68 ToolDefinition {
69 name: "list",
70 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 run. When the workspace is empty and write mode is enabled, use add_spec to load Lemma source.",
71 input_schema: serde_json::json!({
72 "type": "object",
73 "properties": {}
74 }),
75 },
76 ToolDefinition {
77 name: "show",
78 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 run/ask turns. Human intake: call guide (default = evaluate guide).",
79 input_schema: serde_json::json!({
80 "type": "object",
81 "properties": {
82 "spec": {
83 "type": "string",
84 "description": "Spec set id, e.g. pricing"
85 },
86 "repository": {
87 "type": "string",
88 "description": "Optional repository qualifier (e.g. lemma, @org/repo). Omit for workspace."
89 },
90 "effective": {
91 "type": "string",
92 "description": "Optional: show at a specific effective datetime"
93 }
94 },
95 "required": ["spec"]
96 }),
97 },
98 ToolDefinition {
99 name: "source",
100 description: "Return formatted Lemma source. Pass `repository` (e.g. `lemma` for embedded units stdlib) for the whole repo, or `spec` for a workspace or repository spec. After add_spec / update_spec, call this and paste the result in chat for user verify; do not present the draft you authored.",
101 input_schema: serde_json::json!({
102 "type": "object",
103 "properties": {
104 "repository": {
105 "type": "string",
106 "description": "Repository qualifier (e.g. lemma). Alone: whole repository. With `spec`: that repo's spec."
107 },
108 "spec": {
109 "type": "string",
110 "description": "Spec set id (workspace when repository omitted)"
111 },
112 "effective": {
113 "type": "string",
114 "description": "Optional: get source at a specific effective datetime"
115 }
116 }
117 }),
118 },
119 ToolDefinition {
120 name: "check",
121 description: "Validate Lemma sources (does not load). On success returns JSON quality recommendations array (empty if none). 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.",
122 input_schema: serde_json::json!({
123 "type": "object",
124 "properties": {
125 "sources": {
126 "type": "array",
127 "items": {
128 "type": "array",
129 "items": { "type": "string" },
130 "minItems": 2,
131 "maxItems": 2
132 },
133 "description": "Array of [label, code] pairs. Label is the source path (e.g. 'pricing.lemma') or a dependency identifier (e.g. '@org/repo')."
134 }
135 },
136 "required": ["sources"]
137 }),
138 },
139 ToolDefinition {
140 name: "guide",
141 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.",
142 input_schema: serde_json::json!({
143 "type": "object",
144 "properties": {
145 "topic": {
146 "type": "string",
147 "enum": ["method", "syntax", "data", "rules", "units", "veto", "composition", "anti_patterns", "evaluate", "full"],
148 "description": "Optional. Omit for evaluate guide (do not write specs). Use full when authoring; method then data also fine."
149 }
150 }
151 }),
152 },
153 ]
154}
155
156pub fn list_resources() -> Vec<ResourceDefinition> {
157 let mut resources = vec![ResourceDefinition {
158 uri: "lemma://guide".to_string(),
159 name: "Lemma evaluate guide".to_string(),
160 mime_type: "text/plain",
161 description: "Default evaluate guide for CS intake. Same as guide tool with no topic. Use lemma://guide/full only when authoring.".to_string(),
162 }];
163 for topic in GuideTopic::ALL {
164 resources.push(ResourceDefinition {
165 uri: format!("lemma://guide/{}", topic.as_str()),
166 name: format!("Lemma guide: {}", topic.as_str()),
167 mime_type: "text/plain",
168 description: format!("Guide section '{}'", topic.as_str()),
169 });
170 }
171 for example in EXAMPLE_RESOURCES {
172 resources.push(ResourceDefinition {
173 uri: format!("lemma://examples/{}", example.path),
174 name: example.path.to_string(),
175 mime_type: "text/plain",
176 description: format!("Example Lemma source: {}", example.path),
177 });
178 }
179 resources
180}
181
182pub fn read_resource(uri: &str) -> Result<&'static str, ResourceError> {
183 if uri == "lemma://guide" {
184 return Ok(EVALUATE_GUIDE);
185 }
186 if let Some(topic_name) = uri.strip_prefix("lemma://guide/") {
187 let topic = GuideTopic::parse(topic_name).ok_or_else(|| {
188 ResourceError::UnknownUri(format!(
189 "Unknown guide topic '{topic_name}'. Valid: {}",
190 GuideTopic::VALID_LIST
191 ))
192 })?;
193 return Ok(topic.section_text());
194 }
195 if let Some(path) = uri.strip_prefix("lemma://examples/") {
196 return example_by_path(path)
197 .ok_or_else(|| ResourceError::UnknownUri(format!("Unknown example resource: {uri}")));
198 }
199 Err(ResourceError::UnknownUri(format!(
200 "Unknown resource URI: {uri}. Use resources/list."
201 )))
202}