mcp_streamable_proxy/
client.rs1use anyhow::{Context, Result};
7use mcp_common::McpClientConfig;
8use rmcp::{
9 RoleClient, ServiceExt,
10 model::{ClientCapabilities, ClientInfo, Implementation},
11 service::RunningService,
12 transport::streamable_http_client::{StreamableHttpClientTransport, StreamableHttpClientTransportConfig},
13};
14
15use crate::proxy_handler::ProxyHandler;
16use mcp_common::ToolFilter;
17
18pub struct StreamClientConnection {
41 inner: RunningService<RoleClient, ClientInfo>,
42}
43
44impl StreamClientConnection {
45 pub async fn connect(config: McpClientConfig) -> Result<Self> {
54 let http_client = build_http_client(&config)?;
55
56 let transport_config = StreamableHttpClientTransportConfig {
57 uri: config.url.clone().into(),
58 ..Default::default()
59 };
60
61 let transport = StreamableHttpClientTransport::with_client(http_client, transport_config);
62
63 let client_info = create_default_client_info();
64 let running = client_info
65 .serve(transport)
66 .await
67 .context("Failed to initialize MCP client")?;
68
69 Ok(Self { inner: running })
70 }
71
72 pub async fn list_tools(&self) -> Result<Vec<ToolInfo>> {
74 let result = self.inner.list_tools(None).await?;
75 Ok(result
76 .tools
77 .into_iter()
78 .map(|t| ToolInfo {
79 name: t.name.to_string(),
80 description: t.description.map(|d| d.to_string()),
81 })
82 .collect())
83 }
84
85 pub fn is_closed(&self) -> bool {
87 use std::ops::Deref;
88 self.inner.deref().is_transport_closed()
89 }
90
91 pub fn peer_info(&self) -> Option<&rmcp::model::ServerInfo> {
93 self.inner.peer_info()
94 }
95
96 pub fn into_handler(self, mcp_id: String, tool_filter: ToolFilter) -> ProxyHandler {
105 ProxyHandler::with_tool_filter(self.inner, mcp_id, tool_filter)
106 }
107
108 pub fn into_running_service(self) -> RunningService<RoleClient, ClientInfo> {
112 self.inner
113 }
114}
115
116#[derive(Clone, Debug)]
118pub struct ToolInfo {
119 pub name: String,
121 pub description: Option<String>,
123}
124
125fn build_http_client(config: &McpClientConfig) -> Result<reqwest::Client> {
127 let mut headers = reqwest::header::HeaderMap::new();
128 for (key, value) in &config.headers {
129 let header_name = key
130 .parse::<reqwest::header::HeaderName>()
131 .with_context(|| format!("Invalid header name: {}", key))?;
132 let header_value = value
133 .parse()
134 .with_context(|| format!("Invalid header value for {}: {}", key, value))?;
135 headers.insert(header_name, header_value);
136 }
137
138 let mut builder = reqwest::Client::builder().default_headers(headers);
139
140 if let Some(timeout) = config.connect_timeout {
141 builder = builder.connect_timeout(timeout);
142 }
143
144 if let Some(timeout) = config.read_timeout {
145 builder = builder.timeout(timeout);
146 }
147
148 builder.build().context("Failed to build HTTP client")
149}
150
151fn create_default_client_info() -> ClientInfo {
153 ClientInfo {
154 protocol_version: Default::default(),
155 capabilities: ClientCapabilities::builder()
156 .enable_experimental()
157 .enable_roots()
158 .enable_roots_list_changed()
159 .enable_sampling()
160 .build(),
161 client_info: Implementation {
162 name: "mcp-streamable-proxy-client".to_string(),
163 version: env!("CARGO_PKG_VERSION").to_string(),
164 title: None,
165 website_url: None,
166 icons: None,
167 },
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_tool_info() {
177 let info = ToolInfo {
178 name: "test_tool".to_string(),
179 description: Some("A test tool".to_string()),
180 };
181 assert_eq!(info.name, "test_tool");
182 assert_eq!(info.description, Some("A test tool".to_string()));
183 }
184}