1use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Duration;
9
10use anyhow::{bail, Result};
11use tokio_util::sync::CancellationToken;
12use tracing::{info, warn};
13
14use rmcp::transport::streamable_http_server::{
15 StreamableHttpServerConfig, StreamableHttpService,
16};
17
18use crate::backend_client::UpstreamPeerRegistry;
19use crate::backend_connector::{
20 BackendConnector, BackendIsolation, StdioBackendConnector, UrlBackendConnector,
21};
22use crate::{ProxyAwareSessionManager, ProxyHandler, ToolFilter};
23
24#[derive(Debug, Clone)]
28pub enum BackendConfig {
29 Stdio {
31 command: String,
33 args: Option<Vec<String>>,
35 env: Option<HashMap<String, String>>,
37 },
38 Url {
40 url: String,
42 headers: Option<HashMap<String, String>>,
44 },
45}
46
47#[derive(Debug, Clone)]
49pub struct StreamServerConfig {
50 pub stateful_mode: bool,
52 pub mcp_id: Option<String>,
54 pub tool_filter: Option<ToolFilter>,
56 pub backend_isolation: Option<BackendIsolation>,
58 pub max_sessions: Option<usize>,
60 pub connect_timeout: Duration,
62}
63
64impl Default for StreamServerConfig {
65 fn default() -> Self {
66 Self {
67 stateful_mode: false,
68 mcp_id: None,
69 tool_filter: None,
70 backend_isolation: None,
71 max_sessions: None,
72 connect_timeout: Duration::from_secs(30),
73 }
74 }
75}
76
77pub struct StreamServerBuilder {
79 backend_config: BackendConfig,
80 server_config: StreamServerConfig,
81}
82
83impl StreamServerBuilder {
84 pub fn new(backend: BackendConfig) -> Self {
86 Self {
87 backend_config: backend,
88 server_config: StreamServerConfig::default(),
89 }
90 }
91
92 pub fn stateful(mut self, enabled: bool) -> Self {
94 self.server_config.stateful_mode = enabled;
95 self
96 }
97
98 pub fn mcp_id(mut self, id: impl Into<String>) -> Self {
100 self.server_config.mcp_id = Some(id.into());
101 self
102 }
103
104 pub fn tool_filter(mut self, filter: ToolFilter) -> Self {
106 self.server_config.tool_filter = Some(filter);
107 self
108 }
109
110 pub fn backend_isolation(mut self, isolation: BackendIsolation) -> Self {
112 self.server_config.backend_isolation = Some(isolation);
113 self
114 }
115
116 pub fn max_sessions(mut self, max: usize) -> Self {
118 self.server_config.max_sessions = Some(max);
119 self
120 }
121
122 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
124 self.server_config.connect_timeout = timeout;
125 self
126 }
127
128 pub async fn build(self) -> Result<(axum::Router, CancellationToken, ProxyHandler)> {
134 let mcp_id = self
135 .server_config
136 .mcp_id
137 .clone()
138 .unwrap_or_else(|| "stream-proxy".into());
139 let tool_filter = self
140 .server_config
141 .tool_filter
142 .clone()
143 .unwrap_or_default();
144
145 let isolation = self.server_config.backend_isolation.unwrap_or(
146 match &self.backend_config {
147 BackendConfig::Url { .. } => BackendIsolation::for_url(),
148 BackendConfig::Stdio { .. } => BackendIsolation::for_stdio(),
149 },
150 );
151
152 if let (BackendConfig::Stdio { .. }, BackendIsolation::PerSession) =
153 (&self.backend_config, isolation)
154 {
155 bail!("Stdio backend does not support BackendIsolation::PerSession; use Shared");
156 }
157
158 let (router, ct, management_handler) = match (&self.backend_config, isolation) {
159 (BackendConfig::Stdio { command, args, env }, BackendIsolation::Shared) => {
160 let connector = StdioBackendConnector::new(
161 command.clone(),
162 args.clone(),
163 env.clone(),
164 mcp_id.clone(),
165 );
166 let registry = Arc::new(UpstreamPeerRegistry::new());
167 let client = connector.connect_shared(registry).await?;
168 let handler = ProxyHandler::with_tool_filter(client, mcp_id.clone(), tool_filter);
169 let (router, ct) = self
170 .create_shared_server(handler.clone())
171 .await?;
172 (router, ct, handler)
173 }
174 (BackendConfig::Url { url, headers }, BackendIsolation::Shared) => {
175 let connector = Arc::new(
176 UrlBackendConnector::new(url.clone(), headers.clone())
177 .with_connect_timeout(self.server_config.connect_timeout),
178 );
179 let registry = Arc::new(UpstreamPeerRegistry::new());
180 let client = connector.connect_shared(registry).await?;
181 let handler = ProxyHandler::with_tool_filter(client, mcp_id.clone(), tool_filter);
182 let (router, ct) = self
183 .create_shared_server(handler.clone())
184 .await?;
185 (router, ct, handler)
186 }
187 (BackendConfig::Url { url, headers }, BackendIsolation::PerSession) => {
188 let connector: Arc<dyn BackendConnector> = Arc::new(
189 UrlBackendConnector::new(url.clone(), headers.clone())
190 .with_connect_timeout(self.server_config.connect_timeout),
191 );
192 let (router, ct) = self
193 .create_per_session_server(connector.clone(), mcp_id.clone(), tool_filter.clone())
194 .await?;
195 let management =
197 ProxyHandler::new_per_session(connector, mcp_id.clone(), tool_filter);
198 (router, ct, management)
199 }
200 (BackendConfig::Stdio { .. }, BackendIsolation::PerSession) => unreachable!(),
201 };
202
203 info!(
204 "[StreamServerBuilder] Server created - mcp_id: {}, stateful: {}, isolation: {:?}",
205 mcp_id, self.server_config.stateful_mode, isolation
206 );
207
208 Ok((router, ct, management_handler))
209 }
210
211 async fn create_shared_server(
212 &self,
213 proxy_handler: ProxyHandler,
214 ) -> Result<(axum::Router, CancellationToken)> {
215 let handler = Arc::new(proxy_handler);
216 let ct = CancellationToken::new();
217
218 if self.server_config.stateful_mode {
219 let session_manager = ProxyAwareSessionManager::new(handler.clone())
220 .with_max_sessions(self.server_config.max_sessions);
221 let handler_for_service = handler.clone();
222 let mut server_config = StreamableHttpServerConfig::default();
223 server_config.stateful_mode = true;
224 let service = StreamableHttpService::new(
225 move || Ok((*handler_for_service).clone()),
226 session_manager.into(),
227 server_config,
228 );
229 Ok((axum::Router::new().fallback_service(service), ct))
230 } else {
231 use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
232 let handler_for_service = handler.clone();
233 let server_config = StreamableHttpServerConfig::default();
234 let service = StreamableHttpService::new(
235 move || Ok((*handler_for_service).clone()),
236 LocalSessionManager::default().into(),
237 server_config,
238 );
239 Ok((axum::Router::new().fallback_service(service), ct))
240 }
241 }
242
243 async fn create_per_session_server(
244 &self,
245 connector: Arc<dyn BackendConnector>,
246 mcp_id: String,
247 tool_filter: ToolFilter,
248 ) -> Result<(axum::Router, CancellationToken)> {
249 if !self.server_config.stateful_mode {
250 warn!(
251 "Per-session isolation is intended for stateful_mode; enabling stateful_mode"
252 );
253 }
254 let ct = CancellationToken::new();
255 let template = Arc::new(ProxyHandler::new_per_session(
258 connector.clone(),
259 mcp_id.clone(),
260 tool_filter.clone(),
261 ));
262 let session_manager = ProxyAwareSessionManager::new(template)
263 .with_max_sessions(self.server_config.max_sessions);
264
265 let mut server_config = StreamableHttpServerConfig::default();
266 server_config.stateful_mode = true;
267 let service = StreamableHttpService::new(
268 move || {
269 Ok(ProxyHandler::new_per_session(
270 connector.clone(),
271 mcp_id.clone(),
272 tool_filter.clone(),
273 ))
274 },
275 session_manager.into(),
276 server_config,
277 );
278 Ok((axum::Router::new().fallback_service(service), ct))
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_builder_creation() {
288 let builder = StreamServerBuilder::new(BackendConfig::Stdio {
289 command: "echo".into(),
290 args: Some(vec!["hello".into()]),
291 env: None,
292 })
293 .mcp_id("test")
294 .stateful(true);
295
296 assert!(builder.server_config.mcp_id.is_some());
297 assert_eq!(builder.server_config.mcp_id.as_deref(), Some("test"));
298 assert!(builder.server_config.stateful_mode);
299 }
300
301 #[test]
302 fn test_url_backend_config() {
303 let mut headers = HashMap::new();
304 headers.insert("Authorization".into(), "Bearer token123".into());
305 headers.insert("X-Custom".into(), "value".into());
306
307 let builder = StreamServerBuilder::new(BackendConfig::Url {
308 url: "http://localhost:8080/mcp".into(),
309 headers: Some(headers),
310 });
311
312 match &builder.backend_config {
313 BackendConfig::Url { url, headers } => {
314 assert_eq!(url, "http://localhost:8080/mcp");
315 assert!(headers.is_some());
316 }
317 _ => panic!("Expected URL backend"),
318 }
319 }
320
321 #[test]
322 fn default_isolation_by_backend() {
323 assert_eq!(BackendIsolation::for_url(), BackendIsolation::PerSession);
324 assert_eq!(BackendIsolation::for_stdio(), BackendIsolation::Shared);
325 }
326}