use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, ChildStdout, Stdio};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
const PROTOCOL: &str = "2025-06-18";
const GRACE: Duration = Duration::from_millis(500);
const POLL: Duration = Duration::from_millis(20);
pub(super) struct Server {
child: Child,
input: Option<ChildStdin>,
output: BufReader<ChildStdout>,
head: String,
spent: u64,
}
impl Server {
pub(super) fn start(argv: &[String]) -> Result<Self, String> {
let (head, rest) = argv.split_first().ok_or("the MCP server argv is empty")?;
let mut cmd = crate::spawn::command(head);
cmd.args(rest)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = crate::spawn::spawn(&mut cmd)
.map_err(|e| format!("the MCP server {head:?} would not start ({e})"))?;
let input = child
.stdin
.take()
.ok_or("the MCP server was given no stdin")?;
let output = child
.stdout
.take()
.ok_or("the MCP server was given no stdout")?;
Ok(Self {
child,
input: Some(input),
output: BufReader::new(output),
head: head.clone(),
spent: 0,
})
}
pub(super) fn initialize(&mut self) -> Result<(), String> {
let hello = json!({"protocolVersion": PROTOCOL, "capabilities": {},
"clientInfo": {"name": "thrall", "version": env!("CARGO_PKG_VERSION")}});
self.request("initialize", &hello)?;
self.send(&json!({"jsonrpc": "2.0", "method": "notifications/initialized"}));
Ok(())
}
pub(super) fn list(&mut self) -> Result<Value, String> {
self.request("tools/list", &json!({}))
}
pub(super) fn call(&mut self, tool: &str, arguments: &Value) -> Result<Value, String> {
self.request("tools/call", &json!({"name": tool, "arguments": arguments}))
}
fn request(&mut self, method: &str, params: &Value) -> Result<Value, String> {
self.spent += 1;
let id = self.spent;
self.send(&json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}));
self.reply(id, method)
}
fn send(&mut self, message: &Value) {
if let Some(pipe) = self.input.as_mut() {
let _ = writeln!(pipe, "{message}");
let _ = pipe.flush();
}
}
fn reply(&mut self, id: u64, stage: &str) -> Result<Value, String> {
loop {
let mut line = String::new();
if self.output.read_line(&mut line).unwrap_or(0) == 0 {
return Err(self.said(stage, "ended before answering"));
}
let Ok(message) = serde_json::from_str::<Value>(&line) else {
continue;
};
if message.get("id").and_then(Value::as_u64) != Some(id) {
continue;
}
return match message.get("result") {
Some(result) => Ok(result.clone()),
None => Err(self.refusal(stage, &message)),
};
}
}
fn said(&self, stage: &str, what: &str) -> String {
format!("the MCP server {:?} {what} {stage}", self.head)
}
fn refusal(&self, stage: &str, message: &Value) -> String {
let said = message
.pointer("/error/message")
.and_then(Value::as_str)
.unwrap_or("it sent neither a result nor an error");
format!("{}: {said}", self.said(stage, "refused"))
}
}
impl Drop for Server {
fn drop(&mut self) {
self.input = None;
let group = self.child.id();
let until = Instant::now() + GRACE;
while Instant::now() < until && !matches!(self.child.try_wait(), Ok(Some(_))) {
std::thread::sleep(POLL);
}
crate::sys::kill_group(group);
let _ = self.child.wait();
}
}