Skip to main content

mcp_streamable_proxy/
client.rs

1//! Streamable HTTP Client Connection Module
2//!
3//! Provides a high-level API for connecting to MCP servers via Streamable HTTP protocol.
4//! This module encapsulates the rmcp transport details and exposes a simple interface.
5
6use anyhow::{Context, Result};
7use mcp_common::McpClientConfig;
8use rmcp::{
9    ServiceExt,
10    transport::{
11        common::client_side_sse::SseRetryPolicy,
12        streamable_http_client::{
13            StreamableHttpClientTransport, StreamableHttpClientTransportConfig,
14        },
15    },
16};
17use std::sync::Arc;
18use std::time::Duration;
19
20use crate::backend_client::{BackendNotificationBridge, UpstreamPeerRegistry};
21use crate::fallback::DiscoverySnapshot;
22use crate::proxy_handler::{BackendRunningService, ProxyHandler};
23use mcp_common::ToolFilter;
24
25/// 自定义的指数退避重试策略,支持最大间隔限制
26///
27/// 重试间隔按照指数增长,但不会超过 max_interval
28/// - 第 1 次重试:base_duration × 2^0
29/// - 第 2 次重试:base_duration × 2^1
30/// - ...
31/// - 第 n 次重试:min(base_duration × 2^(n-1), max_interval)
32#[derive(Debug, Clone)]
33pub struct CappedExponentialBackoff {
34    /// 最大重试次数,None 表示无限制
35    pub max_times: Option<usize>,
36    /// 基础延迟时间(第一次重试前的等待时间)
37    pub base_duration: Duration,
38    /// 最大延迟间隔(重试间隔不会超过这个值)
39    pub max_interval: Duration,
40}
41
42impl CappedExponentialBackoff {
43    /// 创建一个新的带上限的指数退避策略
44    ///
45    /// # Arguments
46    /// * `max_times` - 最大重试次数,None 表示无限制
47    /// * `base_duration` - 基础延迟时间
48    /// * `max_interval` - 最大延迟间隔
49    pub fn new(max_times: Option<usize>, base_duration: Duration, max_interval: Duration) -> Self {
50        Self {
51            max_times,
52            base_duration,
53            max_interval,
54        }
55    }
56}
57
58impl Default for CappedExponentialBackoff {
59    fn default() -> Self {
60        Self {
61            max_times: None,
62            base_duration: Duration::from_secs(1),
63            max_interval: Duration::from_secs(60),
64        }
65    }
66}
67
68impl SseRetryPolicy for CappedExponentialBackoff {
69    fn retry(&self, current_times: usize) -> Option<Duration> {
70        // 检查是否超过最大重试次数
71        if let Some(max_times) = self.max_times
72            && current_times >= max_times
73        {
74            return None;
75        }
76
77        Some(mcp_common::capped_exponential_delay(
78            self.base_duration,
79            self.max_interval,
80            current_times,
81        ))
82    }
83}
84
85/// Opaque wrapper for Streamable HTTP client connection
86///
87/// This type encapsulates an active connection to an MCP server via Streamable HTTP protocol.
88/// It hides the internal `RunningService` type and provides only the methods
89/// needed by consuming code.
90///
91/// Note: This type is not Clone because the underlying RunningService
92/// is designed for single-owner use. Use `into_handler()` or `into_running_service()`
93/// to consume the connection.
94///
95/// # Example
96///
97/// ```rust,ignore
98/// use mcp_streamable_proxy::{StreamClientConnection, McpClientConfig};
99///
100/// let config = McpClientConfig::new("http://localhost:8080/mcp")
101///     .with_header("Authorization", "Bearer token");
102///
103/// let conn = StreamClientConnection::connect(config).await?;
104/// let tools = conn.list_tools().await?;
105/// println!("Available tools: {:?}", tools);
106/// ```
107pub struct StreamClientConnection {
108    inner: BackendRunningService,
109}
110
111impl StreamClientConnection {
112    /// Connect to a Streamable HTTP MCP server (new upstream peer registry).
113    pub async fn connect(config: McpClientConfig) -> Result<Self> {
114        Self::connect_with_peers(config, Arc::new(UpstreamPeerRegistry::new())).await
115    }
116
117    /// Connect using a shared [`UpstreamPeerRegistry`] (for proxy reconnect / hot-swap).
118    pub async fn connect_with_peers(
119        config: McpClientConfig,
120        upstream_peers: Arc<UpstreamPeerRegistry>,
121    ) -> Result<Self> {
122        let http_client = build_http_client(&config)?;
123
124        // 配置指数退避重试策略,最大间隔 1 分钟,不限制重试次数
125        let retry_policy = CappedExponentialBackoff::new(
126            None,                    // 不限制重试次数
127            Duration::from_secs(1),  // 基础延迟 1 秒
128            Duration::from_secs(60), // 最大间隔 60 秒
129        );
130
131        let mut transport_config =
132            StreamableHttpClientTransportConfig::with_uri(config.url.clone());
133        transport_config.retry_config = Arc::new(retry_policy);
134
135        let transport = StreamableHttpClientTransport::with_client(http_client, transport_config);
136
137        let bridge = BackendNotificationBridge::with_default_info(upstream_peers);
138        let running = bridge
139            .serve(transport)
140            .await
141            .context("Failed to initialize MCP client")?;
142
143        Ok(Self { inner: running })
144    }
145
146    /// List available tools from the MCP server
147    pub async fn list_tools(&self) -> Result<Vec<ToolInfo>> {
148        let result = self.inner.list_tools(None).await?;
149        Ok(result
150            .tools
151            .into_iter()
152            .map(|t| ToolInfo {
153                name: t.name.to_string(),
154                description: t.description.map(|d| d.to_string()),
155            })
156            .collect())
157    }
158
159    /// Fetch an unfiltered, complete discovery snapshot from the real upstream.
160    pub async fn fetch_discovery_snapshot(&self) -> Result<DiscoverySnapshot> {
161        use std::collections::HashSet;
162
163        const MAX_PAGES: usize = 10_000;
164        let server_info = self
165            .peer_info()
166            .map(|info| (*info).clone())
167            .context("Streamable HTTP upstream did not return initialize server info")?;
168        let mut tools = Vec::new();
169        let mut cursor: Option<String> = None;
170        let mut seen = HashSet::new();
171        let mut response_envelope = None;
172
173        for _ in 0..MAX_PAGES {
174            let request = cursor.clone().map(|cursor| {
175                rmcp::model::PaginatedRequestParams::default().with_cursor(Some(cursor))
176            });
177            let mut result = self
178                .inner
179                .list_tools(request)
180                .await
181                .context("failed to list Streamable HTTP upstream tools")?;
182            tools.append(&mut result.tools);
183            let next_cursor = result.next_cursor.take();
184            if response_envelope.is_none() {
185                response_envelope = Some(result);
186            }
187            match next_cursor {
188                Some(next) => {
189                    if !seen.insert(next.clone()) {
190                        anyhow::bail!(
191                            "Streamable HTTP tools pagination returned a repeated cursor"
192                        );
193                    }
194                    cursor = Some(next);
195                }
196                None => {
197                    let mut complete = response_envelope
198                        .context("Streamable HTTP tools pagination returned no response")?;
199                    complete.tools = tools;
200                    complete.next_cursor = None;
201                    return Ok(DiscoverySnapshot {
202                        server_info,
203                        tools: complete,
204                    });
205                }
206            }
207        }
208        anyhow::bail!("Streamable HTTP tools pagination exceeded {MAX_PAGES} pages")
209    }
210
211    /// Check if the connection is closed
212    pub fn is_closed(&self) -> bool {
213        use std::ops::Deref;
214        self.inner.deref().is_transport_closed()
215    }
216
217    /// Get the peer info from the server
218    pub fn peer_info(&self) -> Option<Arc<rmcp::model::ServerInfo>> {
219        // rmcp 3.1.0 narrowed peer_info() to ServerPeerInfo; rebuild the full
220        // ServerInfo (InitializeResult) shape the proxy caches/exposes.
221        self.inner
222            .peer_info()
223            .map(|p| Arc::new(crate::proxy_handler::peer_info_to_server_info((*p).clone())))
224    }
225
226    /// Convert this connection into a ProxyHandler for serving
227    ///
228    /// This consumes the connection and creates a ProxyHandler that can
229    /// proxy requests to the backend MCP server.
230    ///
231    /// # Arguments
232    /// * `mcp_id` - Identifier for logging purposes
233    /// * `tool_filter` - Tool filtering configuration
234    pub fn into_handler(self, mcp_id: String, tool_filter: ToolFilter) -> ProxyHandler {
235        ProxyHandler::with_tool_filter(self.inner, mcp_id, tool_filter)
236    }
237
238    /// Extract the internal RunningService for use with swap_backend
239    ///
240    /// This is used internally to support backend hot-swapping.
241    pub fn into_running_service(self) -> BackendRunningService {
242        self.inner
243    }
244}
245
246/// Simplified tool information
247#[derive(Clone, Debug)]
248pub struct ToolInfo {
249    /// Tool name
250    pub name: String,
251    /// Tool description (optional)
252    pub description: Option<String>,
253}
254
255/// Build an HTTP client with the given configuration
256fn build_http_client(config: &McpClientConfig) -> Result<reqwest::Client> {
257    let mut headers = reqwest::header::HeaderMap::new();
258    for (key, value) in &config.headers {
259        let header_name = key
260            .parse::<reqwest::header::HeaderName>()
261            .with_context(|| format!("Invalid header name: {}", key))?;
262        let header_value = value
263            .parse()
264            .with_context(|| format!("Invalid header value for {key}"))?;
265        headers.insert(header_name, header_value);
266    }
267
268    let mut builder = reqwest::Client::builder().default_headers(headers);
269
270    if let Some(timeout) = config.connect_timeout {
271        builder = builder.connect_timeout(timeout);
272    }
273
274    if let Some(timeout) = config.read_timeout {
275        builder = builder.timeout(timeout);
276    }
277
278    builder.build().context("Failed to build HTTP client")
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_tool_info() {
287        let info = ToolInfo {
288            name: "test_tool".to_string(),
289            description: Some("A test tool".to_string()),
290        };
291        assert_eq!(info.name, "test_tool");
292        assert_eq!(info.description, Some("A test tool".to_string()));
293    }
294
295    #[test]
296    fn invalid_header_error_does_not_expose_value() {
297        let secret = "secret-token\ninvalid";
298        let config = McpClientConfig::new("http://localhost").with_header("Authorization", secret);
299        let error = build_http_client(&config).expect_err("invalid header must fail");
300
301        assert!(error.to_string().contains("Authorization"));
302        assert!(!error.to_string().contains("secret-token"));
303    }
304
305    #[test]
306    fn test_capped_exponential_backoff() {
307        // 测试带上限的指数退避策略
308        let policy = CappedExponentialBackoff::new(
309            None,                    // 不限制重试次数
310            Duration::from_secs(1),  // 基础延迟 1 秒
311            Duration::from_secs(60), // 最大间隔 60 秒
312        );
313
314        // 验证第 1 次重试:1 秒
315        assert_eq!(policy.retry(0), Some(Duration::from_secs(1)));
316        // 验证第 2 次重试:2 秒
317        assert_eq!(policy.retry(1), Some(Duration::from_secs(2)));
318        // 验证第 3 次重试:4 秒
319        assert_eq!(policy.retry(2), Some(Duration::from_secs(4)));
320        // 验证第 7 次重试:64 秒,但被限制为 60 秒
321        assert_eq!(policy.retry(6), Some(Duration::from_secs(60)));
322        // 验证第 10 次重试:仍然是 60 秒
323        assert_eq!(policy.retry(9), Some(Duration::from_secs(60)));
324    }
325
326    #[test]
327    fn test_capped_exponential_backoff_with_max_times() {
328        let policy =
329            CappedExponentialBackoff::new(Some(3), Duration::from_secs(1), Duration::from_secs(60));
330
331        assert_eq!(policy.retry(0), Some(Duration::from_secs(1)));
332        assert_eq!(policy.retry(1), Some(Duration::from_secs(2)));
333        assert_eq!(policy.retry(2), Some(Duration::from_secs(4)));
334        // 超过最大次数
335        assert_eq!(policy.retry(3), None);
336    }
337
338    #[test]
339    fn test_capped_exponential_backoff_never_overflows() {
340        let policy = CappedExponentialBackoff::default();
341
342        assert_eq!(policy.retry(31), Some(Duration::from_secs(60)));
343        assert_eq!(policy.retry(32), Some(Duration::from_secs(60)));
344        assert_eq!(policy.retry(usize::MAX), Some(Duration::from_secs(60)));
345    }
346}