use elenchus_solver::{
CompileError, FileResolver, MemoryResolver, PortBinding, read_data_bindings,
verify_source_with, verify_with,
};
use serde_json::{Value, json};
use crate::{messages, rpc};
const CHECK: &str = "elenchus_check";
const VERSION: &str = "elenchus_version";
const ABOUT: &str = "elenchus_about";
pub fn definitions() -> Vec<Value> {
vec![check_def(), version_def(), about_def()]
}
fn simple_def(name: &str, description: &str) -> Value {
json!({
"name": name,
"description": description,
"inputSchema": { "type": "object", "properties": {} }
})
}
fn check_def() -> Value {
json!({
"name": CHECK,
"description": messages::CHECK_TOOL,
"inputSchema": {
"type": "object",
"properties": {
"program": { "type": "string", "description": messages::CHECK_ARG_PROGRAM },
"path": { "type": "string", "description": messages::CHECK_ARG_PATH },
"format": {
"type": "string",
"enum": ["human", "json"],
"description": messages::CHECK_ARG_FORMAT
},
"max_classes": {
"type": "integer",
"minimum": 0,
"description": messages::CHECK_ARG_MAX_CLASSES
},
"max_per_class": {
"type": "integer",
"minimum": 0,
"description": messages::CHECK_ARG_MAX_PER_CLASS
},
"values": {
"type": "object",
"additionalProperties": { "type": "boolean" },
"description": messages::CHECK_ARG_VALUES
},
"files": {
"type": "object",
"additionalProperties": { "type": "string" },
"description": messages::CHECK_ARG_FILES
},
"data": {
"type": "object",
"additionalProperties": { "type": "string" },
"description": messages::CHECK_ARG_DATA
}
},
"oneOf": [{ "required": ["program"] }, { "required": ["path"] }]
}
})
}
fn version_def() -> Value {
simple_def(VERSION, messages::VERSION_TOOL)
}
fn about_def() -> Value {
simple_def(ABOUT, messages::ABOUT_TOOL)
}
pub fn call(id: Value, params: Option<&Value>) -> Value {
let Some(params) = params else {
return rpc::error(id, -32602, "missing params");
};
let name = params.get("name").and_then(Value::as_str).unwrap_or("");
match name {
VERSION => rpc::tool_result(id, format!("elenchus {}", env!("CARGO_PKG_VERSION")), false),
ABOUT => rpc::tool_result(id, messages::ABOUT_TOOL.to_string(), false),
CHECK => check(id, params.get("arguments")),
other => rpc::tool_result(id, format!("unknown tool: {other}"), true),
}
}
fn check(id: Value, args: Option<&Value>) -> Value {
let format = args
.and_then(|a| a.get("format"))
.and_then(Value::as_str)
.unwrap_or("json");
let arg_limit = |name: &str| {
let n = args
.and_then(|a| a.get(name))
.and_then(Value::as_u64)
.unwrap_or(0) as usize;
(n > 0).then_some(n)
};
let inputs = match collect_inputs(args) {
Ok(inputs) => inputs,
Err(e) => return rpc::tool_result(id, e.to_string(), true),
};
let program = args.and_then(|a| a.get("program")).and_then(Value::as_str);
let path = args.and_then(|a| a.get("path")).and_then(Value::as_str);
let result = match (program, path) {
(Some(_), Some(_)) => {
return rpc::tool_result(
id,
"give either `program` (inline) or `path` (a .vrf file), not both".into(),
true,
);
}
(None, None) => {
return rpc::tool_result(
id,
"missing entry: pass `program` (inline .vrf text) or `path` (a .vrf file)".into(),
true,
);
}
(None, Some(path)) => verify_with(path, &FileResolver, &inputs),
(Some(program), None) => {
match args.and_then(|a| a.get("files")).and_then(Value::as_object) {
Some(files) if !files.is_empty() => {
let mut resolver = MemoryResolver::new();
for (key, content) in files {
if let Some(text) = content.as_str() {
resolver.add(key, text);
}
}
resolver.add("<mcp>", program);
verify_with("<mcp>", &resolver, &inputs)
}
_ => verify_source_with("<mcp>", program, &inputs),
}
}
};
match result {
Ok(report) => {
let text = if format == "human" {
format!("{report}")
} else {
report.to_json()
};
rpc::tool_result(id, text, false)
}
Err(CompileError::Parse(diag)) => {
let text = diag.render(arg_limit("max_classes"), arg_limit("max_per_class"));
rpc::tool_result(id, text, true)
}
Err(other) => rpc::tool_result(id, other.to_string(), true),
}
}
fn collect_inputs(args: Option<&Value>) -> Result<Vec<(String, PortBinding)>, CompileError> {
let mut inputs: Vec<(String, PortBinding)> = Vec::new();
if let Some(m) = args
.and_then(|a| a.get("values"))
.and_then(Value::as_object)
{
for (k, v) in m {
if let Some(value) = v.as_bool() {
inputs.push((
k.clone(),
PortBinding {
value,
origin: "api".to_string(),
},
));
}
}
}
if let Some(m) = args.and_then(|a| a.get("data")).and_then(Value::as_object) {
for (name, content) in m {
if let Some(src) = content.as_str() {
inputs.extend(read_data_bindings(name, src)?);
}
}
}
Ok(inputs)
}