Skip to main content

a3s_code_core/mcp/
projection.rs

1//! Capability projection adapter for one exact MCP connection.
2
3use std::fmt;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use tokio_util::sync::CancellationToken;
8
9use super::manager::connect_ready_client;
10use super::{McpBinding, McpClient, McpServerConfig};
11use crate::capability::{
12    CapabilityAdapterError, CapabilityEffect, CapabilityEffectError, CapabilityProjectionAdapter,
13    CapabilityValue, PreparedCapability,
14};
15
16/// Fallible MCP preparation owned by one capability contribution.
17///
18/// A trusted host constructs the configuration from an already selected A3S
19/// Use surface and its exact Runtime/Gateway evidence. This adapter performs
20/// only standard MCP transport connection, initialize, and `tools/list`; it
21/// does not inspect package files, select a provider, resolve a Registry, or
22/// publish a Use generation.
23pub struct McpProjectionAdapter {
24    config: McpServerConfig,
25}
26
27impl McpProjectionAdapter {
28    pub fn new(config: McpServerConfig) -> Self {
29        Self { config }
30    }
31}
32
33impl fmt::Debug for McpProjectionAdapter {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        // Configuration can contain environment values, OAuth credentials,
36        // and authorization headers. Never include it in diagnostics.
37        formatter
38            .debug_struct("McpProjectionAdapter")
39            .field("server_name", &self.config.name)
40            .finish_non_exhaustive()
41    }
42}
43
44#[async_trait]
45impl CapabilityProjectionAdapter for McpProjectionAdapter {
46    async fn prepare(
47        self: Box<Self>,
48        cancellation: CancellationToken,
49    ) -> std::result::Result<PreparedCapability, CapabilityAdapterError> {
50        if cancellation.is_cancelled() {
51            return Err(CapabilityAdapterError::new(
52                "MCP projection preparation was cancelled",
53            ));
54        }
55
56        let (client, tools) = connect_ready_client(&self.config)
57            .await
58            .map_err(|error| CapabilityAdapterError::new(error.to_string()))?;
59        if cancellation.is_cancelled() {
60            close_after_failed_prepare(&self.config.name, &client).await;
61            return Err(CapabilityAdapterError::new(
62                "MCP projection preparation was cancelled",
63            ));
64        }
65
66        let binding = match McpBinding::new(&self.config.name, Arc::clone(&client), tools) {
67            Ok(binding) => Arc::new(binding),
68            Err(error) => {
69                close_after_failed_prepare(&self.config.name, &client).await;
70                return Err(CapabilityAdapterError::new(error.to_string()));
71            }
72        };
73        let mut prepared = PreparedCapability::new(CapabilityValue::Mcp(binding));
74        prepared.push_effect(McpConnectionEffect {
75            server_name: self.config.name.into_boxed_str(),
76            client,
77        })?;
78        Ok(prepared)
79    }
80}
81
82struct McpConnectionEffect {
83    server_name: Box<str>,
84    client: Arc<McpClient>,
85}
86
87#[async_trait]
88impl CapabilityEffect for McpConnectionEffect {
89    fn name(&self) -> &str {
90        "mcp.projected.connection"
91    }
92
93    async fn close(self: Box<Self>) -> std::result::Result<(), CapabilityEffectError> {
94        self.client.close().await.map_err(|error| {
95            CapabilityEffectError::new(format!(
96                "Failed to close projected MCP server '{}': {error}",
97                self.server_name
98            ))
99        })
100    }
101}
102
103async fn close_after_failed_prepare(server_name: &str, client: &McpClient) {
104    if let Err(error) = client.close().await {
105        tracing::warn!(
106            server = %server_name,
107            error = %error,
108            "Failed to close MCP connection after projection preparation failed"
109        );
110    }
111}