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 if !config.enabled {
97 return Err(anyhow!("MCP server is disabled: {}", name));
98 }
99
100 let auth_header = Self::resolve_auth_header(config.oauth.as_ref()).await?;
102
103 let transport: Arc<dyn McpTransport> = match &config.transport {
105 McpTransportConfig::Stdio { command, args } => Arc::new(
106 StdioTransport::spawn_with_timeout(
107 command,
108 args,
109 &config.env,
110 config.tool_timeout_secs,
111 )
112 .await?,
113 ),
114 McpTransportConfig::Http { url, headers } => {
115 let mut merged = headers.clone();
116 if let Some((k, v)) = &auth_header {
117 merged.insert(k.clone(), v.clone());
118 }
119 Arc::new(
120 HttpSseTransport::connect_with_timeout(url, merged, config.tool_timeout_secs)
121 .await?,
122 )
123 }
124 McpTransportConfig::StreamableHttp { url, headers } => {
125 let mut merged = headers.clone();
126 if let Some((k, v)) = &auth_header {
127 merged.insert(k.clone(), v.clone());
128 }
129 Arc::new(
130 StreamableHttpTransport::connect_with_timeout(
131 url,
132 merged,
133 config.tool_timeout_secs,
134 )
135 .await?,
136 )
137 }
138 };
139
140 let client = Arc::new(McpClient::new(name.to_string(), transport));
142
143 if let Err(error) = client.initialize().await {
147 if let Err(close_error) = client.close().await {
148 tracing::warn!(
149 server = %name,
150 error = %close_error,
151 "Failed to close MCP transport after initialize failure"
152 );
153 }
154 return Err(error);
155 }
156
157 let tools = match client.list_tools().await {
158 Ok(tools) => tools,
159 Err(error) => {
160 if let Err(close_error) = client.close().await {
161 tracing::warn!(
162 server = %name,
163 error = %close_error,
164 "Failed to close MCP transport after tool discovery failure"
165 );
166 }
167 return Err(error);
168 }
169 };
170 tracing::info!("MCP server '{}' connected with {} tools", name, tools.len());
171
172 {
175 let mut clients = self.clients.write().await;
176 clients.insert(name.to_string(), client);
177 }
178 self.last_used_at_ms
179 .write()
180 .await
181 .insert(name.to_string(), now_epoch_ms());
182
183 Ok(())
184 }
185
186 pub async fn disconnect(&self, name: &str) -> Result<()> {
188 let client = {
189 let mut clients = self.clients.write().await;
190 clients.remove(name)
191 };
192 self.last_used_at_ms.write().await.remove(name);
193
194 if let Some(client) = client {
195 client.close().await?;
196 tracing::info!("MCP server '{}' disconnected", name);
197 }
198
199 Ok(())
200 }
201
202 pub async fn remove_server(&self, name: &str) -> Result<bool> {
209 let client = self.clients.write().await.remove(name);
213 let had_error = self.connect_errors.write().await.remove(name).is_some();
214 let had_timestamp = self.last_used_at_ms.write().await.remove(name).is_some();
215 let had_config = self.configs.write().await.remove(name).is_some();
216 let removed = client.is_some() || had_error || had_timestamp || had_config;
217
218 if let Some(client) = client {
219 client.close().await?;
220 tracing::info!("MCP server '{}' removed", name);
221 }
222
223 Ok(removed)
224 }
225
226 pub async fn contains_server(&self, name: &str) -> bool {
228 self.configs.read().await.contains_key(name)
229 }
230
231 #[cfg(test)]
232 pub(crate) async fn insert_client_for_test(&self, name: &str, client: Arc<McpClient>) {
233 self.clients.write().await.insert(name.to_string(), client);
234 self.last_used_at_ms
235 .write()
236 .await
237 .insert(name.to_string(), now_epoch_ms());
238 }
239
240 pub async fn last_used_at_ms(&self, name: &str) -> Option<u64> {
243 self.last_used_at_ms.read().await.get(name).copied()
244 }
245
246 pub async fn touch(&self, name: &str) {
252 self.last_used_at_ms
253 .write()
254 .await
255 .insert(name.to_string(), now_epoch_ms());
256 }
257
258 pub async fn disconnect_idle(&self, idle_threshold_ms: u64) -> Vec<String> {
275 let cutoff = now_epoch_ms().saturating_sub(idle_threshold_ms);
276 let candidates: Vec<String> = {
278 let clients = self.clients.read().await;
279 let last_used = self.last_used_at_ms.read().await;
280 clients
281 .keys()
282 .filter(|name| match last_used.get(*name) {
283 Some(ts) => *ts < cutoff,
284 None => true,
287 })
288 .cloned()
289 .collect()
290 };
291 let mut disconnected = Vec::with_capacity(candidates.len());
292 for name in candidates {
293 match self.disconnect(&name).await {
294 Ok(()) => disconnected.push(name),
295 Err(e) => tracing::warn!(
296 server = %name,
297 error = %e,
298 "MCP idle disconnect failed; entry already removed from registry"
299 ),
300 }
301 }
302 {
309 let clients = self.clients.read().await;
310 self.last_used_at_ms
311 .write()
312 .await
313 .retain(|name, _| clients.contains_key(name));
314 }
315 disconnected
316 }
317
318 pub async fn all_configs(&self) -> Vec<McpServerConfig> {
320 self.configs.read().await.values().cloned().collect()
321 }
322
323 pub async fn get_all_tools(&self) -> Vec<(String, McpTool)> {
329 let clients = self.clients.read().await;
330 let mut all_tools = Vec::new();
331
332 for (server_name, client) in clients.iter() {
333 let tools = client.get_cached_tools().await;
334 for tool in tools {
335 all_tools.push((server_name.clone(), tool));
336 }
337 }
338
339 all_tools
340 }
341
342 pub async fn call_tool(
346 &self,
347 full_name: &str,
348 arguments: Option<serde_json::Value>,
349 ) -> Result<CallToolResult> {
350 let (server_name, tool_name) = Self::parse_tool_name(full_name)?;
352
353 let client = {
355 let clients = self.clients.read().await;
356 clients
357 .get(&server_name)
358 .cloned()
359 .ok_or_else(|| anyhow!("MCP server not connected: {}", server_name))?
360 };
361
362 self.last_used_at_ms
365 .write()
366 .await
367 .insert(server_name.clone(), now_epoch_ms());
368
369 client.call_tool(&tool_name, arguments).await
371 }
372
373 async fn resolve_auth_header(oauth: Option<&OAuthConfig>) -> Result<Option<(String, String)>> {
379 let Some(oauth) = oauth else {
380 return Ok(None);
381 };
382
383 let token = if let Some(static_token) = &oauth.access_token {
384 static_token.clone()
385 } else {
386 oauth::exchange_client_credentials(
387 &oauth.token_url,
388 &oauth.client_id,
389 oauth.client_secret.as_deref().unwrap_or(""),
390 &oauth.scopes,
391 )
392 .await?
393 };
394
395 Ok(Some((
396 "Authorization".to_string(),
397 format!("Bearer {}", token),
398 )))
399 }
400
401 fn parse_tool_name(full_name: &str) -> Result<(String, String)> {
403 if !full_name.starts_with("mcp__") {
405 return Err(anyhow!("Invalid MCP tool name: {}", full_name));
406 }
407
408 let rest = &full_name[5..]; let parts: Vec<&str> = rest.splitn(2, "__").collect();
410
411 if parts.len() != 2 {
412 return Err(anyhow!("Invalid MCP tool name format: {}", full_name));
413 }
414
415 Ok((parts[0].to_string(), parts[1].to_string()))
416 }
417
418 pub async fn get_status(&self) -> HashMap<String, McpServerStatus> {
420 let configs = self.configs.read().await;
421 let clients = self.clients.read().await;
422 let errors = self.connect_errors.read().await;
423 let mut status = HashMap::new();
424
425 for (name, config) in configs.iter() {
426 let client = clients.get(name);
427 let (connected, tool_count) = if let Some(c) = client {
428 (c.is_connected(), c.get_cached_tools().await.len())
429 } else {
430 (false, 0)
431 };
432
433 status.insert(
434 name.clone(),
435 McpServerStatus {
436 name: name.clone(),
437 connected,
438 enabled: config.enabled,
439 tool_count,
440 error: errors.get(name).cloned(),
441 },
442 );
443 }
444
445 status
446 }
447
448 pub async fn get_client(&self, name: &str) -> Option<Arc<McpClient>> {
450 let clients = self.clients.read().await;
451 clients.get(name).cloned()
452 }
453
454 pub async fn is_connected(&self, name: &str) -> bool {
456 let clients = self.clients.read().await;
457 clients.get(name).map(|c| c.is_connected()).unwrap_or(false)
458 }
459
460 pub async fn list_connected(&self) -> Vec<String> {
462 let clients = self.clients.read().await;
463 clients.keys().cloned().collect()
464 }
465
466 pub async fn get_server_tools(&self, name: &str) -> Vec<McpTool> {
468 let clients = self.clients.read().await;
469 match clients.get(name) {
470 Some(client) => client.get_cached_tools().await,
471 None => Vec::new(),
472 }
473 }
474}
475
476impl Default for McpManager {
477 fn default() -> Self {
478 Self::new()
479 }
480}
481
482fn now_epoch_ms() -> u64 {
488 std::time::SystemTime::now()
489 .duration_since(std::time::UNIX_EPOCH)
490 .map(|d| d.as_millis() as u64)
491 .unwrap_or(0)
492}
493
494#[cfg(test)]
495#[path = "manager/tests.rs"]
496mod tests;