use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use super::transport::StdioTransport;
pub struct McpClient {
transport: StdioTransport,
pub server_info: Option<ServerInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
pub version: Option<String>,
}
#[derive(Debug, Clone)]
pub struct McpToolDef {
pub name: String,
pub description: String,
pub input_schema: Value,
}
#[derive(Debug, Clone)]
pub struct McpToolResult {
pub content: Vec<ContentBlock>,
pub is_error: bool,
}
#[derive(Debug, Clone)]
pub enum ContentBlock {
Text(String),
Image {
data: String,
mime_type: String,
},
Audio {
data: String,
mime_type: String,
},
ResourceLink {
uri: String,
name: Option<String>,
description: Option<String>,
mime_type: Option<String>,
},
Resource {
uri: String,
mime_type: Option<String>,
text: Option<String>,
blob: Option<String>,
},
}
impl McpClient {
pub fn new(transport: StdioTransport) -> Self {
Self {
transport,
server_info: None,
}
}
pub async fn initialize(&mut self) -> Result<ServerInfo> {
let result = self
.transport
.send_request(
"initialize",
json!({
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {
"name": "mermaid",
"version": env!("CARGO_PKG_VERSION"),
}
}),
)
.await?;
let server_info = ServerInfo {
name: result
.pointer("/serverInfo/name")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string(),
version: result
.pointer("/serverInfo/version")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
};
self.transport
.send_notification("notifications/initialized", json!({}))
.await?;
self.server_info = Some(server_info.clone());
Ok(server_info)
}
pub async fn list_tools(&self) -> Result<Vec<McpToolDef>> {
let result = self.transport.send_request("tools/list", json!({})).await?;
let tools_array = result
.get("tools")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow!("MCP tools/list response missing 'tools' array"))?;
let mut tools = Vec::new();
for tool in tools_array {
let name = tool
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let description = tool
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let input_schema = tool
.get("inputSchema")
.cloned()
.unwrap_or_else(|| json!({"type": "object", "properties": {}}));
if !name.is_empty() {
tools.push(McpToolDef {
name,
description,
input_schema,
});
}
}
Ok(tools)
}
pub async fn call_tool(&self, name: &str, arguments: &Value) -> Result<McpToolResult> {
let params = json!({
"name": name,
"arguments": arguments,
});
let result = self.transport.send_request("tools/call", params).await?;
let is_error = result
.get("isError")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let content_array = result
.get("content")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut content = Vec::new();
for block in content_array {
let block_type = block.get("type").and_then(|v| v.as_str()).unwrap_or("");
match block_type {
"text" => {
if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
content.push(ContentBlock::Text(text.to_string()));
}
},
"image" => {
let data = block
.get("data")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let mime_type = block
.get("mimeType")
.and_then(|v| v.as_str())
.unwrap_or("image/png")
.to_string();
content.push(ContentBlock::Image { data, mime_type });
},
"audio" => {
let data = block
.get("data")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let mime_type = block
.get("mimeType")
.and_then(|v| v.as_str())
.unwrap_or("audio/wav")
.to_string();
content.push(ContentBlock::Audio { data, mime_type });
},
"resource_link" => {
let uri = block
.get("uri")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if uri.is_empty() {
continue;
}
content.push(ContentBlock::ResourceLink {
uri,
name: block.get("name").and_then(|v| v.as_str()).map(String::from),
description: block
.get("description")
.and_then(|v| v.as_str())
.map(String::from),
mime_type: block
.get("mimeType")
.and_then(|v| v.as_str())
.map(String::from),
});
},
"resource" => {
let res = match block.get("resource") {
Some(r) => r,
None => continue,
};
let uri = res
.get("uri")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if uri.is_empty() {
continue;
}
content.push(ContentBlock::Resource {
uri,
mime_type: res
.get("mimeType")
.and_then(|v| v.as_str())
.map(String::from),
text: res.get("text").and_then(|v| v.as_str()).map(String::from),
blob: res.get("blob").and_then(|v| v.as_str()).map(String::from),
});
},
_ => {
if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
content.push(ContentBlock::Text(text.to_string()));
}
},
}
}
Ok(McpToolResult { content, is_error })
}
pub async fn shutdown(&self) {
self.transport.shutdown().await;
}
}