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)> {
327 let clients = self.clients.read().await;
328 let mut all_tools = Vec::new();
329
330 for (server_name, client) in clients.iter() {
331 let tools = client.get_cached_tools().await;
332 for tool in tools {
333 all_tools.push((server_name.clone(), tool));
334 }
335 }
336
337 all_tools
338 }
339
340 pub async fn call_tool(
344 &self,
345 full_name: &str,
346 arguments: Option<serde_json::Value>,
347 ) -> Result<CallToolResult> {
348 let (server_name, tool_name) = Self::parse_tool_name(full_name)?;
350
351 let client = {
353 let clients = self.clients.read().await;
354 clients
355 .get(&server_name)
356 .cloned()
357 .ok_or_else(|| anyhow!("MCP server not connected: {}", server_name))?
358 };
359
360 self.last_used_at_ms
363 .write()
364 .await
365 .insert(server_name.clone(), now_epoch_ms());
366
367 client.call_tool(&tool_name, arguments).await
369 }
370
371 async fn resolve_auth_header(oauth: Option<&OAuthConfig>) -> Result<Option<(String, String)>> {
377 let Some(oauth) = oauth else {
378 return Ok(None);
379 };
380
381 let token = if let Some(static_token) = &oauth.access_token {
382 static_token.clone()
383 } else {
384 oauth::exchange_client_credentials(
385 &oauth.token_url,
386 &oauth.client_id,
387 oauth.client_secret.as_deref().unwrap_or(""),
388 &oauth.scopes,
389 )
390 .await?
391 };
392
393 Ok(Some((
394 "Authorization".to_string(),
395 format!("Bearer {}", token),
396 )))
397 }
398
399 fn parse_tool_name(full_name: &str) -> Result<(String, String)> {
401 if !full_name.starts_with("mcp__") {
403 return Err(anyhow!("Invalid MCP tool name: {}", full_name));
404 }
405
406 let rest = &full_name[5..]; let parts: Vec<&str> = rest.splitn(2, "__").collect();
408
409 if parts.len() != 2 {
410 return Err(anyhow!("Invalid MCP tool name format: {}", full_name));
411 }
412
413 Ok((parts[0].to_string(), parts[1].to_string()))
414 }
415
416 pub async fn get_status(&self) -> HashMap<String, McpServerStatus> {
418 let configs = self.configs.read().await;
419 let clients = self.clients.read().await;
420 let errors = self.connect_errors.read().await;
421 let mut status = HashMap::new();
422
423 for (name, config) in configs.iter() {
424 let client = clients.get(name);
425 let (connected, tool_count) = if let Some(c) = client {
426 (c.is_connected(), c.get_cached_tools().await.len())
427 } else {
428 (false, 0)
429 };
430
431 status.insert(
432 name.clone(),
433 McpServerStatus {
434 name: name.clone(),
435 connected,
436 enabled: config.enabled,
437 tool_count,
438 error: errors.get(name).cloned(),
439 },
440 );
441 }
442
443 status
444 }
445
446 pub async fn get_client(&self, name: &str) -> Option<Arc<McpClient>> {
448 let clients = self.clients.read().await;
449 clients.get(name).cloned()
450 }
451
452 pub async fn is_connected(&self, name: &str) -> bool {
454 let clients = self.clients.read().await;
455 clients.get(name).map(|c| c.is_connected()).unwrap_or(false)
456 }
457
458 pub async fn list_connected(&self) -> Vec<String> {
460 let clients = self.clients.read().await;
461 clients.keys().cloned().collect()
462 }
463
464 pub async fn get_server_tools(&self, name: &str) -> Vec<McpTool> {
466 let clients = self.clients.read().await;
467 match clients.get(name) {
468 Some(client) => client.get_cached_tools().await,
469 None => Vec::new(),
470 }
471 }
472}
473
474impl Default for McpManager {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480fn now_epoch_ms() -> u64 {
486 std::time::SystemTime::now()
487 .duration_since(std::time::UNIX_EPOCH)
488 .map(|d| d.as_millis() as u64)
489 .unwrap_or(0)
490}
491
492pub fn tool_result_to_string(result: &CallToolResult) -> String {
494 let mut output = String::new();
495
496 for content in &result.content {
497 match content {
498 ToolContent::Text { text } => {
499 output.push_str(text);
500 output.push('\n');
501 }
502 ToolContent::Image { data: _, mime_type } => {
503 output.push_str(&format!("[Image: {}]\n", mime_type));
504 }
505 ToolContent::Resource { resource } => {
506 if let Some(text) = &resource.text {
507 output.push_str(text);
508 output.push('\n');
509 } else {
510 output.push_str(&format!("[Resource: {}]\n", resource.uri));
511 }
512 }
513 }
514 }
515
516 output.trim_end().to_string()
517}
518
519#[cfg(test)]
520#[path = "manager/tests.rs"]
521mod tests;