mcp_streamable_proxy/
client.rs1use 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::proxy_handler::{BackendRunningService, ProxyHandler};
22use mcp_common::ToolFilter;
23
24#[derive(Debug, Clone)]
32pub struct CappedExponentialBackoff {
33 pub max_times: Option<usize>,
35 pub base_duration: Duration,
37 pub max_interval: Duration,
39}
40
41impl CappedExponentialBackoff {
42 pub fn new(max_times: Option<usize>, base_duration: Duration, max_interval: Duration) -> Self {
49 Self {
50 max_times,
51 base_duration,
52 max_interval,
53 }
54 }
55}
56
57impl Default for CappedExponentialBackoff {
58 fn default() -> Self {
59 Self {
60 max_times: None,
61 base_duration: Duration::from_secs(1),
62 max_interval: Duration::from_secs(60),
63 }
64 }
65}
66
67impl SseRetryPolicy for CappedExponentialBackoff {
68 fn retry(&self, current_times: usize) -> Option<Duration> {
69 if let Some(max_times) = self.max_times
71 && current_times >= max_times
72 {
73 return None;
74 }
75
76 let exponential_delay = self.base_duration * (2u32.pow(current_times as u32));
78
79 Some(exponential_delay.min(self.max_interval))
81 }
82}
83
84pub struct StreamClientConnection {
107 inner: BackendRunningService,
108}
109
110impl StreamClientConnection {
111 pub async fn connect(config: McpClientConfig) -> Result<Self> {
113 Self::connect_with_peers(config, Arc::new(UpstreamPeerRegistry::new())).await
114 }
115
116 pub async fn connect_with_peers(
118 config: McpClientConfig,
119 upstream_peers: Arc<UpstreamPeerRegistry>,
120 ) -> Result<Self> {
121 let http_client = build_http_client(&config)?;
122
123 let retry_policy = CappedExponentialBackoff::new(
125 None, Duration::from_secs(1), Duration::from_secs(60), );
129
130 let mut transport_config =
131 StreamableHttpClientTransportConfig::with_uri(config.url.clone());
132 transport_config.retry_config = Arc::new(retry_policy);
133
134 let transport = StreamableHttpClientTransport::with_client(http_client, transport_config);
135
136 let bridge = BackendNotificationBridge::with_default_info(upstream_peers);
137 let running = bridge
138 .serve(transport)
139 .await
140 .context("Failed to initialize MCP client")?;
141
142 Ok(Self { inner: running })
143 }
144
145 pub async fn list_tools(&self) -> Result<Vec<ToolInfo>> {
147 let result = self.inner.list_tools(None).await?;
148 Ok(result
149 .tools
150 .into_iter()
151 .map(|t| ToolInfo {
152 name: t.name.to_string(),
153 description: t.description.map(|d| d.to_string()),
154 })
155 .collect())
156 }
157
158 pub fn is_closed(&self) -> bool {
160 use std::ops::Deref;
161 self.inner.deref().is_transport_closed()
162 }
163
164 pub fn peer_info(&self) -> Option<Arc<rmcp::model::ServerInfo>> {
166 self.inner.peer_info()
167 }
168
169 pub fn into_handler(self, mcp_id: String, tool_filter: ToolFilter) -> ProxyHandler {
178 ProxyHandler::with_tool_filter(self.inner, mcp_id, tool_filter)
179 }
180
181 pub fn into_running_service(self) -> BackendRunningService {
185 self.inner
186 }
187}
188
189#[derive(Clone, Debug)]
191pub struct ToolInfo {
192 pub name: String,
194 pub description: Option<String>,
196}
197
198fn build_http_client(config: &McpClientConfig) -> Result<reqwest::Client> {
200 let mut headers = reqwest::header::HeaderMap::new();
201 for (key, value) in &config.headers {
202 let header_name = key
203 .parse::<reqwest::header::HeaderName>()
204 .with_context(|| format!("Invalid header name: {}", key))?;
205 let header_value = value
206 .parse()
207 .with_context(|| format!("Invalid header value for {}: {}", key, value))?;
208 headers.insert(header_name, header_value);
209 }
210
211 let mut builder = reqwest::Client::builder().default_headers(headers);
212
213 if let Some(timeout) = config.connect_timeout {
214 builder = builder.connect_timeout(timeout);
215 }
216
217 if let Some(timeout) = config.read_timeout {
218 builder = builder.timeout(timeout);
219 }
220
221 builder.build().context("Failed to build HTTP client")
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_tool_info() {
230 let info = ToolInfo {
231 name: "test_tool".to_string(),
232 description: Some("A test tool".to_string()),
233 };
234 assert_eq!(info.name, "test_tool");
235 assert_eq!(info.description, Some("A test tool".to_string()));
236 }
237
238 #[test]
239 fn test_capped_exponential_backoff() {
240 let policy = CappedExponentialBackoff::new(
242 None, Duration::from_secs(1), Duration::from_secs(60), );
246
247 assert_eq!(policy.retry(0), Some(Duration::from_secs(1)));
249 assert_eq!(policy.retry(1), Some(Duration::from_secs(2)));
251 assert_eq!(policy.retry(2), Some(Duration::from_secs(4)));
253 assert_eq!(policy.retry(6), Some(Duration::from_secs(60)));
255 assert_eq!(policy.retry(9), Some(Duration::from_secs(60)));
257 }
258
259 #[test]
260 fn test_capped_exponential_backoff_with_max_times() {
261 let policy =
262 CappedExponentialBackoff::new(Some(3), Duration::from_secs(1), Duration::from_secs(60));
263
264 assert_eq!(policy.retry(0), Some(Duration::from_secs(1)));
265 assert_eq!(policy.retry(1), Some(Duration::from_secs(2)));
266 assert_eq!(policy.retry(2), Some(Duration::from_secs(4)));
267 assert_eq!(policy.retry(3), None);
269 }
270}