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);
}
}
pub struct McpClientCache {
entries: Vec<(String, McpClient)>,
cap: usize,
}
impl McpClientCache {
pub fn with_capacity(cap: usize) -> Self {
Self { entries: Vec::with_capacity(cap), cap }
}
pub fn call(
&mut self,
server: &str,
tool: &str,
args: serde_json::Value,
) -> Result<serde_json::Value, String> {
if let Some(idx) = self.entries.iter().position(|(k, _)| k == server) {
let (key, mut client) = self.entries.remove(idx);
match client.call_tool(tool, args) {
Ok(v) => {
self.entries.push((key, client));
Ok(v)
}
Err(e) => {
Err(e)
}
}
} else {
let mut client = McpClient::spawn(server)?;
let result = client.call_tool(tool, args);
if result.is_ok() {
if self.entries.len() >= self.cap && !self.entries.is_empty() {
self.entries.remove(0);
}
self.entries.push((server.to_string(), client));
}
result
}
}
pub fn len(&self) -> usize { self.entries.len() }
pub fn is_empty(&self) -> bool { self.entries.is_empty() }
}
impl Default for McpClientCache {
fn default() -> Self { Self::with_capacity(16) }
}
#[cfg(test)]
mod cache_tests {
use super::*;
#[test]
fn empty_cache_starts_at_zero_with_configured_cap() {
let c = McpClientCache::with_capacity(4);
assert_eq!(c.len(), 0);
assert!(c.is_empty());
assert_eq!(c.cap, 4);
}
}