use std::sync::Arc;
use async_trait::async_trait;
use rmcp::model::{CallToolRequestParams, ContentBlock};
use rmcp::service::{RoleClient, RunningService};
use rmcp::transport::{StreamableHttpClientTransport, TokioChildProcess};
use rmcp::ServiceExt;
use serde_json::Value;
use crate::error::{Error, ToolError};
use crate::schema::ObjectSchema;
use crate::tool::Tool;
use crate::value_objects::ToolOutput;
pub struct McpToolset {
#[allow(dead_code)]
service: Arc<RunningService<RoleClient, ()>>,
tools: Vec<Arc<dyn Tool>>,
}
impl McpToolset {
pub async fn connect_stdio(
command: impl AsRef<str>,
args: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, Error> {
let mut process = tokio::process::Command::new(command.as_ref());
process.args(args.into_iter().map(Into::into));
let transport = TokioChildProcess::new(process).map_err(|e| Error::Mcp {
message: format!("failed to spawn '{}': {e}", command.as_ref()),
})?;
let service = ().serve(transport).await.map_err(|e| Error::Mcp {
message: e.to_string(),
})?;
Self::from_running_service(service).await
}
pub async fn connect_http(url: impl Into<String>) -> Result<Self, Error> {
let transport = StreamableHttpClientTransport::from_uri(url.into());
let service = ().serve(transport).await.map_err(|e| Error::Mcp {
message: e.to_string(),
})?;
Self::from_running_service(service).await
}
async fn from_running_service(service: RunningService<RoleClient, ()>) -> Result<Self, Error> {
let service = Arc::new(service);
let mcp_tools = service.list_all_tools().await.map_err(|e| Error::Mcp {
message: e.to_string(),
})?;
let tools = mcp_tools
.into_iter()
.map(|tool| {
let name = tool.name.to_string();
let description = tool.description.map(|d| d.to_string()).unwrap_or_default();
let parameters = ObjectSchema::from_raw_json_schema(
name.clone(),
Value::Object((*tool.input_schema).clone()),
);
Arc::new(McpTool {
service: service.clone(),
name,
description,
parameters,
}) as Arc<dyn Tool>
})
.collect();
Ok(Self { service, tools })
}
pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
self.tools.clone()
}
}
struct McpTool {
service: Arc<RunningService<RoleClient, ()>>,
name: String,
description: String,
parameters: ObjectSchema,
}
#[async_trait]
impl Tool for McpTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn parameters(&self) -> &ObjectSchema {
&self.parameters
}
async fn call(&self, args: Value) -> Result<ToolOutput, ToolError> {
let mut params = CallToolRequestParams::new(self.name.clone());
if let Value::Object(arguments) = args {
params = params.with_arguments(arguments);
}
let result = self
.service
.call_tool(params)
.await
.map_err(|e| ToolError::Runtime {
name: self.name.clone(),
message: e.to_string(),
})?;
let text = result
.content
.iter()
.filter_map(|block| match block {
ContentBlock::Text(text) => Some(text.text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
if result.is_error == Some(true) {
return Err(ToolError::Runtime {
name: self.name.clone(),
message: if text.is_empty() {
"tool call failed with no further detail".to_string()
} else {
text
},
});
}
let mut artifacts = Vec::new();
artifacts.extend(result.structured_content);
for block in &result.content {
if !matches!(block, ContentBlock::Text(_)) {
if let Ok(value) = serde_json::to_value(block) {
artifacts.push(value);
}
}
}
Ok(ToolOutput {
content: text,
artifacts,
})
}
}