1use crate::mcp::client::McpClient;
6use crate::mcp::oauth;
7use crate::mcp::protocol::{
8 CallToolResult, McpServerConfig, McpTool, McpTransportConfig, OAuthConfig,
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 std::time::Duration;
18use tokio::sync::RwLock;
19use tokio::time::timeout;
20
21pub use crate::mcp::result::tool_result_to_string;
22
23#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
25pub struct McpServerStatus {
26 pub name: String,
27 pub connected: bool,
28 pub enabled: bool,
29 pub tool_count: usize,
30 pub error: Option<String>,
31}
32
33pub struct McpManager {
35 clients: RwLock<HashMap<String, Arc<McpClient>>>,
37 configs: RwLock<HashMap<String, McpServerConfig>>,
39 connect_errors: RwLock<HashMap<String, String>>,
41 last_used_at_ms: RwLock<HashMap<String, u64>>,
47}
48
49impl McpManager {
50 pub fn new() -> Self {
52 Self {
53 clients: RwLock::new(HashMap::new()),
54 configs: RwLock::new(HashMap::new()),
55 connect_errors: RwLock::new(HashMap::new()),
56 last_used_at_ms: RwLock::new(HashMap::new()),
57 }
58 }
59
60 pub async fn register_server(&self, config: McpServerConfig) {
62 let name = config.name.clone();
63 let mut configs = self.configs.write().await;
64 configs.insert(name.clone(), config);
65 tracing::info!("Registered MCP server: {}", name);
66 }
67
68 pub async fn connect(&self, name: &str) -> Result<()> {
73 let result = self.do_connect(name).await;
74 match &result {
75 Ok(_) => {
76 self.connect_errors.write().await.remove(name);
77 }
78 Err(e) => {
79 self.connect_errors
80 .write()
81 .await
82 .insert(name.to_string(), e.to_string());
83 }
84 }
85 result
86 }
87
88 pub async fn connect_with_timeout(&self, name: &str, budget: Duration) -> Result<()> {
95 match timeout(budget, self.connect(name)).await {
96 Ok(result) => result,
97 Err(_) => {
98 let message = format!("MCP connect timed out after {}s", budget.as_secs().max(1));
99 self.connect_errors
100 .write()
101 .await
102 .insert(name.to_string(), message.clone());
103 Err(anyhow!(message))
104 }
105 }
106 }
107
108 async fn do_connect(&self, name: &str) -> Result<()> {
109 let config = {
111 let configs = self.configs.read().await;
112 configs
113 .get(name)
114 .cloned()
115 .ok_or_else(|| anyhow!("MCP server not found: {}", name))?
116 };
117
118 let (client, tools) = connect_ready_client(&config).await?;
119 tracing::info!("MCP server '{}' connected with {} tools", name, tools.len());
120
121 {
124 let mut clients = self.clients.write().await;
125 clients.insert(name.to_string(), client);
126 }
127 self.last_used_at_ms
128 .write()
129 .await
130 .insert(name.to_string(), now_epoch_ms());
131
132 Ok(())
133 }
134
135 pub async fn disconnect(&self, name: &str) -> Result<()> {
137 let client = {
138 let mut clients = self.clients.write().await;
139 clients.remove(name)
140 };
141 self.last_used_at_ms.write().await.remove(name);
142
143 if let Some(client) = client {
144 client.close().await?;
145 tracing::info!("MCP server '{}' disconnected", name);
146 }
147
148 Ok(())
149 }
150
151 pub async fn remove_server(&self, name: &str) -> Result<bool> {
158 let client = self.clients.write().await.remove(name);
162 let had_error = self.connect_errors.write().await.remove(name).is_some();
163 let had_timestamp = self.last_used_at_ms.write().await.remove(name).is_some();
164 let had_config = self.configs.write().await.remove(name).is_some();
165 let removed = client.is_some() || had_error || had_timestamp || had_config;
166
167 if let Some(client) = client {
168 client.close().await?;
169 tracing::info!("MCP server '{}' removed", name);
170 }
171
172 Ok(removed)
173 }
174
175 pub async fn contains_server(&self, name: &str) -> bool {
177 self.configs.read().await.contains_key(name)
178 }
179
180 #[cfg(test)]
181 pub(crate) async fn insert_client_for_test(&self, name: &str, client: Arc<McpClient>) {
182 self.clients.write().await.insert(name.to_string(), client);
183 self.last_used_at_ms
184 .write()
185 .await
186 .insert(name.to_string(), now_epoch_ms());
187 }
188
189 pub async fn last_used_at_ms(&self, name: &str) -> Option<u64> {
192 self.last_used_at_ms.read().await.get(name).copied()
193 }
194
195 pub async fn touch(&self, name: &str) {
201 self.last_used_at_ms
202 .write()
203 .await
204 .insert(name.to_string(), now_epoch_ms());
205 }
206
207 pub async fn disconnect_idle(&self, idle_threshold_ms: u64) -> Vec<String> {
224 let cutoff = now_epoch_ms().saturating_sub(idle_threshold_ms);
225 let candidates: Vec<String> = {
227 let clients = self.clients.read().await;
228 let last_used = self.last_used_at_ms.read().await;
229 clients
230 .keys()
231 .filter(|name| match last_used.get(*name) {
232 Some(ts) => *ts < cutoff,
233 None => true,
236 })
237 .cloned()
238 .collect()
239 };
240 let mut disconnected = Vec::with_capacity(candidates.len());
241 for name in candidates {
242 match self.disconnect(&name).await {
243 Ok(()) => disconnected.push(name),
244 Err(e) => tracing::warn!(
245 server = %name,
246 error = %e,
247 "MCP idle disconnect failed; entry already removed from registry"
248 ),
249 }
250 }
251 {
258 let clients = self.clients.read().await;
259 self.last_used_at_ms
260 .write()
261 .await
262 .retain(|name, _| clients.contains_key(name));
263 }
264 disconnected
265 }
266
267 pub async fn all_configs(&self) -> Vec<McpServerConfig> {
269 self.configs.read().await.values().cloned().collect()
270 }
271
272 pub async fn get_all_tools(&self) -> Vec<(String, McpTool)> {
278 let clients = self.clients.read().await;
279 let mut all_tools = Vec::new();
280
281 for (server_name, client) in clients.iter() {
282 let tools = client.get_cached_tools().await;
283 for tool in tools {
284 all_tools.push((server_name.clone(), tool));
285 }
286 }
287
288 all_tools
289 }
290
291 pub async fn call_tool(
295 &self,
296 full_name: &str,
297 arguments: Option<serde_json::Value>,
298 ) -> Result<CallToolResult> {
299 let servers = self
300 .configs
301 .read()
302 .await
303 .keys()
304 .cloned()
305 .collect::<Vec<_>>();
306 let (server_name, tool_name) = Self::resolve_published_name(full_name, &servers)?;
307 self.call_server_tool(&server_name, &tool_name, arguments)
308 .await
309 }
310
311 pub async fn call_server_tool(
316 &self,
317 server_name: &str,
318 tool_name: &str,
319 arguments: Option<serde_json::Value>,
320 ) -> Result<CallToolResult> {
321 let client = {
322 let clients = self.clients.read().await;
323 clients
324 .get(server_name)
325 .cloned()
326 .ok_or_else(|| anyhow!("MCP server not connected: {}", server_name))?
327 };
328
329 self.last_used_at_ms
332 .write()
333 .await
334 .insert(server_name.to_string(), now_epoch_ms());
335
336 client.call_tool(tool_name, arguments).await
338 }
339
340 async fn resolve_auth_header(oauth: Option<&OAuthConfig>) -> Result<Option<(String, String)>> {
346 let Some(oauth) = oauth else {
347 return Ok(None);
348 };
349
350 let token = if let Some(static_token) = &oauth.access_token {
351 static_token.clone()
352 } else {
353 oauth::exchange_client_credentials(
354 &oauth.token_url,
355 &oauth.client_id,
356 oauth.client_secret.as_deref().unwrap_or(""),
357 &oauth.scopes,
358 )
359 .await?
360 };
361
362 Ok(Some((
363 "Authorization".to_string(),
364 format!("Bearer {}", token),
365 )))
366 }
367
368 fn resolve_published_name(full_name: &str, servers: &[String]) -> Result<(String, String)> {
370 if !full_name.starts_with("mcp__") {
371 return Err(anyhow!("Invalid MCP tool name: {full_name}"));
372 }
373 let rest = &full_name["mcp__".len()..];
374 let matches = servers
375 .iter()
376 .map(String::as_str)
377 .filter(|server| {
378 !server.is_empty()
379 && rest
380 .strip_prefix(*server)
381 .is_some_and(|tail| tail.starts_with("__") && tail.len() > 2)
382 })
383 .collect::<Vec<_>>();
384 match matches.len() {
385 0 => Self::parse_tool_name(full_name),
386 1 => Ok((
387 matches[0].to_string(),
388 rest[matches[0].len() + 2..].to_string(),
389 )),
390 _ => Err(anyhow!(
391 "MCP tool name '{full_name}' is ambiguous among registered servers"
392 )),
393 }
394 }
395
396 fn parse_tool_name(full_name: &str) -> Result<(String, String)> {
397 if !full_name.starts_with("mcp__") {
399 return Err(anyhow!("Invalid MCP tool name: {}", full_name));
400 }
401
402 let rest = &full_name[5..]; let parts: Vec<&str> = rest.splitn(2, "__").collect();
404
405 if parts.len() != 2 {
406 return Err(anyhow!("Invalid MCP tool name format: {}", full_name));
407 }
408
409 Ok((parts[0].to_string(), parts[1].to_string()))
410 }
411
412 pub async fn get_status(&self) -> HashMap<String, McpServerStatus> {
414 let configs = self.configs.read().await;
415 let clients = self.clients.read().await;
416 let errors = self.connect_errors.read().await;
417 let mut status = HashMap::new();
418
419 for (name, config) in configs.iter() {
420 let client = clients.get(name);
421 let (connected, tool_count) = if let Some(c) = client {
422 (c.is_connected(), c.get_cached_tools().await.len())
423 } else {
424 (false, 0)
425 };
426
427 status.insert(
428 name.clone(),
429 McpServerStatus {
430 name: name.clone(),
431 connected,
432 enabled: config.enabled,
433 tool_count,
434 error: errors.get(name).cloned(),
435 },
436 );
437 }
438
439 status
440 }
441
442 pub async fn get_client(&self, name: &str) -> Option<Arc<McpClient>> {
444 let clients = self.clients.read().await;
445 clients.get(name).cloned()
446 }
447
448 pub async fn is_connected(&self, name: &str) -> bool {
450 let clients = self.clients.read().await;
451 clients.get(name).map(|c| c.is_connected()).unwrap_or(false)
452 }
453
454 pub async fn list_connected(&self) -> Vec<String> {
456 let clients = self.clients.read().await;
457 clients.keys().cloned().collect()
458 }
459
460 pub async fn get_server_tools(&self, name: &str) -> Vec<McpTool> {
462 let clients = self.clients.read().await;
463 match clients.get(name) {
464 Some(client) => client.get_cached_tools().await,
465 None => Vec::new(),
466 }
467 }
468}
469
470pub(crate) async fn connect_ready_client(
477 config: &McpServerConfig,
478) -> Result<(Arc<McpClient>, Vec<McpTool>)> {
479 if !config.enabled {
480 return Err(anyhow!("MCP server is disabled: {}", config.name));
481 }
482
483 let auth_header = McpManager::resolve_auth_header(config.oauth.as_ref()).await?;
484 let transport: Arc<dyn McpTransport> = match &config.transport {
485 McpTransportConfig::Stdio { command, args } => Arc::new(
486 StdioTransport::spawn_with_timeout(
487 command,
488 args,
489 &config.env,
490 config.tool_timeout_secs,
491 )
492 .await?,
493 ),
494 McpTransportConfig::Http { url, headers } => {
495 let mut merged = headers.clone();
496 if let Some((key, value)) = &auth_header {
497 merged.insert(key.clone(), value.clone());
498 }
499 Arc::new(
500 HttpSseTransport::connect_with_timeout(url, merged, config.tool_timeout_secs)
501 .await?,
502 )
503 }
504 McpTransportConfig::StreamableHttp { url, headers } => {
505 let mut merged = headers.clone();
506 if let Some((key, value)) = &auth_header {
507 merged.insert(key.clone(), value.clone());
508 }
509 Arc::new(
510 StreamableHttpTransport::connect_with_timeout(
511 url,
512 merged,
513 config.tool_timeout_secs,
514 )
515 .await?,
516 )
517 }
518 };
519
520 let client = Arc::new(McpClient::new(config.name.clone(), transport));
521 if let Err(error) = client.initialize().await {
522 if let Err(close_error) = client.close().await {
523 tracing::warn!(
524 server = %config.name,
525 error = %close_error,
526 "Failed to close MCP transport after initialize failure"
527 );
528 }
529 return Err(error);
530 }
531 let tools = match client.list_tools().await {
532 Ok(tools) => tools,
533 Err(error) => {
534 if let Err(close_error) = client.close().await {
535 tracing::warn!(
536 server = %config.name,
537 error = %close_error,
538 "Failed to close MCP transport after tool discovery failure"
539 );
540 }
541 return Err(error);
542 }
543 };
544 Ok((client, tools))
545}
546
547impl Default for McpManager {
548 fn default() -> Self {
549 Self::new()
550 }
551}
552
553fn now_epoch_ms() -> u64 {
559 std::time::SystemTime::now()
560 .duration_since(std::time::UNIX_EPOCH)
561 .map(|d| d.as_millis() as u64)
562 .unwrap_or(0)
563}
564
565#[cfg(test)]
566#[path = "manager/tests.rs"]
567mod tests;