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>> {
139 let result = self.transport.send_request("tools/list", json!({})).await?;
140
141 let tools_array = result
142 .get("tools")
143 .and_then(|v| v.as_array())
144 .ok_or_else(|| anyhow!("MCP tools/list response missing 'tools' array"))?;
145
146 let mut tools = Vec::new();
147 for tool in tools_array {
148 let name = tool
149 .get("name")
150 .and_then(|v| v.as_str())
151 .unwrap_or("")
152 .to_string();
153 let description = tool
154 .get("description")
155 .and_then(|v| v.as_str())
156 .unwrap_or("")
157 .to_string();
158 let input_schema = tool
159 .get("inputSchema")
160 .cloned()
161 .unwrap_or_else(|| json!({"type": "object", "properties": {}}));
162
163 if !name.is_empty() {
164 tools.push(McpToolDef {
165 name,
166 description,
167 input_schema,
168 });
169 }
170 }
171
172 Ok(tools)
173 }
174
175 pub async fn call_tool(&self, name: &str, arguments: &Value) -> Result<McpToolResult> {
177 let params = json!({
178 "name": name,
179 "arguments": arguments,
180 });
181
182 let result = self.transport.send_request("tools/call", params).await?;
183
184 let is_error = result
185 .get("isError")
186 .and_then(|v| v.as_bool())
187 .unwrap_or(false);
188
189 let content_array = result
190 .get("content")
191 .and_then(|v| v.as_array())
192 .cloned()
193 .unwrap_or_default();
194
195 let mut content = Vec::new();
196 for block in content_array {
197 let block_type = block.get("type").and_then(|v| v.as_str()).unwrap_or("");
198 match block_type {
199 "text" => {
200 if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
201 content.push(ContentBlock::Text(text.to_string()));
202 }
203 },
204 "image" => {
205 let data = block
206 .get("data")
207 .and_then(|v| v.as_str())
208 .unwrap_or("")
209 .to_string();
210 let mime_type = block
211 .get("mimeType")
212 .and_then(|v| v.as_str())
213 .unwrap_or("image/png")
214 .to_string();
215 content.push(ContentBlock::Image { data, mime_type });
216 },
217 "audio" => {
218 let data = block
219 .get("data")
220 .and_then(|v| v.as_str())
221 .unwrap_or("")
222 .to_string();
223 let mime_type = block
224 .get("mimeType")
225 .and_then(|v| v.as_str())
226 .unwrap_or("audio/wav")
227 .to_string();
228 content.push(ContentBlock::Audio { data, mime_type });
229 },
230 "resource_link" => {
231 let uri = block
232 .get("uri")
233 .and_then(|v| v.as_str())
234 .unwrap_or("")
235 .to_string();
236 if uri.is_empty() {
237 continue;
238 }
239 content.push(ContentBlock::ResourceLink {
240 uri,
241 name: block.get("name").and_then(|v| v.as_str()).map(String::from),
242 description: block
243 .get("description")
244 .and_then(|v| v.as_str())
245 .map(String::from),
246 mime_type: block
247 .get("mimeType")
248 .and_then(|v| v.as_str())
249 .map(String::from),
250 });
251 },
252 "resource" => {
253 let res = match block.get("resource") {
255 Some(r) => r,
256 None => continue,
257 };
258 let uri = res
259 .get("uri")
260 .and_then(|v| v.as_str())
261 .unwrap_or("")
262 .to_string();
263 if uri.is_empty() {
264 continue;
265 }
266 content.push(ContentBlock::Resource {
267 uri,
268 mime_type: res
269 .get("mimeType")
270 .and_then(|v| v.as_str())
271 .map(String::from),
272 text: res.get("text").and_then(|v| v.as_str()).map(String::from),
273 blob: res.get("blob").and_then(|v| v.as_str()).map(String::from),
274 });
275 },
276 _ => {
277 if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
279 content.push(ContentBlock::Text(text.to_string()));
280 }
281 },
282 }
283 }
284
285 Ok(McpToolResult { content, is_error })
286 }
287
288 pub async fn shutdown(&self) {
290 self.transport.shutdown().await;
291 }
292}