Skip to main content

adk_tool/mcp/
http.rs

1// MCP HTTP Transport (Streamable HTTP)
2//
3// Provides HTTP-based transport for connecting to remote MCP servers.
4// Uses the streamable HTTP transport from rmcp when the http-transport feature is enabled.
5
6use super::auth::McpAuth;
7use adk_core::{AdkError, Result};
8use std::collections::HashMap;
9use std::time::Duration;
10
11/// Builder for HTTP-based MCP connections.
12///
13/// This builder creates connections to remote MCP servers using the
14/// streamable HTTP transport (SEP-1686 compliant).
15///
16/// # Example
17///
18/// ```rust,ignore
19/// use adk_tool::mcp::{McpHttpClientBuilder, McpAuth, OAuth2Config};
20///
21/// // Simple connection
22/// let toolset = McpHttpClientBuilder::new("https://mcp.example.com/v1")
23///     .connect()
24///     .await?;
25///
26/// // With OAuth2 authentication
27/// let toolset = McpHttpClientBuilder::new("https://mcp.example.com/v1")
28///     .with_auth(McpAuth::oauth2(
29///         OAuth2Config::new("client-id", "https://auth.example.com/token")
30///             .with_secret("client-secret")
31///             .with_scopes(vec!["mcp:read".into()])
32///     ))
33///     .timeout(Duration::from_secs(60))
34///     .connect()
35///     .await?;
36/// ```
37#[derive(Clone)]
38pub struct McpHttpClientBuilder {
39    /// MCP server endpoint URL
40    endpoint: String,
41    /// Authentication configuration
42    auth: McpAuth,
43    /// Request timeout
44    timeout: Duration,
45    /// Custom headers
46    headers: HashMap<String, String>,
47}
48
49impl McpHttpClientBuilder {
50    /// Create a new HTTP client builder for the given endpoint.
51    ///
52    /// # Arguments
53    ///
54    /// * `endpoint` - The MCP server URL (e.g., "https://mcp.example.com/v1")
55    pub fn new(endpoint: impl Into<String>) -> Self {
56        Self {
57            endpoint: endpoint.into(),
58            auth: McpAuth::None,
59            timeout: Duration::from_secs(30),
60            headers: HashMap::new(),
61        }
62    }
63
64    /// Set authentication for the connection.
65    ///
66    /// # Example
67    ///
68    /// ```rust,ignore
69    /// let builder = McpHttpClientBuilder::new("https://mcp.example.com")
70    ///     .with_auth(McpAuth::bearer("my-token"));
71    /// ```
72    pub fn with_auth(mut self, auth: McpAuth) -> Self {
73        self.auth = auth;
74        self
75    }
76
77    /// Set the request timeout.
78    ///
79    /// Default is 30 seconds.
80    pub fn timeout(mut self, timeout: Duration) -> Self {
81        self.timeout = timeout;
82        self
83    }
84
85    /// Add a custom header to all requests.
86    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
87        self.headers.insert(key.into(), value.into());
88        self
89    }
90
91    /// Get the endpoint URL.
92    pub fn endpoint(&self) -> &str {
93        &self.endpoint
94    }
95
96    /// Get the configured timeout.
97    pub fn get_timeout(&self) -> Duration {
98        self.timeout
99    }
100
101    /// Get the authentication configuration.
102    pub fn get_auth(&self) -> &McpAuth {
103        &self.auth
104    }
105
106    /// Connect to the MCP server and create a toolset.
107    ///
108    /// This method establishes a connection to the remote MCP server
109    /// using the streamable HTTP transport.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if:
114    /// - The `http-transport` feature is not enabled
115    /// - Connection to the server fails
116    /// - Authentication fails
117    #[cfg(feature = "http-transport")]
118    pub async fn connect(
119        self,
120    ) -> Result<super::McpToolset<impl rmcp::service::Service<rmcp::RoleClient>>> {
121        use rmcp::ServiceExt;
122        use rmcp::transport::streamable_http_client::{
123            StreamableHttpClientTransport, StreamableHttpClientTransportConfig,
124        };
125
126        // Extract the raw token from auth config
127        // rmcp's bearer_auth() adds "Bearer " prefix automatically
128        let token = match &self.auth {
129            McpAuth::Bearer(token) => Some(token.clone()),
130            McpAuth::OAuth2(config) => {
131                // Get token from OAuth2 flow
132                let token = config
133                    .get_or_refresh_token()
134                    .await
135                    .map_err(|e| AdkError::Tool(format!("OAuth2 authentication failed: {}", e)))?;
136                Some(token)
137            }
138            McpAuth::ApiKey { .. } => {
139                // API key auth not supported via rmcp's auth_header (uses different header)
140                // Would need custom client implementation
141                None
142            }
143            McpAuth::None => None,
144        };
145
146        // Build transport config with authentication
147        let mut config = StreamableHttpClientTransportConfig::with_uri(self.endpoint.as_str());
148
149        // Set auth header if we have a token (rmcp adds "Bearer " prefix via bearer_auth)
150        if let Some(token) = token {
151            config = config.auth_header(token);
152        }
153
154        // Create transport with config
155        let transport = StreamableHttpClientTransport::from_config(config);
156
157        // Connect using the service extension
158        let client = ()
159            .serve(transport)
160            .await
161            .map_err(|e| AdkError::Tool(format!("Failed to connect to MCP server: {}", e)))?;
162
163        Ok(super::McpToolset::new(client))
164    }
165
166    /// Connect to the MCP server (stub when http-transport feature is disabled).
167    #[cfg(not(feature = "http-transport"))]
168    pub async fn connect(self) -> Result<()> {
169        Err(AdkError::Tool(
170            "HTTP transport requires the 'http-transport' feature. \
171             Add `adk-tool = { features = [\"http-transport\"] }` to your Cargo.toml"
172                .to_string(),
173        ))
174    }
175}
176
177impl std::fmt::Debug for McpHttpClientBuilder {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.debug_struct("McpHttpClientBuilder")
180            .field("endpoint", &self.endpoint)
181            .field("auth", &self.auth)
182            .field("timeout", &self.timeout)
183            .field("headers", &self.headers.keys().collect::<Vec<_>>())
184            .finish()
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_builder_new() {
194        let builder = McpHttpClientBuilder::new("https://mcp.example.com");
195        assert_eq!(builder.endpoint(), "https://mcp.example.com");
196        assert_eq!(builder.get_timeout(), Duration::from_secs(30));
197    }
198
199    #[test]
200    fn test_builder_with_auth() {
201        let builder = McpHttpClientBuilder::new("https://mcp.example.com")
202            .with_auth(McpAuth::bearer("test-token"));
203        assert!(builder.get_auth().is_configured());
204    }
205
206    #[test]
207    fn test_builder_timeout() {
208        let builder =
209            McpHttpClientBuilder::new("https://mcp.example.com").timeout(Duration::from_secs(60));
210        assert_eq!(builder.get_timeout(), Duration::from_secs(60));
211    }
212
213    #[test]
214    fn test_builder_headers() {
215        let builder =
216            McpHttpClientBuilder::new("https://mcp.example.com").header("X-Custom", "value");
217        assert!(builder.headers.contains_key("X-Custom"));
218    }
219}