use std::collections::{BTreeMap, HashMap};
use std::process::Stdio;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use serde::Deserialize;
use serde_json::{Value, json};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, ChildStdin, Command};
use tokio::sync::{Mutex, broadcast, oneshot};
use crate::config::{McpConfig, ServerSpec};
pub const PROTOCOL_VERSION: &str = "2025-06-18";
#[derive(Debug, Clone, Deserialize)]
pub struct Tool {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
#[serde(rename = "outputSchema", default)]
pub output_schema: Option<Value>,
}
#[derive(Debug, Clone, Deserialize, thiserror::Error)]
#[error("{message} (code {code})")]
pub struct RpcError {
pub code: i64,
pub message: String,
#[serde(default)]
pub data: Option<Value>,
}
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("{server}: {error}")]
Rpc {
server: String,
#[source]
error: Box<RpcError>,
},
#[error("{server}: the server exited before answering")]
ServerGone {
server: String,
},
#[error("{server}: {context}: {source}")]
Io {
server: String,
context: &'static str,
#[source]
source: std::io::Error,
},
#[error("{server}: {method} answered an unexpected shape: {detail}")]
Shape {
server: String,
method: &'static str,
detail: String,
},
#[error("{server}: call cancelled by the engine")]
Cancelled {
server: String,
},
}
type Pending = HashMap<u64, oneshot::Sender<Result<Value, RpcError>>>;
pub struct McpClient {
name: String,
next_id: AtomicU64,
pending: Arc<Mutex<Pending>>,
stdin: Mutex<ChildStdin>,
child: Mutex<Child>,
progress: broadcast::Sender<Value>,
pub server_info: Value,
}
impl McpClient {
pub fn name(&self) -> &str {
&self.name
}
pub async fn spawn(name: &str, spec: &ServerSpec) -> Result<Arc<Self>, ClientError> {
let io = |context: &'static str| {
let server = name.to_owned();
move |source| ClientError::Io {
server,
context,
source,
}
};
let command = spec.command.as_deref().ok_or_else(|| ClientError::Shape {
server: name.to_owned(),
method: "spawn",
detail: "no command (the document check admits stdio servers only)".to_owned(),
})?;
let mut cmd = Command::new(command);
cmd.args(&spec.args)
.envs(&spec.env)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(cwd) = &spec.cwd {
cmd.current_dir(cwd);
}
cmd.kill_on_drop(true);
let mut child = cmd.spawn().map_err(io("spawning"))?;
let stdin = child.stdin.take().ok_or_else(|| ClientError::Shape {
server: name.to_owned(),
method: "spawn",
detail: "no stdin pipe".to_owned(),
})?;
let stdout = child.stdout.take().ok_or_else(|| ClientError::Shape {
server: name.to_owned(),
method: "spawn",
detail: "no stdout pipe".to_owned(),
})?;
let stderr = child.stderr.take();
let pending: Arc<Mutex<Pending>> = Arc::new(Mutex::new(HashMap::new()));
let (progress, _) = broadcast::channel(64);
let client = Arc::new(Self {
name: name.to_owned(),
next_id: AtomicU64::new(1),
pending: Arc::clone(&pending),
stdin: Mutex::new(stdin),
child: Mutex::new(child),
progress,
server_info: Value::Null,
});
let reader_client = Arc::clone(&client);
tokio::spawn(async move {
let mut lines = BufReader::new(stdout).lines();
loop {
match lines.next_line().await {
Ok(Some(line)) => reader_client.on_line(&line).await,
Ok(None) => break,
Err(error) => {
tracing::warn!(server = %reader_client.name, %error, "stdout read failed");
break;
}
}
}
let mut pending = reader_client.pending.lock().await;
for (_, sender) in pending.drain() {
let _ = sender.send(Err(RpcError {
code: -32000,
message: "server exited".to_owned(),
data: None,
}));
}
tracing::warn!(server = %reader_client.name, "server stdout closed");
});
if let Some(stderr) = stderr {
let server = name.to_owned();
tokio::spawn(async move {
let mut lines = BufReader::new(stderr).lines();
while let Ok(Some(line)) = lines.next_line().await {
tracing::debug!(server = %server, "{line}");
}
});
}
let init = client
.request(
"initialize",
json!({
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "aion-mcp-worker", "version": env!("CARGO_PKG_VERSION")},
}),
)
.await?;
client
.notify("notifications/initialized", json!({}))
.await?;
let info = init.get("serverInfo").cloned().unwrap_or(Value::Null);
tracing::info!(server = %name, %info, "initialized");
let mut client = client;
if let Some(inner) = Arc::get_mut(&mut client) {
inner.server_info = init.get("serverInfo").cloned().unwrap_or(Value::Null);
}
Ok(client)
}
async fn on_line(&self, line: &str) {
let message: Value = match serde_json::from_str(line) {
Ok(value) => value,
Err(error) => {
tracing::warn!(server = %self.name, %error, line, "non-JSON line from server");
return;
}
};
let id = message.get("id").and_then(Value::as_u64);
let method = message.get("method").and_then(Value::as_str);
match (id, method) {
(Some(id), None) => {
let outcome = if let Some(error) = message.get("error") {
Err(
serde_json::from_value::<RpcError>(error.clone()).unwrap_or(RpcError {
code: -32603,
message: error.to_string(),
data: None,
}),
)
} else {
Ok(message.get("result").cloned().unwrap_or(Value::Null))
};
let sender = self.pending.lock().await.remove(&id);
if let Some(sender) = sender {
let _ = sender.send(outcome);
} else {
tracing::warn!(server = %self.name, id, "response to no open request");
}
}
(Some(_), Some(method)) => {
let reply = json!({"jsonrpc": "2.0", "id": message.get("id"), "error": {"code": -32601, "message": format!("aion-mcp-worker does not serve {method}")}});
if let Err(error) = self.write(&reply).await {
tracing::warn!(server = %self.name, %error, "could not refuse server request");
}
}
(None, Some("notifications/progress")) => {
let _ = self
.progress
.send(message.get("params").cloned().unwrap_or(Value::Null));
}
(None, Some(method)) => {
let params = message.get("params").cloned().unwrap_or(Value::Null);
tracing::debug!(server = %self.name, method, %params, "notification");
}
(None, None) => {
tracing::warn!(server = %self.name, line, "message with neither id nor method");
}
}
}
async fn write(&self, message: &Value) -> Result<(), ClientError> {
let mut text = serde_json::to_string(message).map_err(|source| ClientError::Shape {
server: self.name.clone(),
method: "write",
detail: source.to_string(),
})?;
text.push('\n');
let mut stdin = self.stdin.lock().await;
stdin
.write_all(text.as_bytes())
.await
.map_err(|source| ClientError::Io {
server: self.name.clone(),
context: "writing to the server",
source,
})?;
stdin.flush().await.map_err(|source| ClientError::Io {
server: self.name.clone(),
context: "flushing to the server",
source,
})
}
async fn notify(&self, method: &str, params: Value) -> Result<(), ClientError> {
self.write(&json!({"jsonrpc": "2.0", "method": method, "params": params}))
.await
}
async fn request(&self, method: &'static str, params: Value) -> Result<Value, ClientError> {
let (id, receiver) = self.open(method, params).await?;
self.await_reply(id, receiver).await
}
async fn open(
&self,
method: &'static str,
params: Value,
) -> Result<(u64, oneshot::Receiver<Result<Value, RpcError>>), ClientError> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let (sender, receiver) = oneshot::channel();
self.pending.lock().await.insert(id, sender);
if let Err(error) = self
.write(&json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}))
.await
{
self.pending.lock().await.remove(&id);
return Err(error);
}
Ok((id, receiver))
}
async fn await_reply(
&self,
id: u64,
receiver: oneshot::Receiver<Result<Value, RpcError>>,
) -> Result<Value, ClientError> {
match receiver.await {
Ok(Ok(value)) => Ok(value),
Ok(Err(error)) if error.code == -32000 && error.message == "server exited" => {
Err(ClientError::ServerGone {
server: self.name.clone(),
})
}
Ok(Err(error)) => Err(ClientError::Rpc {
server: self.name.clone(),
error: Box::new(error),
}),
Err(_) => {
self.pending.lock().await.remove(&id);
Err(ClientError::ServerGone {
server: self.name.clone(),
})
}
}
}
pub async fn list_tools(&self) -> Result<Vec<Tool>, ClientError> {
let mut tools = Vec::new();
let mut cursor: Option<String> = None;
loop {
let params = cursor
.as_ref()
.map_or_else(|| json!({}), |c| json!({"cursor": c}));
let page = self.request("tools/list", params).await?;
let listed = page.get("tools").cloned().unwrap_or(Value::Null);
let parsed: Vec<Tool> =
serde_json::from_value(listed).map_err(|error| ClientError::Shape {
server: self.name.clone(),
method: "tools/list",
detail: error.to_string(),
})?;
tools.extend(parsed);
match page.get("nextCursor").and_then(Value::as_str) {
Some(next) if !next.is_empty() => cursor = Some(next.to_owned()),
_ => return Ok(tools),
}
}
}
pub async fn call_tool(
&self,
tool: &str,
arguments: Value,
cancelled: impl std::future::Future<Output = ()>,
mut on_progress: impl FnMut(&Value),
) -> Result<Value, ClientError> {
let token = format!("aion-{}", self.next_id.load(Ordering::Relaxed));
let params =
json!({"name": tool, "arguments": arguments, "_meta": {"progressToken": token}});
let (id, receiver) = self.open("tools/call", params).await?;
let mut progress = self.progress.subscribe();
tokio::pin!(cancelled);
let reply = self.await_reply(id, receiver);
tokio::pin!(reply);
loop {
tokio::select! {
outcome = &mut reply => return outcome,
() = &mut cancelled => {
let notice = json!({"requestId": id, "reason": "activity cancelled by the Aion engine"});
if let Err(error) = self.notify("notifications/cancelled", notice).await {
tracing::warn!(server = %self.name, %error, "could not send cancellation");
}
self.pending.lock().await.remove(&id);
return Err(ClientError::Cancelled { server: self.name.clone() });
}
event = progress.recv() => {
match event {
Ok(params) => {
let mine = params.get("progressToken").and_then(Value::as_str) == Some(token.as_str());
if mine { on_progress(¶ms); }
}
Err(broadcast::error::RecvError::Lagged(n)) => tracing::debug!(server = %self.name, n, "progress lagged"),
Err(broadcast::error::RecvError::Closed) => {}
}
}
}
}
}
pub async fn shutdown(&self) {
{
let mut stdin = self.stdin.lock().await;
let _ = stdin.shutdown().await;
}
let mut child = self.child.lock().await;
match tokio::time::timeout(std::time::Duration::from_secs(5), child.wait()).await {
Ok(Ok(status)) => tracing::info!(server = %self.name, %status, "server exited"),
Ok(Err(error)) => tracing::warn!(server = %self.name, %error, "waiting on server"),
Err(_) => {
tracing::warn!(server = %self.name, "server did not exit after stdin closed; killing");
if let Err(error) = child.kill().await {
tracing::warn!(server = %self.name, %error, "kill failed");
}
}
}
}
}
pub async fn spawn_all(config: &McpConfig) -> Result<Vec<Arc<McpClient>>, ClientError> {
let mut clients: Vec<Arc<McpClient>> = Vec::new();
let servers: BTreeMap<&String, &ServerSpec> = config.enabled_servers().into_iter().collect();
for (name, spec) in servers {
match McpClient::spawn(name, spec).await {
Ok(client) => clients.push(client),
Err(error) => {
for started in &clients {
started.shutdown().await;
}
return Err(error);
}
}
}
Ok(clients)
}