mermaid_cli/mcp/
server_manager.rs1use anyhow::{Result, anyhow};
7use std::collections::HashMap;
8use tracing::{info, warn};
9
10use super::client::{ContentBlock, McpClient, McpToolDef, McpToolResult};
11use super::transport::StdioTransport;
12use crate::app::McpServerConfig;
13
14pub struct McpServerManager {
16 servers: HashMap<String, McpClient>,
18 tools: Vec<(String, McpToolDef)>,
20}
21
22impl McpServerManager {
23 pub async fn start(configs: &HashMap<String, McpServerConfig>) -> Self {
27 let mut servers = HashMap::new();
28 let mut all_tools = Vec::new();
29
30 for (name, config) in configs {
31 info!(
32 "Starting MCP server: {} ({} {})",
33 name,
34 config.command,
35 config.args.join(" ")
36 );
37
38 match Self::start_one(name, config).await {
39 Ok((client, tools)) => {
40 let tool_count = tools.len();
41 for tool in &tools {
42 all_tools.push((name.clone(), tool.clone()));
43 }
44 info!(
45 "MCP server '{}' ready: {} tools ({})",
46 name,
47 tool_count,
48 client
49 .server_info
50 .as_ref()
51 .map(|s| s.name.as_str())
52 .unwrap_or("?")
53 );
54 servers.insert(name.clone(), client);
55 },
56 Err(e) => {
57 warn!("Failed to start MCP server '{}': {}", name, e);
58 },
59 }
60 }
61
62 Self {
63 servers,
64 tools: all_tools,
65 }
66 }
67
68 async fn start_one(
70 name: &str,
71 config: &McpServerConfig,
72 ) -> Result<(McpClient, Vec<McpToolDef>)> {
73 let transport = StdioTransport::spawn(&config.command, &config.args, &config.env).await?;
74 let mut client = McpClient::new(transport);
75
76 client
77 .initialize()
78 .await
79 .map_err(|e| anyhow!("MCP server '{}' initialization failed: {}", name, e))?;
80
81 let tools = client
82 .list_tools()
83 .await
84 .map_err(|e| anyhow!("MCP server '{}' tool discovery failed: {}", name, e))?;
85
86 Ok((client, tools))
87 }
88
89 pub fn get_all_tools(&self) -> &[(String, McpToolDef)] {
91 &self.tools
92 }
93
94 pub fn has_server(&self, name: &str) -> bool {
97 self.servers.contains_key(name)
98 }
99
100 pub fn has_servers(&self) -> bool {
102 !self.servers.is_empty()
103 }
104
105 pub async fn call_tool(
116 &self,
117 server_name: &str,
118 tool_name: &str,
119 arguments: &serde_json::Value,
120 ) -> Result<McpToolResult> {
121 let client = self
122 .servers
123 .get(server_name)
124 .ok_or_else(|| anyhow!("MCP server '{}' not found or not running", server_name))?;
125
126 client.call_tool(tool_name, arguments).await
127 }
128
129 pub fn format_tool_result(result: &McpToolResult) -> (String, Option<Vec<String>>) {
135 let mut text_parts = Vec::new();
136 let mut images = Vec::new();
137
138 for block in &result.content {
139 match block {
140 ContentBlock::Text(text) => text_parts.push(text.clone()),
141 ContentBlock::Image { data, .. } => images.push(data.clone()),
142 ContentBlock::Audio { data, mime_type } => {
143 images.push(data.clone());
144 text_parts.push(format!("[audio attachment: {}]", mime_type));
145 },
146 ContentBlock::ResourceLink {
147 uri,
148 name,
149 description,
150 mime_type,
151 } => {
152 let label = name.as_deref().unwrap_or(uri.as_str());
153 let desc = description.as_deref().unwrap_or("");
154 let mime = mime_type.as_deref().unwrap_or("");
155 text_parts.push(format!(
156 "[resource link: {} ({}) — {} → {}]",
157 label, mime, desc, uri
158 ));
159 },
160 ContentBlock::Resource {
161 uri,
162 mime_type,
163 text,
164 blob,
165 } => {
166 let mime = mime_type.as_deref().unwrap_or("");
167 if let Some(t) = text {
168 text_parts.push(format!("[resource {}]:\n{}", uri, t));
169 } else if let Some(b) = blob {
170 text_parts.push(format!(
171 "[resource {} ({}): {} bytes of base64]",
172 uri,
173 mime,
174 b.len()
175 ));
176 } else {
177 text_parts.push(format!("[resource {} ({})]", uri, mime));
178 }
179 },
180 }
181 }
182
183 let text = if text_parts.is_empty() {
184 if result.is_error {
185 "MCP tool returned an error with no message".to_string()
186 } else {
187 "MCP tool returned no text content".to_string()
188 }
189 } else {
190 text_parts.join("\n")
191 };
192
193 let images = if images.is_empty() {
194 None
195 } else {
196 Some(images)
197 };
198
199 (text, images)
200 }
201
202 pub async fn shutdown(&self) {
204 for (name, client) in &self.servers {
205 info!("Shutting down MCP server: {}", name);
206 client.shutdown().await;
207 }
208 }
209}