use serde_json::{json, Value};
use std::io::{BufRead, BufReader, Write};
use std::process::{ChildStdin, ChildStdout, Command, Stdio};
use std::time::Duration;
pub struct McpClient {
child: std::process::Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
next_id: i64,
}
impl McpClient {
pub fn spawn(command_line: &str) -> Result<Self, String> {
let parts: Vec<&str> = command_line.split_whitespace().collect();
let cmd = parts.first()
.ok_or_else(|| "mcp.spawn: empty command".to_string())?;
let mut child = Command::new(cmd)
.args(&parts[1..])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("mcp.spawn `{cmd}`: {e}"))?;
let stdin = child.stdin.take()
.ok_or_else(|| "mcp.spawn: no stdin".to_string())?;
let stdout = BufReader::new(child.stdout.take()
.ok_or_else(|| "mcp.spawn: no stdout".to_string())?);
let mut client = Self { child, stdin, stdout, next_id: 0 };
client.initialize()?;
Ok(client)
}
fn next_id(&mut self) -> i64 {
self.next_id += 1;
self.next_id
}
fn rpc(&mut self, method: &str, params: Value) -> Result<Value, String> {
let id = self.next_id();
let req = json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
});
let line = serde_json::to_string(&req)
.map_err(|e| format!("mcp.rpc: serialize: {e}"))?;
self.stdin.write_all(line.as_bytes())
.map_err(|e| format!("mcp.rpc: write: {e}"))?;
self.stdin.write_all(b"\n")
.map_err(|e| format!("mcp.rpc: write: {e}"))?;
self.stdin.flush()
.map_err(|e| format!("mcp.rpc: flush: {e}"))?;
loop {
let mut buf = String::new();
let n = self.stdout.read_line(&mut buf)
.map_err(|e| format!("mcp.rpc: read: {e}"))?;
if n == 0 {
return Err("mcp.rpc: server closed stdout".into());
}
let resp: Value = serde_json::from_str(buf.trim())
.map_err(|e| format!("mcp.rpc: parse `{}`: {e}",
buf.trim().chars().take(120).collect::<String>()))?;
if resp.get("id").is_none() { continue; }
if resp["id"] != json!(id) { continue; }
if let Some(err) = resp.get("error") {
return Err(format!("mcp.rpc {method}: {err}"));
}
return Ok(resp.get("result").cloned().unwrap_or(Value::Null));
}
}
fn initialize(&mut self) -> Result<(), String> {
self.rpc("initialize", json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "lex-runtime", "version": env!("CARGO_PKG_VERSION") },
}))?;
Ok(())
}
pub fn call_tool(&mut self, name: &str, args: Value) -> Result<Value, String> {
self.rpc("tools/call", json!({
"name": name,
"arguments": args,
}))
}
}
impl Drop for McpClient {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let _ = Duration::from_millis(0);
}
}