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 crate::utils::redact_secrets(&config.args.join(" "))
37 );
38
39 match Self::start_one(name, config).await {
40 Ok((client, tools)) => {
41 let tool_count = tools.len();
42 for tool in &tools {
43 all_tools.push((name.clone(), tool.clone()));
44 }
45 info!(
46 "MCP server '{}' ready: {} tools ({})",
47 name,
48 tool_count,
49 client
50 .server_info
51 .as_ref()
52 .map(|s| s.name.as_str())
53 .unwrap_or("?")
54 );
55 servers.insert(name.clone(), client);
56 },
57 Err(e) => {
58 warn!("Failed to start MCP server '{}': {}", name, e);
59 },
60 }
61 }
62
63 Self {
64 servers,
65 tools: all_tools,
66 }
67 }
68
69 async fn start_one(
71 name: &str,
72 config: &McpServerConfig,
73 ) -> Result<(McpClient, Vec<McpToolDef>)> {
74 let transport = StdioTransport::spawn(&config.command, &config.args, &config.env).await?;
75 let mut client = McpClient::new(transport);
76
77 client
78 .initialize()
79 .await
80 .map_err(|e| anyhow!("MCP server '{}' initialization failed: {}", name, e))?;
81
82 let tools = client
83 .list_tools()
84 .await
85 .map_err(|e| anyhow!("MCP server '{}' tool discovery failed: {}", name, e))?;
86
87 Ok((client, tools))
88 }
89
90 pub fn get_all_tools(&self) -> &[(String, McpToolDef)] {
92 &self.tools
93 }
94
95 pub fn has_server(&self, name: &str) -> bool {
98 self.servers.contains_key(name)
99 }
100
101 pub fn has_servers(&self) -> bool {
103 !self.servers.is_empty()
104 }
105
106 pub async fn call_tool(
117 &self,
118 server_name: &str,
119 tool_name: &str,
120 arguments: &serde_json::Value,
121 ) -> Result<McpToolResult> {
122 let client = self
123 .servers
124 .get(server_name)
125 .ok_or_else(|| anyhow!("MCP server '{}' not found or not running", server_name))?;
126
127 client.call_tool(tool_name, arguments).await
128 }
129
130 pub fn format_tool_result(result: &McpToolResult) -> (String, Option<Vec<String>>) {
136 let mut text_parts = Vec::new();
137 let mut images = Vec::new();
138
139 for block in &result.content {
140 match block {
141 ContentBlock::Text(text) => text_parts.push(text.clone()),
142 ContentBlock::Image { data, .. } => images.push(data.clone()),
143 ContentBlock::Audio { data, mime_type } => {
144 images.push(data.clone());
145 text_parts.push(format!("[audio attachment: {}]", mime_type));
146 },
147 ContentBlock::ResourceLink {
148 uri,
149 name,
150 description,
151 mime_type,
152 } => {
153 let label = name.as_deref().unwrap_or(uri.as_str());
154 let desc = description.as_deref().unwrap_or("");
155 let mime = mime_type.as_deref().unwrap_or("");
156 text_parts.push(format!(
157 "[resource link: {} ({}) — {} → {}]",
158 label, mime, desc, uri
159 ));
160 },
161 ContentBlock::Resource {
162 uri,
163 mime_type,
164 text,
165 blob,
166 } => {
167 let mime = mime_type.as_deref().unwrap_or("");
168 if let Some(t) = text {
169 text_parts.push(format!("[resource {}]:\n{}", uri, t));
170 } else if let Some(b) = blob {
171 text_parts.push(format!(
172 "[resource {} ({}): {} bytes of base64]",
173 uri,
174 mime,
175 b.len()
176 ));
177 } else {
178 text_parts.push(format!("[resource {} ({})]", uri, mime));
179 }
180 },
181 }
182 }
183
184 let text = if text_parts.is_empty() {
185 if result.is_error {
186 "MCP tool returned an error with no message".to_string()
187 } else {
188 "MCP tool returned no text content".to_string()
189 }
190 } else {
191 text_parts.join("\n")
192 };
193
194 let images = if images.is_empty() {
195 None
196 } else {
197 Some(images)
198 };
199
200 (text, images)
201 }
202
203 pub async fn shutdown(&self) {
205 for (name, client) in &self.servers {
206 info!("Shutting down MCP server: {}", name);
207 client.shutdown().await;
208 }
209 }
210
211 pub async fn stop_server(&self, name: &str) -> bool {
221 match self.servers.get(name) {
222 Some(client) => {
223 info!("Stopping MCP server: {}", name);
224 client.shutdown().await;
225 true
226 },
227 None => false,
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[tokio::test]
237 async fn stop_unknown_server_returns_false() {
238 let mgr = McpServerManager::start(&HashMap::new()).await;
241 assert!(!mgr.has_servers());
242 assert!(!mgr.stop_server("does-not-exist").await);
243 }
244}