use crate::ai::rpc::{Handled, RpcPeer};
use crate::ai::types::AiError;
use serde_json::{Value, json};
use std::collections::BTreeMap;
use std::time::Duration;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
const PROTOCOL_VERSION: &str = "2025-06-18";
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct McpConfig {
pub command: String,
pub args: Vec<String>,
pub env: BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct McpTool {
pub name: String,
pub description: String,
pub schema: Value,
}
pub struct McpServer {
peer: RpcPeer,
pub tools: Vec<McpTool>,
}
impl McpServer {
pub fn start(config: &McpConfig) -> Result<Self, AiError> {
let peer = RpcPeer::spawn(&config.command, &config.args, &config.env, None, |_| Handled::Unsupported)?;
let init = json!({ "protocolVersion": PROTOCOL_VERSION, "capabilities": {}, "clientInfo": { "name": "aura", "version": "0.1" } });
peer.request("initialize", init, REQUEST_TIMEOUT)?;
peer.notify("notifications/initialized", json!({}))?;
let tools = Self::list_tools(&peer)?;
Ok(Self { peer, tools })
}
fn list_tools(peer: &RpcPeer) -> Result<Vec<McpTool>, AiError> {
let (mut tools, mut cursor) = (Vec::new(), None::<String>);
loop {
let params = cursor.as_ref().map_or_else(|| json!({}), |cursor| json!({ "cursor": cursor }));
let page = peer.request("tools/list", params, REQUEST_TIMEOUT)?;
for tool in page["tools"].as_array().into_iter().flatten() {
let Some(name) = tool["name"].as_str() else { continue };
tools.push(McpTool {
name: name.to_owned(),
description: tool["description"].as_str().unwrap_or_default().to_owned(),
schema: tool.get("inputSchema").cloned().unwrap_or_else(|| json!({ "type": "object" })),
});
}
cursor = page["nextCursor"].as_str().map(str::to_owned);
if cursor.is_none() {
return Ok(tools);
}
}
}
pub fn call_tool(&self, name: &str, arguments: &Value) -> Result<(String, bool), AiError> {
let result =
self.peer.request("tools/call", json!({ "name": name, "arguments": arguments }), REQUEST_TIMEOUT)?;
let text = result["content"]
.as_array()
.into_iter()
.flatten()
.filter_map(|part| part["text"].as_str())
.collect::<Vec<_>>()
.join("\n");
Ok((text, result["isError"].as_bool().unwrap_or(false)))
}
}