1use crate::mcp::client::McpClient;
6use crate::mcp::oauth;
7use crate::mcp::protocol::{
8 CallToolResult, McpServerConfig, McpTool, McpTransportConfig, OAuthConfig, ToolContent,
9};
10use crate::mcp::transport::http_sse::HttpSseTransport;
11use crate::mcp::transport::stdio::StdioTransport;
12use crate::mcp::transport::streamable_http::StreamableHttpTransport;
13use crate::mcp::transport::McpTransport;
14use anyhow::{anyhow, Result};
15use std::collections::HashMap;
16use std::sync::Arc;
17use tokio::sync::RwLock;
18
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct McpServerStatus {
22 pub name: String,
23 pub connected: bool,
24 pub enabled: bool,
25 pub tool_count: usize,
26 pub error: Option<String>,
27}
28
29pub struct McpManager {
31 clients: RwLock<HashMap<String, Arc<McpClient>>>,
33 configs: RwLock<HashMap<String, McpServerConfig>>,
35 connect_errors: RwLock<HashMap<String, String>>,
37 last_used_at_ms: RwLock<HashMap<String, u64>>,
43}
44
45impl McpManager {
46 pub fn new() -> Self {
48 Self {
49 clients: RwLock::new(HashMap::new()),
50 configs: RwLock::new(HashMap::new()),
51 connect_errors: RwLock::new(HashMap::new()),
52 last_used_at_ms: RwLock::new(HashMap::new()),
53 }
54 }
55
56 pub async fn register_server(&self, config: McpServerConfig) {
58 let name = config.name.clone();
59 let mut configs = self.configs.write().await;
60 configs.insert(name.clone(), config);
61 tracing::info!("Registered MCP server: {}", name);
62 }
63
64 pub async fn connect(&self, name: &str) -> Result<()> {
69 let result = self.do_connect(name).await;
70 match &result {
71 Ok(_) => {
72 self.connect_errors.write().await.remove(name);
73 }
74 Err(e) => {
75 self.connect_errors
76 .write()
77 .await
78 .insert(name.to_string(), e.to_string());
79 }
80 }
81 result
82 }
83
84 async fn do_connect(&self, name: &str) -> Result<()> {
85 let config = {
87 let configs = self.configs.read().await;
88 configs
89 .get(name)
90 .cloned()
91 .ok_or_else(|| anyhow!("MCP server not found: {}", name))?
92 };
93
94 if !config.enabled {
95 return Err(anyhow!("MCP server is disabled: {}", name));
96 }
97
98 let auth_header = Self::resolve_auth_header(config.oauth.as_ref()).await?;
100
101 let transport: Arc<dyn McpTransport> = match &config.transport {
103 McpTransportConfig::Stdio { command, args } => Arc::new(
104 StdioTransport::spawn_with_timeout(
105 command,
106 args,
107 &config.env,
108 config.tool_timeout_secs,
109 )
110 .await?,
111 ),
112 McpTransportConfig::Http { url, headers } => {
113 let mut merged = headers.clone();
114 if let Some((k, v)) = &auth_header {
115 merged.insert(k.clone(), v.clone());
116 }
117 Arc::new(
118 HttpSseTransport::connect_with_timeout(url, merged, config.tool_timeout_secs)
119 .await?,
120 )
121 }
122 McpTransportConfig::StreamableHttp { url, headers } => {
123 let mut merged = headers.clone();
124 if let Some((k, v)) = &auth_header {
125 merged.insert(k.clone(), v.clone());
126 }
127 Arc::new(
128 StreamableHttpTransport::connect_with_timeout(
129 url,
130 merged,
131 config.tool_timeout_secs,
132 )
133 .await?,
134 )
135 }
136 };
137
138 let client = Arc::new(McpClient::new(name.to_string(), transport));
140
141 if let Err(error) = client.initialize().await {
145 if let Err(close_error) = client.close().await {
146 tracing::warn!(
147 server = %name,
148 error = %close_error,
149 "Failed to close MCP transport after initialize failure"
150 );
151 }
152 return Err(error);
153 }
154
155 let tools = match client.list_tools().await {
156 Ok(tools) => tools,
157 Err(error) => {
158 if let Err(close_error) = client.close().await {
159 tracing::warn!(
160 server = %name,
161 error = %close_error,
162 "Failed to close MCP transport after tool discovery failure"
163 );
164 }
165 return Err(error);
166 }
167 };
168 tracing::info!("MCP server '{}' connected with {} tools", name, tools.len());
169
170 {
173 let mut clients = self.clients.write().await;
174 clients.insert(name.to_string(), client);
175 }
176 self.last_used_at_ms
177 .write()
178 .await
179 .insert(name.to_string(), now_epoch_ms());
180
181 Ok(())
182 }
183
184 pub async fn disconnect(&self, name: &str) -> Result<()> {
186 let client = {
187 let mut clients = self.clients.write().await;
188 clients.remove(name)
189 };
190 self.last_used_at_ms.write().await.remove(name);
191
192 if let Some(client) = client {
193 client.close().await?;
194 tracing::info!("MCP server '{}' disconnected", name);
195 }
196
197 Ok(())
198 }
199
200 pub async fn remove_server(&self, name: &str) -> Result<bool> {
207 let client = self.clients.write().await.remove(name);
211 let had_error = self.connect_errors.write().await.remove(name).is_some();
212 let had_timestamp = self.last_used_at_ms.write().await.remove(name).is_some();
213 let had_config = self.configs.write().await.remove(name).is_some();
214 let removed = client.is_some() || had_error || had_timestamp || had_config;
215
216 if let Some(client) = client {
217 client.close().await?;
218 tracing::info!("MCP server '{}' removed", name);
219 }
220
221 Ok(removed)
222 }
223
224 pub async fn contains_server(&self, name: &str) -> bool {
226 self.configs.read().await.contains_key(name)
227 }
228
229 #[cfg(test)]
230 pub(crate) async fn insert_client_for_test(&self, name: &str, client: Arc<McpClient>) {
231 self.clients.write().await.insert(name.to_string(), client);
232 self.last_used_at_ms
233 .write()
234 .await
235 .insert(name.to_string(), now_epoch_ms());
236 }
237
238 pub async fn last_used_at_ms(&self, name: &str) -> Option<u64> {
241 self.last_used_at_ms.read().await.get(name).copied()
242 }
243
244 pub async fn touch(&self, name: &str) {
250 self.last_used_at_ms
251 .write()
252 .await
253 .insert(name.to_string(), now_epoch_ms());
254 }
255
256 pub async fn disconnect_idle(&self, idle_threshold_ms: u64) -> Vec<String> {
273 let cutoff = now_epoch_ms().saturating_sub(idle_threshold_ms);
274 let candidates: Vec<String> = {
276 let clients = self.clients.read().await;
277 let last_used = self.last_used_at_ms.read().await;
278 clients
279 .keys()
280 .filter(|name| match last_used.get(*name) {
281 Some(ts) => *ts < cutoff,
282 None => true,
285 })
286 .cloned()
287 .collect()
288 };
289 let mut disconnected = Vec::with_capacity(candidates.len());
290 for name in candidates {
291 match self.disconnect(&name).await {
292 Ok(()) => disconnected.push(name),
293 Err(e) => tracing::warn!(
294 server = %name,
295 error = %e,
296 "MCP idle disconnect failed; entry already removed from registry"
297 ),
298 }
299 }
300 {
307 let clients = self.clients.read().await;
308 self.last_used_at_ms
309 .write()
310 .await
311 .retain(|name, _| clients.contains_key(name));
312 }
313 disconnected
314 }
315
316 pub async fn all_configs(&self) -> Vec<McpServerConfig> {
318 self.configs.read().await.values().cloned().collect()
319 }
320
321 pub async fn get_all_tools(&self) -> Vec<(String, McpTool)> {
326 let clients = self.clients.read().await;
327 let mut all_tools = Vec::new();
328
329 for (server_name, client) in clients.iter() {
330 let tools = client.get_cached_tools().await;
331 for tool in tools {
332 all_tools.push((server_name.clone(), tool));
333 }
334 }
335
336 all_tools
337 }
338
339 pub async fn call_tool(
343 &self,
344 full_name: &str,
345 arguments: Option<serde_json::Value>,
346 ) -> Result<CallToolResult> {
347 let (server_name, tool_name) = Self::parse_tool_name(full_name)?;
349
350 let client = {
352 let clients = self.clients.read().await;
353 clients
354 .get(&server_name)
355 .cloned()
356 .ok_or_else(|| anyhow!("MCP server not connected: {}", server_name))?
357 };
358
359 self.last_used_at_ms
362 .write()
363 .await
364 .insert(server_name.clone(), now_epoch_ms());
365
366 client.call_tool(&tool_name, arguments).await
368 }
369
370 async fn resolve_auth_header(oauth: Option<&OAuthConfig>) -> Result<Option<(String, String)>> {
376 let Some(oauth) = oauth else {
377 return Ok(None);
378 };
379
380 let token = if let Some(static_token) = &oauth.access_token {
381 static_token.clone()
382 } else {
383 oauth::exchange_client_credentials(
384 &oauth.token_url,
385 &oauth.client_id,
386 oauth.client_secret.as_deref().unwrap_or(""),
387 &oauth.scopes,
388 )
389 .await?
390 };
391
392 Ok(Some((
393 "Authorization".to_string(),
394 format!("Bearer {}", token),
395 )))
396 }
397
398 fn parse_tool_name(full_name: &str) -> Result<(String, String)> {
400 if !full_name.starts_with("mcp__") {
402 return Err(anyhow!("Invalid MCP tool name: {}", full_name));
403 }
404
405 let rest = &full_name[5..]; let parts: Vec<&str> = rest.splitn(2, "__").collect();
407
408 if parts.len() != 2 {
409 return Err(anyhow!("Invalid MCP tool name format: {}", full_name));
410 }
411
412 Ok((parts[0].to_string(), parts[1].to_string()))
413 }
414
415 pub async fn get_status(&self) -> HashMap<String, McpServerStatus> {
417 let configs = self.configs.read().await;
418 let clients = self.clients.read().await;
419 let errors = self.connect_errors.read().await;
420 let mut status = HashMap::new();
421
422 for (name, config) in configs.iter() {
423 let client = clients.get(name);
424 let (connected, tool_count) = if let Some(c) = client {
425 (c.is_connected(), c.get_cached_tools().await.len())
426 } else {
427 (false, 0)
428 };
429
430 status.insert(
431 name.clone(),
432 McpServerStatus {
433 name: name.clone(),
434 connected,
435 enabled: config.enabled,
436 tool_count,
437 error: errors.get(name).cloned(),
438 },
439 );
440 }
441
442 status
443 }
444
445 pub async fn get_client(&self, name: &str) -> Option<Arc<McpClient>> {
447 let clients = self.clients.read().await;
448 clients.get(name).cloned()
449 }
450
451 pub async fn is_connected(&self, name: &str) -> bool {
453 let clients = self.clients.read().await;
454 clients.get(name).map(|c| c.is_connected()).unwrap_or(false)
455 }
456
457 pub async fn list_connected(&self) -> Vec<String> {
459 let clients = self.clients.read().await;
460 clients.keys().cloned().collect()
461 }
462
463 pub async fn get_server_tools(&self, name: &str) -> Vec<McpTool> {
465 let clients = self.clients.read().await;
466 match clients.get(name) {
467 Some(client) => client.get_cached_tools().await,
468 None => Vec::new(),
469 }
470 }
471}
472
473impl Default for McpManager {
474 fn default() -> Self {
475 Self::new()
476 }
477}
478
479fn now_epoch_ms() -> u64 {
485 std::time::SystemTime::now()
486 .duration_since(std::time::UNIX_EPOCH)
487 .map(|d| d.as_millis() as u64)
488 .unwrap_or(0)
489}
490
491pub fn tool_result_to_string(result: &CallToolResult) -> String {
493 let mut output = String::new();
494
495 for content in &result.content {
496 match content {
497 ToolContent::Text { text } => {
498 output.push_str(text);
499 output.push('\n');
500 }
501 ToolContent::Image { data: _, mime_type } => {
502 output.push_str(&format!("[Image: {}]\n", mime_type));
503 }
504 ToolContent::Resource { resource } => {
505 if let Some(text) = &resource.text {
506 output.push_str(text);
507 output.push('\n');
508 } else {
509 output.push_str(&format!("[Resource: {}]\n", resource.uri));
510 }
511 }
512 }
513 }
514
515 output.trim_end().to_string()
516}
517
518#[cfg(test)]
519#[path = "manager/tests.rs"]
520mod tests;