use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, Command, Stdio};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader as TokioBufReader};
use tokio::process::{Child as TokioChild, Command as TokioCommand};
use tokio::sync::mpsc;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MinimalMcpError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Protocol error: {0}")]
Protocol(String),
#[error("Transport error: {0}")]
Transport(String),
}
pub type MinimalMcpResult<T> = Result<T, MinimalMcpError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpMessage {
pub jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<McpError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl McpMessage {
pub fn request(id: Value, method: String, params: Option<Value>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id: Some(id),
method: Some(method),
params,
result: None,
error: None,
}
}
pub fn response(id: Value, result: Value) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id: Some(id),
method: None,
params: None,
result: Some(result),
error: None,
}
}
pub fn error_response(id: Value, error: McpError) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id: Some(id),
method: None,
params: None,
result: None,
error: Some(error),
}
}
pub fn notification(method: String, params: Option<Value>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id: None,
method: Some(method),
params,
result: None,
error: None,
}
}
}
pub struct MinimalMcpClient {
child: Option<TokioChild>,
stdin: Option<tokio::process::ChildStdin>,
stdout_reader: Option<TokioBufReader<tokio::process::ChildStdout>>,
next_id: u64,
server_info: Option<Value>,
}
impl MinimalMcpClient {
pub fn new() -> Self {
Self {
child: None,
stdin: None,
stdout_reader: None,
next_id: 1,
server_info: None,
}
}
pub async fn connect(&mut self, command: &str, args: &[String], env: HashMap<String, String>) -> MinimalMcpResult<()> {
let mut cmd = TokioCommand::new(command);
cmd.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (key, value) in env {
cmd.env(key, value);
}
let mut child = cmd.spawn()?;
let stdin = child.stdin.take().ok_or_else(|| MinimalMcpError::Transport("Failed to get stdin".to_string()))?;
let stdout = child.stdout.take().ok_or_else(|| MinimalMcpError::Transport("Failed to get stdout".to_string()))?;
self.child = Some(child);
self.stdin = Some(stdin);
self.stdout_reader = Some(TokioBufReader::new(stdout));
self.initialize().await?;
Ok(())
}
async fn initialize(&mut self) -> MinimalMcpResult<()> {
let init_request = McpMessage::request(
json!(self.next_id),
"initialize".to_string(),
Some(json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "coderlib",
"version": "0.1.0"
}
}))
);
self.next_id += 1;
self.send_message(init_request).await?;
if let Some(response) = self.read_message().await? {
if let Some(result) = response.result {
self.server_info = Some(result);
}
}
let initialized = McpMessage::notification("initialized".to_string(), None);
self.send_message(initialized).await?;
Ok(())
}
async fn send_message(&mut self, message: McpMessage) -> MinimalMcpResult<()> {
if let Some(stdin) = &mut self.stdin {
let json_str = serde_json::to_string(&message)?;
stdin.write_all(json_str.as_bytes()).await?;
stdin.write_all(b"\n").await?;
stdin.flush().await?;
}
Ok(())
}
async fn read_message(&mut self) -> MinimalMcpResult<Option<McpMessage>> {
if let Some(reader) = &mut self.stdout_reader {
let mut line = String::new();
match reader.read_line(&mut line).await? {
0 => Ok(None), _ => {
let message: McpMessage = serde_json::from_str(&line)?;
Ok(Some(message))
}
}
} else {
Ok(None)
}
}
pub async fn list_tools(&mut self) -> MinimalMcpResult<Vec<Value>> {
let request = McpMessage::request(
json!(self.next_id),
"tools/list".to_string(),
None
);
self.next_id += 1;
self.send_message(request).await?;
if let Some(response) = self.read_message().await? {
if let Some(result) = response.result {
if let Some(tools) = result.get("tools") {
if let Some(tools_array) = tools.as_array() {
return Ok(tools_array.clone());
}
}
}
}
Ok(vec![])
}
pub async fn call_tool(&mut self, name: &str, arguments: Option<Value>) -> MinimalMcpResult<Value> {
let request = McpMessage::request(
json!(self.next_id),
"tools/call".to_string(),
Some(json!({
"name": name,
"arguments": arguments
}))
);
self.next_id += 1;
self.send_message(request).await?;
if let Some(response) = self.read_message().await? {
if let Some(result) = response.result {
return Ok(result);
} else if let Some(error) = response.error {
return Err(MinimalMcpError::Protocol(format!("Tool call failed: {}", error.message)));
}
}
Err(MinimalMcpError::Protocol("No response received".to_string()))
}
pub async fn disconnect(&mut self) -> MinimalMcpResult<()> {
if let Some(mut child) = self.child.take() {
let _ = child.kill().await;
}
Ok(())
}
}
impl Drop for MinimalMcpClient {
fn drop(&mut self) {
if let Some(mut child) = self.child.take() {
let _ = child.start_kill();
}
}
}