tower_mcp/proxy/builder.rs
1//! Builder for [`McpProxy`].
2
3use std::convert::Infallible;
4use std::fmt;
5
6use tokio::sync::mpsc;
7use tower::Layer;
8use tower::util::BoxCloneService;
9
10use crate::client::{ClientTransport, McpClient};
11use crate::error::{Error, Result};
12use crate::router::{RouterRequest, RouterResponse};
13use crate::transport::CatchError;
14
15use super::backend::{Backend, ListChanged};
16use super::service::{BackendEntry, McpProxy};
17
18/// A backend that was skipped during proxy construction.
19#[derive(Debug)]
20pub struct SkippedBackend {
21 /// The namespace that was assigned to this backend.
22 pub namespace: String,
23 /// The error that caused the backend to be skipped.
24 pub error: Error,
25 /// The phase where the failure occurred.
26 pub phase: SkippedPhase,
27}
28
29/// Which phase a backend failed in.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SkippedPhase {
32 /// Failed to connect the transport.
33 Connect,
34 /// Failed during MCP initialization handshake.
35 Initialize,
36}
37
38impl fmt::Display for SkippedBackend {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 write!(
41 f,
42 "[{}] {:?} failed: {}",
43 self.namespace, self.phase, self.error
44 )
45 }
46}
47
48/// Result of building an [`McpProxy`], including any backends that were skipped.
49pub struct ProxyBuildResult {
50 /// The constructed proxy.
51 pub proxy: McpProxy,
52 /// Backends that failed to connect or initialize and were skipped.
53 pub skipped: Vec<SkippedBackend>,
54}
55
56/// Pending backend before the proxy is built.
57struct PendingBackend {
58 namespace: String,
59 backend: Backend,
60 invalidation_rx: Option<mpsc::Receiver<ListChanged>>,
61 /// Type-erased service (set by `.backend_layer()`).
62 /// If None, BackendService is used directly.
63 custom_service: Option<BoxCloneService<RouterRequest, RouterResponse, Infallible>>,
64}
65
66/// Backends that failed during `backend()` connection, tracked until `build()`.
67struct ConnectionFailure {
68 namespace: String,
69 error: Error,
70}
71
72/// Builder for constructing an [`McpProxy`].
73///
74/// # Example
75///
76/// ```rust,no_run
77/// use tower_mcp::proxy::McpProxy;
78/// use tower_mcp::client::StdioClientTransport;
79///
80/// # async fn example() -> Result<(), tower_mcp::BoxError> {
81/// let proxy = McpProxy::builder("my-proxy", "1.0.0")
82/// .backend("db", StdioClientTransport::spawn("db-server", &[]).await?)
83/// .await
84/// .separator(".")
85/// .build()
86/// .await?;
87/// # Ok(())
88/// # }
89/// ```
90///
91/// # Per-Backend Middleware
92///
93/// Apply Tower middleware to individual backends using
94/// [`backend_layer()`](Self::backend_layer):
95///
96/// ```rust,ignore
97/// use std::time::Duration;
98/// use tower::timeout::TimeoutLayer;
99///
100/// let proxy = McpProxy::builder("proxy", "1.0.0")
101/// .backend("slow-api", slow_transport).await
102/// .backend_layer(TimeoutLayer::new(Duration::from_secs(60)))
103/// .backend("fast-db", fast_transport).await
104/// .backend_layer(TimeoutLayer::new(Duration::from_secs(5)))
105/// .build()
106/// .await?;
107/// ```
108pub struct McpProxyBuilder {
109 name: String,
110 version: String,
111 separator: String,
112 pending: Vec<PendingBackend>,
113 notification_tx: Option<crate::context::NotificationSender>,
114 connection_failures: Vec<ConnectionFailure>,
115 /// Custom instructions override. If set, used instead of aggregated backend instructions.
116 instructions: Option<String>,
117}
118
119impl McpProxyBuilder {
120 /// Create a new proxy builder.
121 pub(crate) fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
122 Self {
123 name: name.into(),
124 version: version.into(),
125 separator: "_".to_string(),
126 pending: Vec::new(),
127 notification_tx: None,
128 connection_failures: Vec::new(),
129 instructions: None,
130 }
131 }
132
133 /// Set the namespace separator (default: `_`).
134 ///
135 /// The separator is inserted between the backend namespace and the
136 /// tool/resource/prompt name. For example, with separator `"_"` and
137 /// namespace `"db"`, a tool named `"query"` becomes `"db_query"`.
138 pub fn separator(mut self, sep: impl Into<String>) -> Self {
139 self.separator = sep.into();
140 self
141 }
142
143 /// Set a notification sender for forwarding backend list-changed
144 /// notifications to downstream clients.
145 ///
146 /// When a backend emits `tools/list_changed`, `resources/list_changed`,
147 /// or `prompts/list_changed`, the proxy refreshes its cache and then
148 /// forwards the notification through this sender so transports can
149 /// relay it to connected clients.
150 ///
151 /// # Example
152 ///
153 /// ```rust,ignore
154 /// use tower_mcp::context::notification_channel;
155 ///
156 /// let (notif_tx, notif_rx) = notification_channel(32);
157 /// let proxy = McpProxy::builder("proxy", "1.0.0")
158 /// .notification_sender(notif_tx)
159 /// .backend("db", transport).await
160 /// .build().await?;
161 ///
162 /// let mut transport = GenericStdioTransport::with_notifications(proxy, notif_rx);
163 /// ```
164 pub fn notification_sender(mut self, tx: crate::context::NotificationSender) -> Self {
165 self.notification_tx = Some(tx);
166 self
167 }
168
169 /// Set custom instructions for the proxy's initialize response.
170 ///
171 /// When set, this overrides the default behavior of aggregating
172 /// backend instructions. Use this to provide a curated description
173 /// of the proxy's capabilities.
174 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
175 self.instructions = Some(instructions.into());
176 self
177 }
178
179 /// Add a backend from a connected [`McpClient`].
180 ///
181 /// Note: backends added this way will not have automatic cache refresh
182 /// on list-changed notifications. Use [`backend()`](Self::backend) with
183 /// a transport for full notification support.
184 pub fn backend_client(mut self, namespace: impl Into<String>, client: McpClient) -> Self {
185 let backend = Backend::from_client(namespace, client, self.separator.clone());
186 self.pending.push(PendingBackend {
187 namespace: backend.namespace.clone(),
188 backend,
189 invalidation_rx: None,
190 custom_service: None,
191 });
192 self
193 }
194
195 /// Add a backend from a [`ClientTransport`].
196 ///
197 /// The transport will be connected immediately with a notification handler
198 /// that watches for list-changed events. Initialization happens during
199 /// [`build()`](Self::build).
200 pub async fn backend(
201 mut self,
202 namespace: impl Into<String>,
203 transport: impl ClientTransport,
204 ) -> Self {
205 let namespace = namespace.into();
206 let (invalidation_tx, invalidation_rx) = mpsc::channel(16);
207
208 match Backend::connect(
209 namespace.clone(),
210 transport,
211 self.separator.clone(),
212 invalidation_tx,
213 )
214 .await
215 {
216 Ok(backend) => {
217 self.pending.push(PendingBackend {
218 namespace,
219 backend,
220 invalidation_rx: Some(invalidation_rx),
221 custom_service: None,
222 });
223 }
224 Err(e) => {
225 tracing::error!(
226 namespace = %namespace,
227 error = %e,
228 "Failed to connect backend"
229 );
230 self.connection_failures.push(ConnectionFailure {
231 namespace,
232 error: e,
233 });
234 }
235 }
236 self
237 }
238
239 /// Add a backend from a transport, returning an error on connection failure.
240 ///
241 /// Unlike [`backend()`](Self::backend) which silently skips failed connections,
242 /// this method returns an error so the caller can decide how to handle it.
243 ///
244 /// # Example
245 ///
246 /// ```rust,ignore
247 /// let builder = McpProxy::builder("proxy", "1.0.0")
248 /// .backend_try("db", transport).await?;
249 /// ```
250 pub async fn backend_try(
251 mut self,
252 namespace: impl Into<String>,
253 transport: impl ClientTransport,
254 ) -> Result<Self> {
255 let namespace = namespace.into();
256 let (invalidation_tx, invalidation_rx) = mpsc::channel(16);
257
258 let backend = Backend::connect(
259 namespace.clone(),
260 transport,
261 self.separator.clone(),
262 invalidation_tx,
263 )
264 .await?;
265
266 self.pending.push(PendingBackend {
267 namespace,
268 backend,
269 invalidation_rx: Some(invalidation_rx),
270 custom_service: None,
271 });
272
273 Ok(self)
274 }
275
276 /// Apply a Tower layer to the most recently added backend.
277 ///
278 /// The layer wraps the backend's dispatch service, allowing standard
279 /// Tower middleware (timeout, rate limit, concurrency limit, etc.) to
280 /// be applied per-backend.
281 ///
282 /// Repeated calls stack: each layer wraps the service composed so far,
283 /// so the layer added last sees a request first. In the example below a
284 /// request passes the timeout, then the rate limiter, then reaches the
285 /// backend, and a `ServiceBuilder` composing the same middleware in one
286 /// call remains equivalent.
287 ///
288 /// Layers that produce errors (e.g., `TimeoutLayer`) are automatically
289 /// wrapped with [`CatchError`] to convert errors into JSON-RPC error
290 /// responses, maintaining the `Error = Infallible` contract.
291 ///
292 /// # Example
293 ///
294 /// ```rust,ignore
295 /// use std::time::Duration;
296 /// use tower::limit::RateLimitLayer;
297 /// use tower::timeout::TimeoutLayer;
298 ///
299 /// let proxy = McpProxy::builder("proxy", "1.0.0")
300 /// .backend("slow", transport).await
301 /// .backend_layer(RateLimitLayer::new(50, Duration::from_secs(1)))
302 /// .backend_layer(TimeoutLayer::new(Duration::from_secs(30)))
303 /// .build()
304 /// .await?;
305 /// ```
306 ///
307 /// # Panics
308 ///
309 /// Panics if no backend has been added yet.
310 pub fn backend_layer<L>(mut self, layer: L) -> Self
311 where
312 L: Layer<BoxCloneService<RouterRequest, RouterResponse, Infallible>> + Send + 'static,
313 L::Service: tower_service::Service<RouterRequest, Response = RouterResponse>
314 + Clone
315 + Send
316 + 'static,
317 <L::Service as tower_service::Service<RouterRequest>>::Error: fmt::Display + Send,
318 <L::Service as tower_service::Service<RouterRequest>>::Future: Send,
319 {
320 let pending = self
321 .pending
322 .last_mut()
323 .expect("backend_layer called before adding a backend");
324
325 // Wrap the service composed so far, not the raw backend: starting
326 // from the base on every call silently discarded all but the most
327 // recent layer (#1173).
328 let current = pending
329 .custom_service
330 .take()
331 .unwrap_or_else(|| BoxCloneService::new(pending.backend.service()));
332 let layered = layer.layer(current);
333 // Wrap with CatchError to convert middleware errors to JSON-RPC errors
334 let caught = CatchError::new(layered);
335 pending.custom_service = Some(BoxCloneService::new(caught));
336
337 self
338 }
339
340 /// Build the proxy, initializing all backends concurrently.
341 ///
342 /// Each backend runs the MCP initialize handshake and discovers its
343 /// capabilities (tools, resources, prompts). Backends that fail to
344 /// initialize are logged and skipped.
345 ///
346 /// Returns a [`ProxyBuildResult`] containing the proxy and any backends
347 /// that were skipped due to initialization failures. Check
348 /// `result.skipped` to see which backends failed and why.
349 ///
350 /// For backends added via [`backend()`](Self::backend), a background task
351 /// is spawned that watches for list-changed notifications and automatically
352 /// refreshes the affected cache.
353 ///
354 /// # Errors
355 ///
356 /// Returns an error if no backends were configured or if all backends
357 /// failed to initialize.
358 pub async fn build(mut self) -> Result<ProxyBuildResult> {
359 if self.pending.is_empty() {
360 return Err(Error::internal("No backends configured"));
361 }
362
363 // Ensure all backends use the builder's final separator.
364 // This handles the case where `.separator()` is called after `.backend()`.
365 for pb in &mut self.pending {
366 pb.backend.separator = self.separator.clone();
367 }
368
369 // Check for duplicate namespaces
370 let namespaces: Vec<&str> = self
371 .pending
372 .iter()
373 .map(|pb| pb.namespace.as_str())
374 .collect();
375 {
376 let mut sorted = namespaces.clone();
377 sorted.sort();
378 sorted.dedup();
379 if sorted.len() != namespaces.len() {
380 return Err(Error::internal("Duplicate backend namespaces"));
381 }
382 }
383
384 // Check for ambiguous namespace prefixes.
385 // With separator "_", namespaces "redis" and "redis_ft" both produce
386 // the prefix "redis_", making "redis_ft_search" ambiguous.
387 let prefixes: Vec<String> = namespaces
388 .iter()
389 .map(|ns| format!("{}{}", ns, self.separator))
390 .collect();
391 for (i, prefix_i) in prefixes.iter().enumerate() {
392 for (j, prefix_j) in prefixes.iter().enumerate() {
393 if i != j && prefix_j.starts_with(prefix_i.as_str()) {
394 return Err(Error::internal(format!(
395 "Ambiguous namespace prefixes: \"{}\" and \"{}\" with separator \"{}\". \
396 The prefix \"{}\" is a prefix of \"{}\", which makes routing ambiguous. \
397 Use a different separator (e.g., \".\") or rename the namespaces.",
398 namespaces[i], namespaces[j], self.separator, prefix_i, prefix_j,
399 )));
400 }
401 }
402 }
403
404 // Initialize all backends concurrently
405 let name = self.name.clone();
406 let version = self.version.clone();
407 let init_futures: Vec<_> = self
408 .pending
409 .into_iter()
410 .map(|mut pb| {
411 let name = name.clone();
412 let version = version.clone();
413 async move {
414 match pb.backend.initialize(&name, &version).await {
415 Ok(instructions) => {
416 pb.backend.instructions = instructions;
417 {
418 let cache = pb.backend.cache.read().await;
419 tracing::info!(
420 namespace = %pb.namespace,
421 tools = cache.tools.len(),
422 resources = cache.resources.len(),
423 prompts = cache.prompts.len(),
424 "Backend initialized"
425 );
426 }
427 Ok(pb)
428 }
429 Err(e) => {
430 tracing::error!(
431 namespace = %pb.namespace,
432 error = %e,
433 "Failed to initialize backend, skipping"
434 );
435 Err(SkippedBackend {
436 namespace: pb.namespace,
437 error: e,
438 phase: SkippedPhase::Initialize,
439 })
440 }
441 }
442 }
443 })
444 .collect();
445
446 let results = futures::future::join_all(init_futures).await;
447
448 let mut backends = Vec::new();
449 let mut entries = Vec::new();
450 let mut invalidation_rxs = Vec::new();
451
452 // Start with connection failures from the `backend()` phase
453 let mut skipped: Vec<SkippedBackend> = self
454 .connection_failures
455 .into_iter()
456 .map(|f| SkippedBackend {
457 namespace: f.namespace,
458 error: f.error,
459 phase: SkippedPhase::Connect,
460 })
461 .collect();
462
463 for result in results {
464 let pb = match result {
465 Ok(pb) => pb,
466 Err(s) => {
467 skipped.push(s);
468 continue;
469 }
470 };
471 let entry = if let Some(svc) = pb.custom_service {
472 BackendEntry::from_backend_with_service(&pb.backend, svc)
473 } else {
474 BackendEntry::from_backend(&pb.backend)
475 };
476
477 if let Some(rx) = pb.invalidation_rx {
478 invalidation_rxs.push((backends.len(), rx));
479 }
480
481 entries.push(entry);
482 backends.push(pb.backend);
483 }
484
485 if backends.is_empty() {
486 return Err(Error::internal("All backends failed to initialize"));
487 }
488
489 // Build instructions: use custom override or aggregate from backends
490 let instructions = if let Some(custom) = self.instructions {
491 Some(custom)
492 } else {
493 let mut parts = vec![format!(
494 "MCP proxy aggregating {} backend servers.",
495 backends.len()
496 )];
497 for b in &backends {
498 if let Some(inst) = &b.instructions {
499 parts.push(format!("[{}] {}", b.namespace, inst));
500 }
501 }
502 // Only include backend details if at least one backend has instructions
503 if parts.len() > 1 {
504 Some(parts.join("\n\n"))
505 } else {
506 Some(parts.remove(0))
507 }
508 };
509
510 let proxy = McpProxy::new(
511 self.name,
512 self.version,
513 backends,
514 entries,
515 self.notification_tx,
516 instructions,
517 self.separator.clone(),
518 );
519
520 // Spawn invalidation watchers for backends with notification handlers.
521 for (backend_idx, rx) in invalidation_rxs {
522 proxy.spawn_invalidation_watcher(backend_idx, rx);
523 }
524
525 Ok(ProxyBuildResult { proxy, skipped })
526 }
527
528 /// Build the proxy, failing if any backend fails to initialize.
529 ///
530 /// Unlike [`build()`](Self::build) which skips failed backends,
531 /// this method returns an error if any backend fails to connect or
532 /// initialize.
533 ///
534 /// # Errors
535 ///
536 /// Returns the first initialization failure encountered (after
537 /// waiting for all backends to attempt initialization).
538 pub async fn build_strict(self) -> Result<McpProxy> {
539 let result = self.build().await?;
540 if let Some(first) = result.skipped.into_iter().next() {
541 return Err(Error::internal(format!(
542 "Backend \"{}\" failed to initialize: {}",
543 first.namespace, first.error
544 )));
545 }
546 Ok(result.proxy)
547 }
548}