use crate::error::ToolError;
use crate::tool::{ToolDefinition, ToolDyn};
use std::future::Future;
use std::pin::Pin;
use std::process::Stdio;
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
use tokio::process::{Child, ChildStdin, ChildStdout};
use tokio::sync::{Mutex, RwLock};
#[derive(Clone)]
pub struct StdioMcpClient {
inner: Arc<Mutex<StdioMcpClientInner>>,
tools: Arc<RwLock<Vec<ToolDefinition>>>,
alive: Arc<AtomicBool>,
request_id: Arc<AtomicI64>,
}
struct StdioMcpClientInner {
_child: Child,
stdin: BufWriter<ChildStdin>,
stdout: BufReader<ChildStdout>,
}
impl StdioMcpClient {
pub async fn spawn(command: &str, args: &[&str]) -> Result<Self, ToolError> {
let mut child = tokio::process::Command::new(command)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
ToolError::McpError(format!("Failed to spawn MCP server '{command}': {e}"))
})?;
let stdin = child
.stdin
.take()
.ok_or_else(|| ToolError::McpError("Failed to capture MCP server stdin".into()))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| ToolError::McpError("Failed to capture MCP server stdout".into()))?;
if let Some(stderr) = child.stderr.take() {
tokio::spawn(async move {
let mut reader = BufReader::new(stderr);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => break,
Ok(_) => {
tracing::debug!(target: "mcp_stdio::stderr", "{}", line.trim_end());
}
Err(_) => break,
}
}
});
}
let client = Self {
inner: Arc::new(Mutex::new(StdioMcpClientInner {
_child: child,
stdin: BufWriter::new(stdin),
stdout: BufReader::new(stdout),
})),
tools: Arc::new(RwLock::new(Vec::new())),
alive: Arc::new(AtomicBool::new(true)),
request_id: Arc::new(AtomicI64::new(1)),
};
client
.send_request(
"initialize",
serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "sac-stdio-client",
"version": "0.1.0"
}
}),
)
.await?;
client
.send_notification("notifications/initialized", serde_json::json!({}))
.await?;
Ok(client)
}
pub async fn discover(&self) -> Result<Vec<ToolDefinition>, ToolError> {
let result = self
.send_request("tools/list", serde_json::json!({}))
.await?;
let tools_array = result
.get("tools")
.and_then(|t| t.as_array())
.cloned()
.unwrap_or_default();
let definitions: Vec<ToolDefinition> = tools_array
.into_iter()
.filter_map(|tool| {
let name = tool.get("name")?.as_str()?.to_string();
let description = tool
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("")
.to_string();
let parameters = tool
.get("inputSchema")
.cloned()
.unwrap_or(serde_json::json!({"type": "object", "properties": {}}));
Some(ToolDefinition {
name,
description,
parameters,
})
})
.collect();
*self.tools.write().await = definitions.clone();
Ok(definitions)
}
pub async fn call_tool(
&self,
name: &str,
args: serde_json::Value,
) -> Result<String, ToolError> {
let result = self
.send_request(
"tools/call",
serde_json::json!({
"name": name,
"arguments": args,
}),
)
.await?;
if result
.get("isError")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
let error_text = result
.get("content")
.and_then(|c| c.as_array())
.and_then(|arr| arr.first())
.and_then(|c| c.get("text"))
.and_then(|t| t.as_str())
.unwrap_or("Unknown error");
return Err(ToolError::ExecutionFailed(error_text.to_string()));
}
let text = result
.get("content")
.and_then(|c| c.as_array())
.map(|arr| {
arr.iter()
.filter_map(|item| item.get("text").and_then(|t| t.as_str()))
.collect::<Vec<_>>()
.join("\n")
})
.unwrap_or_else(|| serde_json::to_string(&result).unwrap_or_default());
Ok(text)
}
pub async fn as_tool_dyns(&self) -> Vec<Arc<dyn ToolDyn>> {
let tools = self.tools.read().await;
tools
.iter()
.map(|def| {
Arc::new(StdioMcpTool {
definition: def.clone(),
client: self.clone(),
}) as Arc<dyn ToolDyn>
})
.collect()
}
pub fn is_alive(&self) -> bool {
self.alive.load(Ordering::SeqCst)
}
pub async fn shutdown(&self) {
self.alive.store(false, Ordering::SeqCst);
let _ = self.send_notification("exit", serde_json::json!({})).await;
}
async fn send_request(
&self,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, ToolError> {
let id = self.request_id.fetch_add(1, Ordering::SeqCst);
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
});
let mut inner = self.inner.lock().await;
let line = serde_json::to_string(&request)
.map_err(|e| ToolError::McpError(format!("Failed to serialize request: {e}")))?;
inner
.stdin
.write_all(line.as_bytes())
.await
.map_err(|e| ToolError::McpError(format!("Failed to write to MCP server: {e}")))?;
inner
.stdin
.write_all(b"\n")
.await
.map_err(|e| ToolError::McpError(format!("Failed to write newline: {e}")))?;
inner
.stdin
.flush()
.await
.map_err(|e| ToolError::McpError(format!("Failed to flush: {e}")))?;
let mut response_line = String::new();
let bytes = inner
.stdout
.read_line(&mut response_line)
.await
.map_err(|e| ToolError::McpError(format!("Failed to read from MCP server: {e}")))?;
if bytes == 0 {
self.alive.store(false, Ordering::SeqCst);
return Err(ToolError::McpError(
"MCP server closed stdout (process died)".into(),
));
}
let response: serde_json::Value = serde_json::from_str(response_line.trim())
.map_err(|e| ToolError::McpError(format!("Invalid JSON from MCP server: {e}")))?;
if let Some(error) = response.get("error") {
let msg = error
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("Unknown error");
return Err(ToolError::McpError(format!("MCP error: {msg}")));
}
Ok(response
.get("result")
.cloned()
.unwrap_or(serde_json::Value::Null))
}
async fn send_notification(
&self,
method: &str,
params: serde_json::Value,
) -> Result<(), ToolError> {
let notification = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
});
let mut inner = self.inner.lock().await;
let line = serde_json::to_string(¬ification)
.map_err(|e| ToolError::McpError(format!("Failed to serialize notification: {e}")))?;
inner
.stdin
.write_all(line.as_bytes())
.await
.map_err(|e| ToolError::McpError(format!("Failed to write notification: {e}")))?;
inner
.stdin
.write_all(b"\n")
.await
.map_err(|e| ToolError::McpError(format!("Failed to write newline: {e}")))?;
inner
.stdin
.flush()
.await
.map_err(|e| ToolError::McpError(format!("Failed to flush: {e}")))?;
Ok(())
}
}
pub struct StdioMcpTool {
definition: ToolDefinition,
client: StdioMcpClient,
}
impl ToolDyn for StdioMcpTool {
fn name(&self) -> &str {
&self.definition.name
}
fn definition<'a>(
&'a self,
_prompt: String,
) -> Pin<Box<dyn Future<Output = ToolDefinition> + Send + 'a>> {
Box::pin(async { self.definition.clone() })
}
fn call<'a>(
&'a self,
args: String,
) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>> {
Box::pin(async move {
let parsed: serde_json::Value = serde_json::from_str(&args)?;
self.client.call_tool(&self.definition.name, parsed).await
})
}
}