mermaid_cli/mcp/
client.rs1use anyhow::{Result, anyhow};
9use serde::{Deserialize, Serialize};
10use serde_json::{Value, json};
11
12use super::transport::StdioTransport;
13
14pub struct McpClient {
16 transport: StdioTransport,
17 pub server_info: Option<ServerInfo>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ServerInfo {
24 pub name: String,
25 pub version: Option<String>,
26}
27
28#[derive(Debug, Clone)]
30pub struct McpToolDef {
31 pub name: String,
32 pub description: String,
33 pub input_schema: Value,
34}
35
36#[derive(Debug, Clone)]
38pub struct McpToolResult {
39 pub content: Vec<ContentBlock>,
40 pub is_error: bool,
41}
42
43#[derive(Debug, Clone)]
47pub enum ContentBlock {
48 Text(String),
49 Image {
50 data: String,
51 mime_type: String,
52 },
53 Audio {
58 data: String,
59 mime_type: String,
60 },
61 ResourceLink {
64 uri: String,
65 name: Option<String>,
66 description: Option<String>,
67 mime_type: Option<String>,
68 },
69 Resource {
73 uri: String,
74 mime_type: Option<String>,
75 text: Option<String>,
76 blob: Option<String>,
77 },
78}
79
80impl McpClient {
81 pub fn new(transport: StdioTransport) -> Self {
83 Self {
84 transport,
85 server_info: None,
86 }
87 }
88
89 pub async fn initialize(&mut self) -> Result<ServerInfo> {
94 let result = self
95 .transport
96 .send_request(
97 "initialize",
98 json!({
99 "protocolVersion": "2025-11-25",
106 "capabilities": {},
107 "clientInfo": {
108 "name": "mermaid",
109 "version": env!("CARGO_PKG_VERSION"),
110 }
111 }),
112 )
113 .await?;
114
115 let server_info = ServerInfo {
117 name: result
118 .pointer("/serverInfo/name")
119 .and_then(|v| v.as_str())
120 .unwrap_or("unknown")
121 .to_string(),
122 version: result
123 .pointer("/serverInfo/version")
124 .and_then(|v| v.as_str())
125 .map(|s| s.to_string()),
126 };
127
128 self.transport
130 .send_notification("notifications/initialized", json!({}))
131 .await?;
132
133 self.server_info = Some(server_info.clone());
134 Ok(server_info)
135 }
136
137 pub async fn list_tools(&self) -> Result<Vec<McpToolDef>> {
142 const MAX_PAGES: usize = 100;
143 let mut tools = Vec::new();
144 let mut cursor: Option<String> = None;
145
146 for _ in 0..MAX_PAGES {
147 let params = match &cursor {
148 Some(c) => json!({ "cursor": c }),
149 None => json!({}),
150 };
151 let result = self.transport.send_request("tools/list", params).await?;
152
153 let tools_array = result
154 .get("tools")
155 .and_then(|v| v.as_array())
156 .ok_or_else(|| anyhow!("MCP tools/list response missing 'tools' array"))?;
157
158 for tool in tools_array {
159 let name = tool
160 .get("name")
161 .and_then(|v| v.as_str())
162 .unwrap_or("")
163 .to_string();
164 let description = tool
165 .get("description")
166 .and_then(|v| v.as_str())
167 .unwrap_or("")
168 .to_string();
169 let input_schema = tool
170 .get("inputSchema")
171 .cloned()
172 .unwrap_or_else(|| json!({"type": "object", "properties": {}}));
173
174 if !name.is_empty() {
175 tools.push(McpToolDef {
176 name,
177 description,
178 input_schema,
179 });
180 }
181 }
182
183 match result.get("nextCursor").and_then(|v| v.as_str()) {
184 Some(next) if !next.is_empty() => cursor = Some(next.to_string()),
185 _ => break,
186 }
187 }
188
189 Ok(tools)
190 }
191
192 pub async fn call_tool(&self, name: &str, arguments: &Value) -> Result<McpToolResult> {
194 let params = json!({
195 "name": name,
196 "arguments": arguments,
197 });
198
199 let result = self
200 .transport
201 .send_request_with_timeout(
202 "tools/call",
203 params,
204 StdioTransport::tool_call_timeout_secs(),
205 )
206 .await?;
207
208 let is_error = result
209 .get("isError")
210 .and_then(|v| v.as_bool())
211 .unwrap_or(false);
212
213 let content_array = result
214 .get("content")
215 .and_then(|v| v.as_array())
216 .cloned()
217 .unwrap_or_default();
218
219 let mut content = Vec::new();
220 for block in content_array {
221 let block_type = block.get("type").and_then(|v| v.as_str()).unwrap_or("");
222 match block_type {
223 "text" => {
224 if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
225 content.push(ContentBlock::Text(text.to_string()));
226 }
227 },
228 "image" => {
229 let data = block
230 .get("data")
231 .and_then(|v| v.as_str())
232 .unwrap_or("")
233 .to_string();
234 let mime_type = block
235 .get("mimeType")
236 .and_then(|v| v.as_str())
237 .unwrap_or("image/png")
238 .to_string();
239 content.push(ContentBlock::Image { data, mime_type });
240 },
241 "audio" => {
242 let data = block
243 .get("data")
244 .and_then(|v| v.as_str())
245 .unwrap_or("")
246 .to_string();
247 let mime_type = block
248 .get("mimeType")
249 .and_then(|v| v.as_str())
250 .unwrap_or("audio/wav")
251 .to_string();
252 content.push(ContentBlock::Audio { data, mime_type });
253 },
254 "resource_link" => {
255 let uri = block
256 .get("uri")
257 .and_then(|v| v.as_str())
258 .unwrap_or("")
259 .to_string();
260 if uri.is_empty() {
261 continue;
262 }
263 content.push(ContentBlock::ResourceLink {
264 uri,
265 name: block.get("name").and_then(|v| v.as_str()).map(String::from),
266 description: block
267 .get("description")
268 .and_then(|v| v.as_str())
269 .map(String::from),
270 mime_type: block
271 .get("mimeType")
272 .and_then(|v| v.as_str())
273 .map(String::from),
274 });
275 },
276 "resource" => {
277 let res = match block.get("resource") {
279 Some(r) => r,
280 None => continue,
281 };
282 let uri = res
283 .get("uri")
284 .and_then(|v| v.as_str())
285 .unwrap_or("")
286 .to_string();
287 if uri.is_empty() {
288 continue;
289 }
290 content.push(ContentBlock::Resource {
291 uri,
292 mime_type: res
293 .get("mimeType")
294 .and_then(|v| v.as_str())
295 .map(String::from),
296 text: res.get("text").and_then(|v| v.as_str()).map(String::from),
297 blob: res.get("blob").and_then(|v| v.as_str()).map(String::from),
298 });
299 },
300 _ => {
301 if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
303 content.push(ContentBlock::Text(text.to_string()));
304 }
305 },
306 }
307 }
308
309 Ok(McpToolResult { content, is_error })
310 }
311
312 pub async fn shutdown(&self) {
314 self.transport.shutdown().await;
315 }
316}