use anyhow::{Context, Result};
use rmcp::{
model::{CallToolRequestParams, InitializeResult, Tool},
service::{RoleClient, RunningService},
transport::{ConfigureCommandExt, TokioChildProcess},
ServiceExt,
};
use serde_json::Value;
use std::borrow::Cow;
use tokio::process::Command;
pub struct McpClient {
service: RunningService<RoleClient, ()>,
server_info: InitializeResult,
tools: Vec<Tool>,
}
impl McpClient {
pub async fn new(command: &str, args: Vec<String>) -> Result<Self> {
let cmd = Command::new(command);
let transport = TokioChildProcess::new(cmd.configure(|c| {
for arg in args {
c.arg(arg);
}
}))
.context("Failed to create child process transport")?;
let service = ().serve(transport).await.context("Failed to connect to MCP server")?;
let server_info = service
.peer_info()
.cloned()
.context("Failed to get server info")?;
let tools_result = service
.list_tools(Default::default())
.await
.context("Failed to list tools from MCP server")?;
Ok(Self {
service,
server_info,
tools: tools_result.tools,
})
}
pub fn server_info(&self) -> &InitializeResult {
&self.server_info
}
pub fn tools(&self) -> &[Tool] {
&self.tools
}
pub async fn call_tool(&self, tool_name: &str, arguments: Option<Value>) -> Result<Value> {
let result = self
.service
.call_tool(CallToolRequestParams {
meta: None,
name: Cow::Owned(tool_name.to_string()),
arguments: arguments.and_then(|v| v.as_object().cloned()),
task: None,
})
.await
.context(format!("Failed to call tool: {}", tool_name))?;
Ok(serde_json::to_value(result)?)
}
pub async fn refresh_tools(&mut self) -> Result<()> {
let tools_result = self
.service
.list_tools(Default::default())
.await
.context("Failed to refresh tools list")?;
self.tools = tools_result.tools;
Ok(())
}
pub async fn close(self) -> Result<()> {
self.service
.cancel()
.await
.context("Failed to close the MCP server")?;
Ok(())
}
}