1use anyhow::{Context, Result, anyhow, bail};
2use async_trait::async_trait;
3use chrono::Utc;
4use parking_lot::RwLock;
5use rmcp::model::{CallToolResult, ClientCapabilities, InitializeRequestParams, RootsCapabilities};
6use rustc_hash::FxHashMap;
7use serde_json::{Map, Value, json};
8use std::collections::BTreeMap;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use std::time::Duration;
12use tracing::{info, warn};
13use vtcode_commons::fs::{ensure_dir_exists, write_file_with_context};
14use vtcode_config::mcp::{McpAllowListConfig, McpClientConfig, McpProviderConfig, McpTransportConfig, McpUiAppsTier};
15
16use super::McpSandboxContext;
17use super::{
18 McpClientStatus, McpElicitationHandler, McpPromptDetail, McpPromptInfo, McpProvider, McpResourceData,
19 McpResourceInfo, McpToolExecutor, McpToolInfo, apply_ui_extension_gate, format_tool_markdown,
20 reject_unsupported_ui_apps_tier, sanitize_filename,
21};
22use crate::connection_pool::McpConnectionPool;
23
24mod status;
25
26pub(super) struct McpClientState {
27 providers: FxHashMap<String, Arc<McpProvider>>,
28 allowlist: McpAllowListConfig,
29 tool_provider_index: FxHashMap<String, String>,
30 resource_provider_index: FxHashMap<String, String>,
31 prompt_provider_index: FxHashMap<String, String>,
32}
33
34fn providers_sorted_by_name<V: Clone>(providers: &FxHashMap<String, V>) -> Vec<V> {
36 let mut entries: Vec<(&String, &V)> = providers.iter().collect();
37 entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
38 entries.into_iter().map(|(_, provider)| provider.clone()).collect()
39}
40
41pub struct McpClient {
42 pub(super) config: McpClientConfig,
43 pub(super) state: RwLock<McpClientState>,
44 elicitation_handler: Option<Arc<dyn McpElicitationHandler>>,
45 sandbox_context: Option<McpSandboxContext>,
46}
47
48impl McpClient {
49 pub fn new(config: McpClientConfig) -> Self {
51 Self::with_sandbox_context(config, None)
52 }
53
54 pub fn with_sandbox_context(config: McpClientConfig, sandbox_context: Option<McpSandboxContext>) -> Self {
56 let allowlist = config.allowlist.clone();
57
58 Self {
59 config,
60 state: RwLock::new(McpClientState {
61 providers: FxHashMap::default(),
62 allowlist,
63 tool_provider_index: FxHashMap::default(),
64 resource_provider_index: FxHashMap::default(),
65 prompt_provider_index: FxHashMap::default(),
66 }),
67 elicitation_handler: None,
68 sandbox_context,
69 }
70 }
71
72 pub fn set_elicitation_handler(&mut self, handler: Arc<dyn McpElicitationHandler>) {
74 self.elicitation_handler = Some(handler);
75 }
76
77 #[must_use]
83 pub fn ui_apps_tier(&self) -> McpUiAppsTier {
84 self.config.ui.apps
85 }
86
87 pub async fn initialize(&mut self) -> Result<()> {
90 if !self.config.enabled {
91 info!("MCP client is disabled in configuration");
92 return Ok(());
93 }
94 reject_unsupported_ui_apps_tier(&self.config.ui)?;
95
96 info!("Initializing MCP client with {} configured providers", self.config.providers.len());
97
98 let max_connections = self.config.max_concurrent_connections.max(1);
99 let pool = McpConnectionPool::new(max_connections, self.config.request_timeout_seconds);
100 let tool_timeout = Some(Duration::from_secs(self.config.request_timeout_seconds));
101 let allowlist_snapshot = self.state.read().allowlist.clone();
102
103 let provider_configs: Vec<McpProviderConfig> =
104 self.config.providers.iter().filter(|c| c.enabled).cloned().collect();
105
106 let results = pool
107 .initialize_providers_parallel(
108 provider_configs,
109 self.elicitation_handler.clone(),
110 self.sandbox_context.clone(),
111 tool_timeout,
112 &allowlist_snapshot,
113 self.config.ui.apps.advertises_ui_extension(),
114 )
115 .await
116 .map_err(|e| anyhow::anyhow!("MCP connection pool initialization failed: {e}"))?;
117
118 let mut initialized = FxHashMap::default();
119 for (name, provider) in results {
120 drop(initialized.insert(name, provider));
121 }
122
123 self.state.write().providers = initialized;
124 info!("MCP client initialization complete. Active providers: {}", self.state.read().providers.len());
125
126 Ok(())
127 }
128
129 fn validate_tool_arguments(&self, _tool_name: &str, args: &Value) -> Result<()> {
131 if self.config.security.validation.max_argument_size > 0 {
133 let args_size = serde_json::to_string(args).map_or(0, |s| u32::try_from(s.len()).unwrap_or(u32::MAX));
134
135 if args_size > self.config.security.validation.max_argument_size {
136 return Err(anyhow::anyhow!(
137 "Tool arguments exceed maximum size of {} bytes",
138 self.config.security.validation.max_argument_size
139 ));
140 }
141 }
142
143 if self.config.security.validation.path_traversal_protection
145 && let Some(path) = args.get("path").and_then(|v| v.as_str())
146 && (path.contains("../") || path.starts_with("../") || path.contains("..\\") || path.starts_with("..\\"))
147 {
148 return Err(anyhow::anyhow!("Path traversal detected in arguments"));
149 }
150
151 Ok(())
152 }
153
154 async fn execute_tool_with_validation(&self, tool_name: &str, args: Value) -> Result<Value> {
160 self.execute_tool_with_validation_ref(tool_name, &args).await
161 }
162
163 async fn execute_tool_with_validation_ref(&self, tool_name: &str, args: &Value) -> Result<Value> {
165 if !self.config.enabled {
166 return Err(anyhow!("MCP support is disabled in the current configuration"));
167 }
168
169 self.validate_tool_arguments(tool_name, args)?;
170
171 let provider = self.resolve_provider_for_tool(tool_name).await?;
172 let allowlist_snapshot = self.state.read().allowlist.clone();
173 let tools = provider
177 .cached_tools_or_refresh_shared(&allowlist_snapshot, self.tool_timeout())
178 .await?;
179 if let Some(tool) = tools.iter().find(|tool| tool.name == tool_name)
180 && !tool.visibility.model
181 {
182 bail!("MCP tool '{tool_name}' is not available to the model (app-only visibility)");
183 }
184 let result = provider
185 .call_tool(tool_name, args, self.tool_timeout(), &allowlist_snapshot)
186 .await?;
187
188 Self::format_tool_result(&provider.name, tool_name, result)
189 }
190
191 pub fn update_allowlist(&self, allowlist: McpAllowListConfig) {
193 let providers: Vec<Arc<McpProvider>> = {
194 let mut state = self.state.write();
195 state.allowlist = allowlist;
196 state.tool_provider_index.clear();
197 state.resource_provider_index.clear();
198 state.prompt_provider_index.clear();
199 state.providers.values().cloned().collect()
200 };
201
202 for provider in providers {
203 provider.invalidate_caches();
204 }
205 }
206
207 pub fn current_allowlist(&self) -> McpAllowListConfig {
209 self.state.read().allowlist.clone()
210 }
211
212 pub fn provider_for_tool(&self, tool_name: &str) -> Option<String> {
214 self.state.read().tool_provider_index.get(tool_name).cloned()
215 }
216
217 pub fn provider_for_resource(&self, uri: &str) -> Option<String> {
219 self.state.read().resource_provider_index.get(uri).cloned()
220 }
221
222 pub fn provider_for_prompt(&self, prompt_name: &str) -> Option<String> {
224 self.state.read().prompt_provider_index.get(prompt_name).cloned()
225 }
226
227 pub async fn execute_tool(&self, tool_name: &str, args: Value) -> Result<Value> {
229 self.execute_tool_with_validation(tool_name, args).await
230 }
231
232 pub async fn list_tools(&self) -> Result<Vec<McpToolInfo>> {
237 Ok(self
238 .collect_tools(false)
239 .await?
240 .into_iter()
241 .filter(|tool| tool.visibility.model)
242 .collect())
243 }
244
245 pub async fn list_resources(&self) -> Result<Vec<McpResourceInfo>> {
247 self.collect_resources(false).await
248 }
249
250 pub async fn refresh_resources(&self) -> Result<Vec<McpResourceInfo>> {
252 self.collect_resources(true).await
253 }
254
255 pub async fn list_prompts(&self) -> Result<Vec<McpPromptInfo>> {
257 self.collect_prompts(false).await
258 }
259
260 pub async fn refresh_prompts(&self) -> Result<Vec<McpPromptInfo>> {
262 self.collect_prompts(true).await
263 }
264
265 pub async fn read_resource(&self, uri: &str) -> Result<McpResourceData> {
267 let provider = self.resolve_provider_for_resource(uri).await?;
268 let provider_name = provider.name.clone();
269 let allowlist_snapshot = self.state.read().allowlist.clone();
270 let data = provider.read_resource(uri, self.request_timeout(), &allowlist_snapshot).await?;
271 drop(self.state.write().resource_provider_index.insert(uri.into(), provider_name));
272 Ok(data)
273 }
274
275 pub async fn get_prompt(
277 &self,
278 prompt_name: &str,
279 arguments: Option<hashbrown::HashMap<String, String>>,
280 ) -> Result<McpPromptDetail> {
281 let provider = self.resolve_provider_for_prompt(prompt_name).await?;
282 let provider_name = provider.name.clone();
283 let allowlist_snapshot = self.state.read().allowlist.clone();
284 let prompt = provider
285 .get_prompt(prompt_name, arguments.unwrap_or_default(), self.request_timeout(), &allowlist_snapshot)
286 .await?;
287 drop(
288 self.state
289 .write()
290 .prompt_provider_index
291 .insert(prompt_name.into(), provider_name),
292 );
293 Ok(prompt)
294 }
295
296 pub async fn shutdown(&self) -> Result<()> {
298 let providers: Vec<Arc<McpProvider>> = {
299 let mut state = self.state.write();
300 let values: Vec<_> = state.providers.values().cloned().collect();
301 state.providers.clear();
302 state.tool_provider_index.clear();
303 state.resource_provider_index.clear();
304 state.prompt_provider_index.clear();
305 values
306 };
307
308 if providers.is_empty() {
309 info!("No active MCP connections to shutdown");
310 return Ok(());
311 }
312
313 info!("Shutting down {} MCP providers", providers.len());
314 for provider in providers {
315 if let Err(err) = provider.shutdown().await {
316 warn!("Provider '{}' shutdown returned error: {err}", provider.name);
317 }
318 }
319 Ok(())
320 }
321
322 async fn collect_tools(&self, force_refresh: bool) -> Result<Vec<McpToolInfo>> {
323 let (providers, allowlist) = {
325 let state = self.state.read();
326 (providers_sorted_by_name(&state.providers), state.allowlist.clone())
327 };
328
329 if providers.is_empty() {
330 return Ok(Vec::new());
331 }
332
333 let timeout = self.tool_timeout();
334 let mut all_tools = Vec::with_capacity(128);
335 let mut index_updates: FxHashMap<String, String> = FxHashMap::with_capacity_and_hasher(128, Default::default());
336
337 for provider in providers {
338 let provider_name = provider.name.clone();
339 let tools = if force_refresh {
340 provider.refresh_tools(&allowlist, timeout).await
341 } else {
342 provider.list_tools(&allowlist, timeout).await
343 };
344
345 match tools {
346 Ok(tools) => {
347 for tool in &tools {
348 let _ = index_updates.entry(tool.name.clone()).or_insert_with(|| provider_name.clone());
349 }
350 all_tools.extend(tools);
351 }
352 Err(err) => {
353 warn!("Failed to list tools for provider '{}': {err}", provider_name);
354 }
355 }
356 }
357
358 if !index_updates.is_empty() || force_refresh {
359 let mut state = self.state.write();
360 if index_updates.is_empty() {
361 state.tool_provider_index.clear();
362 } else {
363 state.tool_provider_index = index_updates;
364 }
365 }
366
367 Ok(all_tools)
368 }
369
370 async fn collect_resources(&self, force_refresh: bool) -> Result<Vec<McpResourceInfo>> {
371 let (providers, allowlist) = {
373 let state = self.state.read();
374 (providers_sorted_by_name(&state.providers), state.allowlist.clone())
375 };
376
377 if providers.is_empty() {
378 self.state.write().resource_provider_index.clear();
379 return Ok(Vec::new());
380 }
381
382 let timeout = self.request_timeout();
383 let mut all_resources = Vec::with_capacity(64);
384
385 for provider in providers {
386 let resources = if force_refresh {
387 provider.refresh_resources(&allowlist, timeout).await
388 } else {
389 provider.list_resources(&allowlist, timeout).await
390 };
391
392 match resources {
393 Ok(resources) => {
394 all_resources.extend(resources);
395 }
396 Err(err) => {
397 warn!("Failed to list resources for provider '{}': {err}", provider.name);
398 }
399 }
400 }
401
402 let mut state = self.state.write();
403 let index = &mut state.resource_provider_index;
404 index.clear();
405 for resource in &all_resources {
406 let _ = index.entry(resource.uri.clone()).or_insert_with(|| resource.provider.clone());
407 }
408
409 Ok(all_resources)
410 }
411
412 async fn collect_prompts(&self, force_refresh: bool) -> Result<Vec<McpPromptInfo>> {
413 let (providers, allowlist) = {
415 let state = self.state.read();
416 (providers_sorted_by_name(&state.providers), state.allowlist.clone())
417 };
418
419 if providers.is_empty() {
420 self.state.write().prompt_provider_index.clear();
421 return Ok(Vec::new());
422 }
423
424 let timeout = self.request_timeout();
425 let mut all_prompts = Vec::with_capacity(32);
426
427 for provider in providers {
428 let prompts = if force_refresh {
429 provider.refresh_prompts(&allowlist, timeout).await
430 } else {
431 provider.list_prompts(&allowlist, timeout).await
432 };
433
434 match prompts {
435 Ok(prompts) => {
436 all_prompts.extend(prompts);
437 }
438 Err(err) => {
439 warn!("Failed to list prompts for provider '{}': {err}", provider.name);
440 }
441 }
442 }
443
444 let mut state = self.state.write();
445 let index = &mut state.prompt_provider_index;
446 index.clear();
447 for prompt in &all_prompts {
448 let _ = index.entry(prompt.name.clone()).or_insert_with(|| prompt.provider.clone());
449 }
450
451 Ok(all_prompts)
452 }
453
454 async fn resolve_provider_for_tool(&self, tool_name: &str) -> Result<Arc<McpProvider>> {
455 if !self.config.enabled {
456 return Err(anyhow!("MCP support is disabled in the current configuration"));
457 }
458
459 if let Some(provider) = self.provider_for_tool(tool_name)
460 && let Some(found) = self.state.read().providers.get(&provider)
461 {
462 return Ok(found.clone());
463 }
464
465 let (allowlist, providers) = {
466 let state = self.state.read();
467 (state.allowlist.clone(), providers_sorted_by_name(&state.providers))
468 };
469 let timeout = self.tool_timeout();
470
471 if providers.is_empty() {
472 if self.config.providers.is_empty() {
473 return Err(anyhow!(
474 "No MCP providers are configured. Use `vtcode mcp add` or update vtcode.toml to register one."
475 ));
476 }
477
478 return Err(anyhow!(
479 "No MCP providers are currently connected. Ensure MCP initialization completed successfully."
480 ));
481 }
482
483 for provider in providers {
484 match provider.has_tool(tool_name, &allowlist, timeout).await {
485 Ok(true) => {
486 drop(
487 self.state
488 .write()
489 .tool_provider_index
490 .insert(tool_name.into(), provider.name.clone()),
491 );
492 return Ok(provider);
493 }
494 Ok(false) => continue,
495 Err(err) => {
496 warn!("Error checking tool '{}' on provider '{}': {err}", tool_name, provider.name);
497 }
498 }
499 }
500
501 match self.collect_tools(true).await {
502 Ok(_) => {
503 if let Some(provider) = self.provider_for_tool(tool_name)
504 && let Some(found) = self.state.read().providers.get(&provider)
505 {
506 return Ok(found.clone());
507 }
508 }
509 Err(err) => {
510 warn!("Failed to refresh MCP tool caches while resolving '{}': {err}", tool_name);
511 }
512 }
513
514 Err(anyhow!(
515 "Tool '{tool_name}' not found on any MCP provider.\n\n\
516 To use this tool:\n\
517 1. Install the MCP server: `uv tool install mcp-server-{tool_name}`\n\
518 2. Add to vtcode.toml:\n \
519 [[mcp.providers]]\n \
520 name = \"{tool_name}\"\n \
521 command = \"uvx\"\n \
522 args = [\"mcp-server-{tool_name}\"]\n\
523 3. Restart VT Code\n\n\
524 Or use the built-in alternative if available (e.g., web_fetch instead of mcp_fetch)"
525 ))
526 }
527
528 async fn resolve_provider_for_resource(&self, uri: &str) -> Result<Arc<McpProvider>> {
529 if let Some(provider) = self.provider_for_resource(uri)
530 && let Some(found) = self.state.read().providers.get(&provider)
531 {
532 return Ok(found.clone());
533 }
534
535 let (allowlist, providers) = {
536 let state = self.state.read();
537 (state.allowlist.clone(), providers_sorted_by_name(&state.providers))
538 };
539 let timeout = self.request_timeout();
540
541 for provider in providers {
542 match provider.has_resource(uri, &allowlist, timeout).await {
543 Ok(true) => {
544 drop(
545 self.state
546 .write()
547 .resource_provider_index
548 .insert(uri.into(), provider.name.clone()),
549 );
550 return Ok(provider);
551 }
552 Ok(false) => continue,
553 Err(err) => {
554 warn!("Error checking resource '{}' on provider '{}': {err}", uri, provider.name);
555 }
556 }
557 }
558
559 Err(anyhow!("Resource '{uri}' not found on any MCP provider"))
560 }
561
562 async fn resolve_provider_for_prompt(&self, prompt_name: &str) -> Result<Arc<McpProvider>> {
563 if let Some(provider) = self.provider_for_prompt(prompt_name)
564 && let Some(found) = self.state.read().providers.get(&provider)
565 {
566 return Ok(found.clone());
567 }
568
569 let (allowlist, providers) = {
570 let state = self.state.read();
571 (state.allowlist.clone(), providers_sorted_by_name(&state.providers))
572 };
573 let timeout = self.request_timeout();
574
575 for provider in providers {
576 match provider.has_prompt(prompt_name, &allowlist, timeout).await {
577 Ok(true) => {
578 drop(
579 self.state
580 .write()
581 .prompt_provider_index
582 .insert(prompt_name.into(), provider.name.clone()),
583 );
584 return Ok(provider);
585 }
586 Ok(false) => continue,
587 Err(err) => {
588 warn!("Error checking prompt '{}' on provider '{}': {err}", prompt_name, provider.name);
589 }
590 }
591 }
592
593 Err(anyhow!("Prompt '{prompt_name}' not found on any MCP provider"))
594 }
595
596 pub(super) fn record_tool_provider(&self, provider: &str, tools: &[McpToolInfo]) {
597 let mut state = self.state.write();
598 let index = &mut state.tool_provider_index;
599 for tool in tools {
600 let _ = index
601 .entry(tool.name.clone())
602 .and_modify(|owner| {
603 if provider < owner.as_str() {
604 *owner = provider.to_string();
605 }
606 })
607 .or_insert_with(|| provider.to_string());
608 }
609 }
610
611 async fn connect_and_initialize_provider(
612 &self,
613 provider_config: &McpProviderConfig,
614 allowlist_snapshot: &McpAllowListConfig,
615 tool_timeout: Option<Duration>,
616 ) -> Result<McpProvider> {
617 let total_attempts = self.provider_retry_attempts();
618 let mut last_error: Option<anyhow::Error> = None;
619
620 for attempt_idx in 0..total_attempts {
621 let attempt_number = attempt_idx + 1;
622 match self
623 .connect_and_initialize_provider_once(provider_config, allowlist_snapshot, tool_timeout)
624 .await
625 {
626 Ok(provider) => return Ok(provider),
627 Err(err) => {
628 if attempt_number == total_attempts {
629 return Err(err);
630 }
631
632 let retries_remaining = total_attempts - attempt_number;
633 warn!(
634 provider = provider_config.name.as_str(),
635 attempt = attempt_number,
636 retries_remaining,
637 error = %err,
638 "MCP provider initialization failed; retrying"
639 );
640 last_error = Some(err);
641 tokio::time::sleep(Self::provider_retry_delay(attempt_idx)).await;
642 }
643 }
644 }
645
646 Err(last_error.unwrap_or_else(|| anyhow!("Failed to initialize MCP provider '{}'", provider_config.name)))
647 }
648
649 async fn connect_and_initialize_provider_once(
650 &self,
651 provider_config: &McpProviderConfig,
652 allowlist_snapshot: &McpAllowListConfig,
653 tool_timeout: Option<Duration>,
654 ) -> Result<McpProvider> {
655 let provider = McpProvider::connect(
656 provider_config.clone(),
657 self.elicitation_handler.clone(),
658 self.sandbox_context.clone(),
659 self.config.ui.apps.advertises_ui_extension(),
660 )
661 .await
662 .with_context(|| format!("Failed to connect to MCP provider '{}'", provider_config.name))?;
663 let provider_startup_timeout = self.resolve_startup_timeout(provider_config);
664 provider
665 .initialize(self.build_initialize_params(), provider_startup_timeout, tool_timeout, allowlist_snapshot)
666 .await
667 .with_context(|| format!("Failed to initialize MCP provider '{}'", provider_config.name))?;
668 Ok(provider)
669 }
670
671 fn startup_timeout(&self) -> Option<Duration> {
672 match self.config.startup_timeout_seconds {
673 Some(0) => None,
674 Some(value) => Some(Duration::from_secs(value)),
675 None => self.request_timeout(),
676 }
677 }
678
679 pub(super) fn requirement_mismatch_reason(&self, provider_config: &McpProviderConfig) -> Option<String> {
680 let requirements = &self.config.requirements;
681 if !requirements.enforce {
682 return None;
683 }
684
685 match &provider_config.transport {
686 McpTransportConfig::Stdio(stdio) => {
687 if requirements
688 .allowed_stdio_commands
689 .iter()
690 .any(|allowed| allowed == &stdio.command)
691 {
692 None
693 } else {
694 Some(format!("stdio command '{}' is not allowlisted", stdio.command))
695 }
696 }
697 McpTransportConfig::Http(http) => {
698 if requirements
699 .allowed_http_endpoints
700 .iter()
701 .any(|allowed| allowed == &http.endpoint)
702 {
703 None
704 } else {
705 Some(format!("HTTP endpoint '{}' is not allowlisted", http.endpoint))
706 }
707 }
708 }
709 }
710
711 fn resolve_startup_timeout(&self, provider_config: &McpProviderConfig) -> Option<Duration> {
712 if let Some(timeout_ms) = provider_config.startup_timeout_ms {
713 if timeout_ms == 0 {
714 None
715 } else {
716 Some(Duration::from_millis(timeout_ms))
717 }
718 } else {
719 self.startup_timeout()
720 }
721 }
722
723 fn provider_retry_attempts(&self) -> usize {
724 self.config.retry_attempts.try_into().unwrap_or(usize::MAX).saturating_add(1)
725 }
726
727 fn provider_retry_delay(attempt_idx: usize) -> Duration {
728 let base_ms = 250u64;
729 let max_ms = 5000u64;
730 let exp = base_ms.saturating_mul(2u64.saturating_pow(u32::try_from(attempt_idx).unwrap_or(u32::MAX)));
731 let delay = exp.min(max_ms);
732 Duration::from_millis(delay)
733 }
734
735 pub(super) fn tool_timeout(&self) -> Option<Duration> {
736 match self.config.tool_timeout_seconds {
737 Some(0) => None,
738 Some(value) => Some(Duration::from_secs(value)),
739 None => self.request_timeout(),
740 }
741 }
742
743 fn request_timeout(&self) -> Option<Duration> {
744 if self.config.request_timeout_seconds == 0 {
745 None
746 } else {
747 Some(Duration::from_secs(self.config.request_timeout_seconds))
748 }
749 }
750
751 fn build_initialize_params(&self) -> InitializeRequestParams {
752 let mut capabilities = ClientCapabilities::default();
753 {
754 let mut roots_cap = RootsCapabilities::default();
755 roots_cap.list_changed = Some(true);
756 capabilities.roots = Some(roots_cap);
757 }
758
759 if self.elicitation_handler.is_some() {
760 capabilities.elicitation = Some(
762 rmcp::model::ElicitationCapability::new()
763 .with_form(rmcp::model::FormElicitationCapability::new().with_schema_validation(true)),
764 );
765 }
766
767 apply_ui_extension_gate(&mut capabilities, self.config.ui.apps.advertises_ui_extension());
768
769 InitializeRequestParams::new(capabilities, super::utils::build_client_implementation())
770 .with_protocol_version(super::rmcp_client::latest_protocol_version())
771 }
772
773 pub(super) fn normalize_arguments(args: &Value) -> Map<String, Value> {
774 match args {
775 Value::Null => Map::new(),
776 Value::Object(map) => map.clone(),
777 other => {
778 let mut map = Map::new();
779 drop(map.insert("value".to_owned(), other.clone()));
780 map
781 }
782 }
783 }
784
785 fn format_tool_result(provider_name: &str, tool_name: &str, result: CallToolResult) -> Result<Value> {
786 if result.is_error == Some(true) {
787 let message = result
788 .content
789 .iter()
790 .filter_map(|block| block.as_text())
791 .map(|block| block.text.as_str())
792 .find(|text| !text.trim().is_empty())
793 .unwrap_or("Unknown MCP tool error");
794 return Err(anyhow!("MCP tool '{tool_name}' on provider '{provider_name}' reported an error: {message}"));
795 }
796
797 let mut payload = Map::new();
798 drop(payload.insert("provider".into(), Value::String(provider_name.to_string())));
799 drop(payload.insert("tool".into(), Value::String(tool_name.to_string())));
800
801 if !result.content.is_empty() {
802 let mut content = serde_json::to_value(result.content)?;
803 if let Some(blocks) = content.as_array_mut() {
804 for block in blocks {
805 if let Some(block) = block.as_object_mut() {
806 drop(block.remove("_meta"));
808 if block.get("type").and_then(Value::as_str) == Some("resource")
809 && let Some(resource) = block.get_mut("resource").and_then(Value::as_object_mut)
810 {
811 drop(resource.remove("_meta"));
812 }
813 }
814 }
815 }
816 drop(payload.insert("content".into(), content));
817 }
818 if let Some(structured_content) = result.structured_content {
819 drop(payload.insert("structuredContent".into(), structured_content));
820 }
821
822 Ok(Value::Object(payload))
823 }
824}
825
826#[async_trait]
827impl McpToolExecutor for McpClient {
828 async fn execute_mcp_tool(&self, tool_name: &str, args: &Value) -> Result<Value> {
829 self.execute_tool_with_validation_ref(tool_name, args).await
830 }
831
832 async fn list_mcp_tools(&self) -> Result<Vec<McpToolInfo>> {
833 self.list_tools().await
834 }
835
836 async fn has_mcp_tool(&self, tool_name: &str) -> Result<bool> {
837 if !self.config.enabled {
838 return Ok(false);
839 }
840
841 if self.provider_for_tool(tool_name).is_some() {
842 return Ok(true);
843 }
844
845 if self.state.read().providers.is_empty() {
846 if self.config.providers.is_empty() {
847 return Ok(false);
848 }
849
850 bail!("No MCP providers are currently connected. Ensure MCP initialization completed successfully.");
851 }
852
853 let tools = self.collect_tools(false).await?;
854 Ok(tools.iter().any(|tool| tool.name == tool_name))
855 }
856
857 fn get_status(&self) -> McpClientStatus {
858 self.get_status()
859 }
860}
861
862#[cfg(test)]
863mod tests;