1use hashbrown::HashMap;
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::sync::{RwLock, Semaphore};
10use tracing::{error, info, warn};
11
12use super::{McpElicitationHandler, McpProvider, McpSandboxContext, apply_ui_extension_gate};
13use rmcp::model::{ClientCapabilities, InitializeRequestParams};
14use vtcode_commons::MultiErrors;
15use vtcode_config::mcp::{McpAllowListConfig, McpProviderConfig};
16
17pub(crate) struct McpConnectionPool {
19 providers: Arc<RwLock<HashMap<String, Arc<McpProvider>>>>,
21 connection_semaphore: Arc<Semaphore>,
23 max_concurrent_connections: usize,
25 connection_timeout: Duration,
27}
28
29impl McpConnectionPool {
30 pub(crate) fn new(max_concurrent_connections: usize, connection_timeout_seconds: u64) -> Self {
31 Self {
32 providers: Arc::new(RwLock::new(HashMap::new())),
33 connection_semaphore: Arc::new(Semaphore::new(max_concurrent_connections)),
34 max_concurrent_connections,
35 connection_timeout: Duration::from_secs(connection_timeout_seconds),
36 }
37 }
38
39 pub(crate) async fn initialize_providers_parallel(
41 &self,
42 provider_configs: Vec<McpProviderConfig>,
43 elicitation_handler: Option<Arc<dyn McpElicitationHandler>>,
44 sandbox_context: Option<McpSandboxContext>,
45 tool_timeout: Option<Duration>,
46 allowlist_snapshot: &McpAllowListConfig,
47 advertise_ui_apps: bool,
48 ) -> Result<Vec<(String, Arc<McpProvider>)>, McpPoolError> {
49 use futures::future::join_all;
50
51 let tasks: Vec<_> = provider_configs
53 .into_iter()
54 .map(|config| {
55 let elicitation_handler = elicitation_handler.clone();
56 let sandbox_context = sandbox_context.clone();
57 let allowlist_snapshot = allowlist_snapshot.clone();
58
59 async move {
60 self.initialize_provider(
61 config,
62 elicitation_handler,
63 sandbox_context,
64 tool_timeout.unwrap_or(Duration::from_secs(30)),
65 allowlist_snapshot,
66 advertise_ui_apps,
67 )
68 .await
69 }
70 })
71 .collect();
72
73 let results = join_all(tasks).await;
75
76 let mut successful_providers = Vec::new();
78 let mut errors: MultiErrors<McpPoolError> = MultiErrors::new();
79
80 for result in results {
81 if let Some(provider) = errors.collect_result(result) {
82 successful_providers.push(provider);
83 }
84 }
85
86 if !errors.is_empty() {
87 warn!("Some MCP provider connections failed: {errors}");
88 }
89
90 Ok(successful_providers)
91 }
92
93 async fn initialize_provider(
95 &self,
96 config: McpProviderConfig,
97 elicitation_handler: Option<Arc<dyn McpElicitationHandler>>,
98 sandbox_context: Option<McpSandboxContext>,
99 tool_timeout: Duration,
100 allowlist_snapshot: McpAllowListConfig,
101 advertise_ui_apps: bool,
102 ) -> Result<(String, Arc<McpProvider>), McpPoolError> {
103 let _permit = self
105 .connection_semaphore
106 .acquire()
107 .await
108 .map_err(|e| McpPoolError::SemaphoreError(e.to_string()))?;
109
110 info!("Initializing MCP provider '{}'", config.name);
111
112 let provider = tokio::time::timeout(
114 self.connection_timeout,
115 McpProvider::connect(config.clone(), elicitation_handler, sandbox_context, advertise_ui_apps),
116 )
117 .await
118 .map_err(|_e| McpPoolError::ConnectionTimeout(config.name.clone()))?
119 .map_err(|e| McpPoolError::ConnectionError(config.name.clone(), e.to_string()))?;
120
121 let provider_startup_timeout = self.resolve_startup_timeout(&config);
123 let initialize_params = build_pool_initialize_params(advertise_ui_apps);
124 let tool_timeout_opt = Some(tool_timeout);
125
126 if let Err(err) = provider
127 .initialize(initialize_params, provider_startup_timeout, tool_timeout_opt, &allowlist_snapshot)
128 .await
129 {
130 return Err(McpPoolError::InitializationError(config.name.clone(), err.to_string()));
131 }
132
133 if let Err(err) = provider
135 .cached_tools_or_refresh_shared(&allowlist_snapshot, tool_timeout_opt)
136 .await
137 {
138 warn!("Failed to fetch tools for provider '{}': {}", config.name, err);
139 }
140
141 info!("Successfully initialized MCP provider '{}'", config.name);
142
143 Ok((config.name.clone(), Arc::new(provider)))
144 }
145
146 async fn get_provider(&self, name: &str) -> Option<Arc<McpProvider>> {
148 let providers = self.providers.read().await;
149 providers.get(name).cloned()
150 }
151
152 async fn get_all_providers(&self) -> Vec<Arc<McpProvider>> {
154 let providers = self.providers.read().await;
155 providers.values().cloned().collect()
156 }
157
158 pub async fn remove_provider(&self, name: &str) -> Option<Arc<McpProvider>> {
160 let mut providers = self.providers.write().await;
161 providers.remove(name)
162 }
163
164 async fn has_provider(&self, name: &str) -> bool {
166 let providers = self.providers.read().await;
167 providers.contains_key(name)
168 }
169
170 async fn stats(&self) -> ConnectionPoolStats {
172 let providers = self.providers.read().await;
173 let semaphore = self.connection_semaphore.available_permits();
174
175 ConnectionPoolStats {
176 active_connections: providers.len(),
177 available_permits: semaphore,
178 max_connections: self.max_concurrent_connections,
179 }
180 }
181
182 pub async fn shutdown_all(&self) {
184 let providers: Vec<_> = {
185 let mut providers = self.providers.write().await;
186 providers.drain().collect()
187 };
188
189 for (name, provider) in providers {
190 if let Err(err) = provider.shutdown().await {
191 error!("Failed to shutdown MCP provider '{}': {}", name, err);
192 }
193 }
194 }
195
196 pub async fn health_check(&self) -> HashMap<String, bool> {
200 let providers: Vec<_> = {
201 let providers = self.providers.read().await;
202 providers
203 .iter()
204 .map(|(name, provider)| (name.clone(), Arc::clone(provider)))
205 .collect()
206 };
207 let mut results = HashMap::with_capacity(providers.len());
208 for (name, provider) in providers {
209 let _previous = results.insert(name, provider.is_healthy().await);
210 }
211 results
212 }
213
214 pub async fn reconnect_unhealthy(
218 &self,
219 startup_timeout: Option<Duration>,
220 tool_timeout: Option<Duration>,
221 allowlist: &McpAllowListConfig,
222 ) -> Vec<String> {
223 let providers: Vec<_> = {
224 let providers = self.providers.read().await;
225 providers
226 .iter()
227 .map(|(name, provider)| (name.clone(), Arc::clone(provider)))
228 .collect()
229 };
230 let mut reconnected = Vec::new();
231 for (name, provider) in providers {
232 if !provider.is_healthy().await {
233 info!("Provider '{}' is unhealthy, attempting reconnect", name);
234 match provider.reconnect(startup_timeout, tool_timeout, allowlist).await {
235 Ok(()) => {
236 info!("Successfully reconnected MCP provider '{}'", name);
237 reconnected.push(name);
238 }
239 Err(err) => {
240 error!("Failed to reconnect MCP provider '{}': {}", name, err);
241 }
242 }
243 }
244 }
245 reconnected
246 }
247
248 fn resolve_startup_timeout(&self, config: &McpProviderConfig) -> Option<Duration> {
250 config.startup_timeout_ms.map(Duration::from_millis)
251 }
252}
253
254#[derive(Debug, Clone)]
256pub struct ConnectionPoolStats {
257 active_connections: usize,
258 available_permits: usize,
259 max_connections: usize,
260}
261
262pub(crate) struct PooledMcpManager {
264 pool: Arc<McpConnectionPool>,
266 tool_cache: Arc<super::tool_discovery_cache::ToolDiscoveryCache>,
268}
269
270impl PooledMcpManager {
271 fn new(max_concurrent_connections: usize, connection_timeout_seconds: u64, tool_cache_capacity: usize) -> Self {
272 Self {
273 pool: Arc::new(McpConnectionPool::new(max_concurrent_connections, connection_timeout_seconds)),
274 tool_cache: Arc::new(super::tool_discovery_cache::ToolDiscoveryCache::new(tool_cache_capacity)),
275 }
276 }
277
278 pub async fn initialize_providers(
280 &self,
281 provider_configs: Vec<McpProviderConfig>,
282 elicitation_handler: Option<Arc<dyn McpElicitationHandler>>,
283 sandbox_context: Option<McpSandboxContext>,
284 tool_timeout: Option<Duration>,
285 allowlist_snapshot: &McpAllowListConfig,
286 advertise_ui_apps: bool,
287 ) -> Result<Vec<(String, Arc<McpProvider>)>, McpPoolError> {
288 let providers = self
290 .pool
291 .initialize_providers_parallel(
292 provider_configs,
293 elicitation_handler,
294 sandbox_context,
295 tool_timeout,
296 allowlist_snapshot,
297 advertise_ui_apps,
298 )
299 .await?;
300
301 let mut pool_providers = self.pool.providers.write().await;
303 for (name, provider) in &providers {
304 drop(pool_providers.insert(name.clone(), provider.clone()));
305 }
306
307 Ok(providers)
308 }
309
310 pub async fn execute_tool(
312 &self,
313 provider_name: &str,
314 tool_name: &str,
315 arguments: serde_json::Value,
316 allowlist: &McpAllowListConfig,
317 tool_timeout: Option<Duration>,
318 ) -> Result<serde_json::Value, McpPoolError> {
319 let provider = self
320 .pool
321 .get_provider(provider_name)
322 .await
323 .ok_or_else(|| McpPoolError::ProviderNotFound(provider_name.to_string()))?;
324
325 let args_ref = &arguments;
327
328 let result = provider
330 .call_tool(tool_name, args_ref, tool_timeout, allowlist)
331 .await
332 .map_err(|e| McpPoolError::ToolExecutionError(provider_name.to_string(), e.to_string()))?;
333
334 Ok(serde_json::to_value(&result).unwrap_or(serde_json::Value::Null))
336 }
337
338 fn is_read_only_tool(&self, tool_name: &str) -> bool {
340 matches!(
343 tool_name,
344 "read_file"
345 | "list_directory"
346 | "search_files"
347 | "get_file_info"
348 | "read_environment"
349 | "get_system_info"
350 | "search_code"
351 | "analyze_code"
352 )
353 }
354
355 async fn stats(&self) -> PooledMcpStats {
357 let pool_stats = self.pool.stats().await;
358 let tool_cache_stats = self.tool_cache.stats();
359
360 PooledMcpStats {
361 connection_pool: pool_stats,
362 tool_cache: tool_cache_stats,
363 }
364 }
365
366 pub async fn shutdown(&self) {
368 self.pool.shutdown_all().await;
369 }
370}
371
372#[derive(Debug, Clone)]
374pub struct PooledMcpStats {
375 connection_pool: ConnectionPoolStats,
376 tool_cache: super::tool_discovery_cache::ToolCacheStats,
377}
378
379fn build_pool_initialize_params(advertise_ui_apps: bool) -> InitializeRequestParams {
381 let mut capabilities = ClientCapabilities::default();
382 apply_ui_extension_gate(&mut capabilities, advertise_ui_apps);
383 InitializeRequestParams::new(capabilities, super::utils::build_client_implementation())
384 .with_protocol_version(super::rmcp_client::latest_protocol_version())
385}
386
387#[derive(Debug, thiserror::Error)]
389pub enum McpPoolError {
390 #[error("Connection timeout for provider '{0}'")]
391 ConnectionTimeout(String),
392
393 #[error("Connection error for provider '{0}': {1}")]
394 ConnectionError(String, String),
395
396 #[error("Initialization timeout for provider '{0}'")]
397 InitializationTimeout(String),
398
399 #[error("Initialization error for provider '{0}': {1}")]
400 InitializationError(String, String),
401
402 #[error("Provider not found: {0}")]
403 ProviderNotFound(String),
404
405 #[error("Tool execution error for provider '{0}': {1}")]
406 ToolExecutionError(String, String),
407
408 #[error("Semaphore error: {0}")]
409 SemaphoreError(String),
410}
411
412#[cfg(test)]
413pub(crate) mod tests {
414 use super::*;
415
416 #[tokio::test]
417 async fn test_connection_pool_creation() {
418 let pool = McpConnectionPool::new(5, 30);
419 let stats = pool.stats().await;
420
421 assert_eq!(stats.active_connections, 0);
422 assert_eq!(stats.max_connections, 5);
423 assert_eq!(stats.available_permits, 5);
424 }
425
426 #[tokio::test]
427 async fn test_connection_pool_semaphore_limits() {
428 let pool = McpConnectionPool::new(3, 30);
429
430 let permit1 = pool.connection_semaphore.acquire().await.unwrap();
432 let _permit2 = pool.connection_semaphore.acquire().await.unwrap();
433 let _permit3 = pool.connection_semaphore.acquire().await.unwrap();
434
435 let stats = pool.stats().await;
436 assert_eq!(stats.available_permits, 0);
437
438 drop(permit1);
440 let _permit4 = pool.connection_semaphore.acquire().await.unwrap();
441
442 let stats = pool.stats().await;
443 assert_eq!(stats.available_permits, 0);
444 }
445
446 #[tokio::test]
447 async fn test_pooled_manager_creation() {
448 let manager = PooledMcpManager::new(10, 30, 100);
449 let stats = manager.stats().await;
450
451 assert_eq!(stats.connection_pool.max_connections, 10);
452 assert_eq!(stats.connection_pool.active_connections, 0);
453 }
454
455 #[tokio::test]
456 async fn test_read_only_tool_detection() {
457 let manager = PooledMcpManager::new(5, 30, 50);
458
459 assert!(manager.is_read_only_tool("read_file"));
460 assert!(manager.is_read_only_tool("search_files"));
461 assert!(manager.is_read_only_tool("get_system_info"));
462 assert!(manager.is_read_only_tool("get_file_info"));
463
464 assert!(!manager.is_read_only_tool("write_file"));
465 assert!(!manager.is_read_only_tool("edit_file"));
466 assert!(!manager.is_read_only_tool("execute_command"));
467 assert!(!manager.is_read_only_tool("delete_file"));
468 }
469
470 #[test]
471 fn test_connection_pool_error_display() {
472 let error = McpPoolError::ConnectionTimeout("test_provider".to_string());
473 assert!(error.to_string().contains("test_provider"));
474
475 let error = McpPoolError::InitializationError("auth".to_string(), "invalid credentials".to_string());
476 assert!(error.to_string().contains("auth"));
477 assert!(error.to_string().contains("invalid credentials"));
478 }
479
480 #[tokio::test]
481 async fn test_pool_provider_not_found() {
482 let pool = McpConnectionPool::new(5, 30);
483 let provider = pool.get_provider("nonexistent").await;
484 assert!(provider.is_none());
485 }
486
487 #[tokio::test]
488 async fn test_pool_has_provider() {
489 let pool = McpConnectionPool::new(5, 30);
490 assert!(!pool.has_provider("test").await);
491 }
492
493 #[tokio::test]
494 async fn test_pool_get_all_providers_empty() {
495 let pool = McpConnectionPool::new(5, 30);
496 let providers = pool.get_all_providers().await;
497 assert_eq!(providers.len(), 0);
498 }
499
500 #[tokio::test]
501 async fn test_pool_stats() {
502 let pool = McpConnectionPool::new(7, 60);
503 let stats = pool.stats().await;
504
505 assert_eq!(stats.max_connections, 7);
506 assert_eq!(stats.available_permits, 7);
507 assert_eq!(stats.active_connections, 0);
508 }
509
510 #[test]
511 fn pool_initialize_params_gate_ui_extension_on_flag() {
512 let advertised = build_pool_initialize_params(true);
513 assert!(
514 advertised
515 .capabilities
516 .extensions
517 .as_ref()
518 .is_some_and(|extensions| extensions.contains_key("io.modelcontextprotocol/ui")),
519 );
520 let silent = build_pool_initialize_params(false);
521 assert_eq!(silent.capabilities.extensions, None);
522 }
523}