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 tokio::sync::RwLock;
18
19pub use crate::mcp::result::tool_result_to_string;
20
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23pub struct McpServerStatus {
24 pub name: String,
25 pub connected: bool,
26 pub enabled: bool,
27 pub tool_count: usize,
28 pub error: Option<String>,
29}
30
31pub struct McpManager {
33 clients: RwLock<HashMap<String, Arc<McpClient>>>,
35 configs: RwLock<HashMap<String, McpServerConfig>>,
37 connect_errors: RwLock<HashMap<String, String>>,
39 last_used_at_ms: RwLock<HashMap<String, u64>>,
45}
46
47impl McpManager {
48 pub fn new() -> Self {
50 Self {
51 clients: RwLock::new(HashMap::new()),
52 configs: RwLock::new(HashMap::new()),
53 connect_errors: RwLock::new(HashMap::new()),
54 last_used_at_ms: RwLock::new(HashMap::new()),
55 }
56 }
57
58 pub async fn register_server(&self, config: McpServerConfig) {
60 let name = config.name.clone();
61 let mut configs = self.configs.write().await;
62 configs.insert(name.clone(), config);
63 tracing::info!("Registered MCP server: {}", name);
64 }
65
66 pub async fn connect(&self, name: &str) -> Result<()> {
71 let result = self.do_connect(name).await;
72 match &result {
73 Ok(_) => {
74 self.connect_errors.write().await.remove(name);
75 }
76 Err(e) => {
77 self.connect_errors
78 .write()
79 .await
80 .insert(name.to_string(), e.to_string());
81 }
82 }
83 result
84 }
85
86 async fn do_connect(&self, name: &str) -> Result<()> {
87 let config = {
89 let configs = self.configs.read().await;
90 configs
91 .get(name)
92 .cloned()
93 .ok_or_else(|| anyhow!("MCP server not found: {}", name))?
94 };
95
96 let (client, tools) = connect_ready_client(&config).await?;
97 tracing::info!("MCP server '{}' connected with {} tools", name, tools.len());
98
99 {
102 let mut clients = self.clients.write().await;
103 clients.insert(name.to_string(), client);
104 }
105 self.last_used_at_ms
106 .write()
107 .await
108 .insert(name.to_string(), now_epoch_ms());
109
110 Ok(())
111 }
112
113 pub async fn disconnect(&self, name: &str) -> Result<()> {
115 let client = {
116 let mut clients = self.clients.write().await;
117 clients.remove(name)
118 };
119 self.last_used_at_ms.write().await.remove(name);
120
121 if let Some(client) = client {
122 client.close().await?;
123 tracing::info!("MCP server '{}' disconnected", name);
124 }
125
126 Ok(())
127 }
128
129 pub async fn remove_server(&self, name: &str) -> Result<bool> {
136 let client = self.clients.write().await.remove(name);
140 let had_error = self.connect_errors.write().await.remove(name).is_some();
141 let had_timestamp = self.last_used_at_ms.write().await.remove(name).is_some();
142 let had_config = self.configs.write().await.remove(name).is_some();
143 let removed = client.is_some() || had_error || had_timestamp || had_config;
144
145 if let Some(client) = client {
146 client.close().await?;
147 tracing::info!("MCP server '{}' removed", name);
148 }
149
150 Ok(removed)
151 }
152
153 pub async fn contains_server(&self, name: &str) -> bool {
155 self.configs.read().await.contains_key(name)
156 }
157
158 #[cfg(test)]
159 pub(crate) async fn insert_client_for_test(&self, name: &str, client: Arc<McpClient>) {
160 self.clients.write().await.insert(name.to_string(), client);
161 self.last_used_at_ms
162 .write()
163 .await
164 .insert(name.to_string(), now_epoch_ms());
165 }
166
167 pub async fn last_used_at_ms(&self, name: &str) -> Option<u64> {
170 self.last_used_at_ms.read().await.get(name).copied()
171 }
172
173 pub async fn touch(&self, name: &str) {
179 self.last_used_at_ms
180 .write()
181 .await
182 .insert(name.to_string(), now_epoch_ms());
183 }
184
185 pub async fn disconnect_idle(&self, idle_threshold_ms: u64) -> Vec<String> {
202 let cutoff = now_epoch_ms().saturating_sub(idle_threshold_ms);
203 let candidates: Vec<String> = {
205 let clients = self.clients.read().await;
206 let last_used = self.last_used_at_ms.read().await;
207 clients
208 .keys()
209 .filter(|name| match last_used.get(*name) {
210 Some(ts) => *ts < cutoff,
211 None => true,
214 })
215 .cloned()
216 .collect()
217 };
218 let mut disconnected = Vec::with_capacity(candidates.len());
219 for name in candidates {
220 match self.disconnect(&name).await {
221 Ok(()) => disconnected.push(name),
222 Err(e) => tracing::warn!(
223 server = %name,
224 error = %e,
225 "MCP idle disconnect failed; entry already removed from registry"
226 ),
227 }
228 }
229 {
236 let clients = self.clients.read().await;
237 self.last_used_at_ms
238 .write()
239 .await
240 .retain(|name, _| clients.contains_key(name));
241 }
242 disconnected
243 }
244
245 pub async fn all_configs(&self) -> Vec<McpServerConfig> {
247 self.configs.read().await.values().cloned().collect()
248 }
249
250 pub async fn get_all_tools(&self) -> Vec<(String, McpTool)> {
256 let clients = self.clients.read().await;
257 let mut all_tools = Vec::new();
258
259 for (server_name, client) in clients.iter() {
260 let tools = client.get_cached_tools().await;
261 for tool in tools {
262 all_tools.push((server_name.clone(), tool));
263 }
264 }
265
266 all_tools
267 }
268
269 pub async fn call_tool(
273 &self,
274 full_name: &str,
275 arguments: Option<serde_json::Value>,
276 ) -> Result<CallToolResult> {
277 let (server_name, tool_name) = Self::parse_tool_name(full_name)?;
279
280 let client = {
282 let clients = self.clients.read().await;
283 clients
284 .get(&server_name)
285 .cloned()
286 .ok_or_else(|| anyhow!("MCP server not connected: {}", server_name))?
287 };
288
289 self.last_used_at_ms
292 .write()
293 .await
294 .insert(server_name.clone(), now_epoch_ms());
295
296 client.call_tool(&tool_name, arguments).await
298 }
299
300 async fn resolve_auth_header(oauth: Option<&OAuthConfig>) -> Result<Option<(String, String)>> {
306 let Some(oauth) = oauth else {
307 return Ok(None);
308 };
309
310 let token = if let Some(static_token) = &oauth.access_token {
311 static_token.clone()
312 } else {
313 oauth::exchange_client_credentials(
314 &oauth.token_url,
315 &oauth.client_id,
316 oauth.client_secret.as_deref().unwrap_or(""),
317 &oauth.scopes,
318 )
319 .await?
320 };
321
322 Ok(Some((
323 "Authorization".to_string(),
324 format!("Bearer {}", token),
325 )))
326 }
327
328 fn parse_tool_name(full_name: &str) -> Result<(String, String)> {
330 if !full_name.starts_with("mcp__") {
332 return Err(anyhow!("Invalid MCP tool name: {}", full_name));
333 }
334
335 let rest = &full_name[5..]; let parts: Vec<&str> = rest.splitn(2, "__").collect();
337
338 if parts.len() != 2 {
339 return Err(anyhow!("Invalid MCP tool name format: {}", full_name));
340 }
341
342 Ok((parts[0].to_string(), parts[1].to_string()))
343 }
344
345 pub async fn get_status(&self) -> HashMap<String, McpServerStatus> {
347 let configs = self.configs.read().await;
348 let clients = self.clients.read().await;
349 let errors = self.connect_errors.read().await;
350 let mut status = HashMap::new();
351
352 for (name, config) in configs.iter() {
353 let client = clients.get(name);
354 let (connected, tool_count) = if let Some(c) = client {
355 (c.is_connected(), c.get_cached_tools().await.len())
356 } else {
357 (false, 0)
358 };
359
360 status.insert(
361 name.clone(),
362 McpServerStatus {
363 name: name.clone(),
364 connected,
365 enabled: config.enabled,
366 tool_count,
367 error: errors.get(name).cloned(),
368 },
369 );
370 }
371
372 status
373 }
374
375 pub async fn get_client(&self, name: &str) -> Option<Arc<McpClient>> {
377 let clients = self.clients.read().await;
378 clients.get(name).cloned()
379 }
380
381 pub async fn is_connected(&self, name: &str) -> bool {
383 let clients = self.clients.read().await;
384 clients.get(name).map(|c| c.is_connected()).unwrap_or(false)
385 }
386
387 pub async fn list_connected(&self) -> Vec<String> {
389 let clients = self.clients.read().await;
390 clients.keys().cloned().collect()
391 }
392
393 pub async fn get_server_tools(&self, name: &str) -> Vec<McpTool> {
395 let clients = self.clients.read().await;
396 match clients.get(name) {
397 Some(client) => client.get_cached_tools().await,
398 None => Vec::new(),
399 }
400 }
401}
402
403pub(crate) async fn connect_ready_client(
410 config: &McpServerConfig,
411) -> Result<(Arc<McpClient>, Vec<McpTool>)> {
412 if !config.enabled {
413 return Err(anyhow!("MCP server is disabled: {}", config.name));
414 }
415
416 let auth_header = McpManager::resolve_auth_header(config.oauth.as_ref()).await?;
417 let transport: Arc<dyn McpTransport> = match &config.transport {
418 McpTransportConfig::Stdio { command, args } => Arc::new(
419 StdioTransport::spawn_with_timeout(
420 command,
421 args,
422 &config.env,
423 config.tool_timeout_secs,
424 )
425 .await?,
426 ),
427 McpTransportConfig::Http { url, headers } => {
428 let mut merged = headers.clone();
429 if let Some((key, value)) = &auth_header {
430 merged.insert(key.clone(), value.clone());
431 }
432 Arc::new(
433 HttpSseTransport::connect_with_timeout(url, merged, config.tool_timeout_secs)
434 .await?,
435 )
436 }
437 McpTransportConfig::StreamableHttp { url, headers } => {
438 let mut merged = headers.clone();
439 if let Some((key, value)) = &auth_header {
440 merged.insert(key.clone(), value.clone());
441 }
442 Arc::new(
443 StreamableHttpTransport::connect_with_timeout(
444 url,
445 merged,
446 config.tool_timeout_secs,
447 )
448 .await?,
449 )
450 }
451 };
452
453 let client = Arc::new(McpClient::new(config.name.clone(), transport));
454 if let Err(error) = client.initialize().await {
455 if let Err(close_error) = client.close().await {
456 tracing::warn!(
457 server = %config.name,
458 error = %close_error,
459 "Failed to close MCP transport after initialize failure"
460 );
461 }
462 return Err(error);
463 }
464 let tools = match client.list_tools().await {
465 Ok(tools) => tools,
466 Err(error) => {
467 if let Err(close_error) = client.close().await {
468 tracing::warn!(
469 server = %config.name,
470 error = %close_error,
471 "Failed to close MCP transport after tool discovery failure"
472 );
473 }
474 return Err(error);
475 }
476 };
477 Ok((client, tools))
478}
479
480impl Default for McpManager {
481 fn default() -> Self {
482 Self::new()
483 }
484}
485
486fn now_epoch_ms() -> u64 {
492 std::time::SystemTime::now()
493 .duration_since(std::time::UNIX_EPOCH)
494 .map(|d| d.as_millis() as u64)
495 .unwrap_or(0)
496}
497
498#[cfg(test)]
499#[path = "manager/tests.rs"]
500mod tests;