Skip to main content

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}