use std::path::Path;
use serde_json::{Value, json};
use super::rpc;
use crate::invocation::Capture;
use crate::json::str_of;
const COMMENTARY: &str = "thrall: what follows is each tool's own hints, as the server stated \
them — for the engine-side policy row, and never advertised.\n";
pub fn entries(server: &[String], exe: &Path) -> Capture {
match pinned(server, exe) {
Ok(capture) => capture,
Err(reason) => crate::exec::refused(super::FAILED, &reason),
}
}
fn pinned(server: &[String], exe: &Path) -> Result<Capture, String> {
let mut mcp = rpc::Server::start(server)?;
mcp.initialize()?;
let catalog = mcp.list()?;
let listed = catalog
.get("tools")
.and_then(Value::as_array)
.ok_or("the MCP server's catalog carries no \"tools\" array")?;
let mut document = Vec::new();
let mut said = COMMENTARY.to_owned();
for tool in listed {
let o = tool
.as_object()
.ok_or("the MCP server's catalog holds something that is not a tool")?;
let name = str_of(o, "name").map_err(|e| format!("the MCP server's catalog: {e}"))?;
document.push(entry(&name, o, exe, server));
if let Some(hints) = o.get("annotations") {
said.push_str(&name);
said.push(' ');
said.push_str(&hints.to_string());
said.push('\n');
}
}
Ok(Capture {
stdout: format!(
"{}\n",
serde_json::to_string_pretty(&Value::Array(document)).unwrap_or_default()
),
stderr: said,
exit_code: 0,
})
}
fn said(tool: &serde_json::Map<String, Value>, key: &str) -> String {
match tool.get(key) {
Some(Value::String(s)) => s.clone(),
_ => String::new(),
}
}
fn entry(
name: &str,
tool: &serde_json::Map<String, Value>,
exe: &Path,
server: &[String],
) -> Value {
let mut command = vec![
Value::String(exe.display().to_string()),
Value::String("mcp".to_owned()),
Value::String(name.to_owned()),
Value::String("--".to_owned()),
];
command.extend(server.iter().map(|word| Value::String(word.clone())));
json!({
"name": name,
"description": said(tool, "description"),
"input_schema": tool.get("inputSchema").cloned().unwrap_or(json!({"type": "object"})),
"command": Value::Array(command),
})
}