Skip to main content

tower_mcp/proxy/
mod.rs

1//! MCP Proxy -- aggregate multiple backend MCP servers behind a single endpoint.
2//!
3//! The proxy connects to N backend MCP servers and exposes their combined
4//! tools, resources, and prompts through a unified `Service<RouterRequest>`
5//! interface. Each backend's capabilities are namespaced to avoid collisions.
6//!
7//! # Architecture
8//!
9//! ```text
10//!                     McpProxy
11//!                    /    |    \
12//!           Backend A  Backend B  Backend C
13//!           (stdio)    (HTTP)     (stdio)
14//! ```
15//!
16//! Each backend is an [`McpClient`](crate::client::McpClient) that the proxy
17//! initializes and manages. Tool/resource/prompt discovery runs concurrently
18//! across all backends. Results are cached and automatically refreshed when
19//! backends emit `tools/list_changed`, `resources/list_changed`, or
20//! `prompts/list_changed` notifications.
21//!
22//! # Quick Start
23//!
24//! ```rust,no_run
25//! use tower_mcp::proxy::McpProxy;
26//! use tower_mcp::client::StdioClientTransport;
27//!
28//! # async fn example() -> Result<(), tower_mcp::BoxError> {
29//! let proxy = McpProxy::builder("my-proxy", "1.0.0")
30//!     .backend("db", StdioClientTransport::spawn("db-server", &[]).await?)
31//!     .await
32//!     .backend("fs", StdioClientTransport::spawn("fs-server", &[]).await?)
33//!     .await
34//!     .build_strict()
35//!     .await?;
36//!
37//! // Tools become db_query, fs_read, etc. (namespace + separator + name)
38//! // Serve over any transport -- stdio, HTTP, WebSocket.
39//! let mut transport = tower_mcp::GenericStdioTransport::new(proxy);
40//! transport.run().await?;
41//! # Ok(())
42//! # }
43//! ```
44//!
45//! # Namespacing
46//!
47//! All tools, resources, and prompts from each backend are prefixed with
48//! `{namespace}{separator}` to avoid naming collisions. The default separator
49//! is `_`, so a tool named `query` on the `db` backend becomes `db_query`.
50//!
51//! Use [`McpProxyBuilder::separator()`] to change the separator:
52//!
53//! ```rust,ignore
54//! McpProxy::builder("proxy", "1.0.0")
55//!     .backend("db", transport).await
56//!     .separator(".")   // tools become "db.query" instead of "db_query"
57//!     .build().await?;
58//! ```
59//!
60//! # Separator Selection
61//!
62//! The separator appears in every namespaced name, so choose carefully:
63//!
64//! | Separator | Example | Pros | Cons |
65//! |-----------|---------|------|------|
66//! | `_` (default) | `db_query` | Natural for tool names | Ambiguous if namespaces contain `_` (e.g., `redis` vs `redis_ft`) |
67//! | `.` | `db.query` | Unambiguous, hierarchical feel | Some MCP clients may not handle dots in names |
68//! | `:` | `db:query` | Clear delimiter, rarely in names | Less common convention |
69//!
70//! The builder validates at build time that no namespace prefix is ambiguous
71//! with the chosen separator. If you use namespaces that contain the separator
72//! character, the build will fail with an error.
73//!
74//! **Recommendation:** Use `.` or `:` when namespace names might share prefixes
75//! or contain underscores.
76//!
77//! # Per-Backend Middleware
78//!
79//! Apply Tower middleware to individual backends using
80//! [`McpProxyBuilder::backend_layer()`]. This is useful for backend-specific
81//! timeouts, rate limits, or retry policies:
82//!
83//! ```rust,ignore
84//! use std::time::Duration;
85//! use tower::timeout::TimeoutLayer;
86//!
87//! let proxy = McpProxy::builder("proxy", "1.0.0")
88//!     // Fast backend: tight timeout
89//!     .backend("cache", cache_transport).await
90//!     .backend_layer(TimeoutLayer::new(Duration::from_secs(2)))
91//!     // Slow backend: generous timeout
92//!     .backend("llm", llm_transport).await
93//!     .backend_layer(TimeoutLayer::new(Duration::from_secs(60)))
94//!     // No middleware on this one
95//!     .backend("db", db_transport).await
96//!     .build().await?;
97//! ```
98//!
99//! Middleware errors (e.g., `tower::timeout::error::Elapsed`) are automatically
100//! converted to JSON-RPC error responses via
101//! [`CatchError`](crate::transport::CatchError), preserving the
102//! `Error = Infallible` contract required by transports.
103//!
104//! # Proxy-Level Middleware
105//!
106//! Because [`McpProxy`] implements `Service<RouterRequest>`, standard Tower
107//! middleware composes naturally at the proxy level:
108//!
109//! ```rust,ignore
110//! use tower::ServiceBuilder;
111//!
112//! let service = ServiceBuilder::new()
113//!     .layer(AuthLayer::new(validator))
114//!     .layer(RateLimitLayer::new(100, Duration::from_secs(1)))
115//!     .service(proxy);
116//! ```
117//!
118//! # Notification Forwarding
119//!
120//! When a backend emits a list-changed notification (e.g., after adding or
121//! removing tools at runtime), the proxy:
122//!
123//! 1. Refreshes its cached capabilities for that backend
124//! 2. Forwards the notification to connected downstream clients
125//!
126//! To enable forwarding, provide a [`NotificationSender`](crate::context::NotificationSender)
127//! via [`McpProxyBuilder::notification_sender()`] and wire it to a transport
128//! that supports notifications:
129//!
130//! ```rust,ignore
131//! use tower_mcp::context::notification_channel;
132//!
133//! let (notif_tx, notif_rx) = notification_channel(32);
134//!
135//! let proxy = McpProxy::builder("proxy", "1.0.0")
136//!     .notification_sender(notif_tx)
137//!     .backend("tools", transport).await
138//!     .build().await?;
139//!
140//! // GenericStdioTransport forwards notifications to connected clients
141//! let mut transport = tower_mcp::GenericStdioTransport::with_notifications(proxy, notif_rx);
142//! transport.run().await?;
143//! ```
144//!
145//! # Health Checks
146//!
147//! Ping all backends concurrently to verify connectivity:
148//!
149//! ```rust,ignore
150//! let health = proxy.health_check().await;
151//! for h in &health {
152//!     println!("{}: {}", h.namespace, if h.healthy { "ok" } else { "down" });
153//! }
154//! ```
155//!
156//! # Request Coalescing
157//!
158//! Use [`tower-resilience`](https://docs.rs/tower-resilience)'s `CoalesceLayer`
159//! to deduplicate concurrent identical requests. This is especially useful for
160//! list operations when multiple clients connect simultaneously:
161//!
162//! ```rust,ignore
163//! use std::mem::discriminant;
164//! use tower::Layer;
165//! use tower_resilience::coalesce::CoalesceLayer;
166//!
167//! // Coalesce by MCP method type -- concurrent list_tools calls share
168//! // a single execution, while call_tool runs independently.
169//! let coalesced = CoalesceLayer::new(|req: &RouterRequest| {
170//!     discriminant(&req.inner)
171//! }).layer(proxy);
172//! ```
173//!
174//! Note that `CoalesceLayer` changes the error type from `Infallible` to
175//! `CoalesceError<Infallible>`. Use [`CatchError`](crate::transport::CatchError)
176//! to convert back to `Infallible` for transport compatibility.
177//!
178//! # Language-Agnostic Backends
179//!
180//! The proxy communicates with backends over standard MCP (JSON-RPC), so
181//! backends can be written in any language or framework -- Python (FastMCP),
182//! TypeScript, Go, or anything that speaks the protocol. This makes
183//! tower-mcp a natural aggregation and middleware layer for polyglot MCP
184//! deployments:
185//!
186//! ```rust,ignore
187//! let proxy = McpProxy::builder("polyglot-proxy", "1.0.0")
188//!     // Python FastMCP server
189//!     .backend("ml", StdioClientTransport::spawn("python", &["-m", "ml_server"]).await?)
190//!     .await
191//!     // TypeScript MCP server
192//!     .backend("docs", StdioClientTransport::spawn("npx", &["docs-server"]).await?)
193//!     .await
194//!     // Rust tower-mcp server
195//!     .backend("data", StdioClientTransport::spawn("data-server", &[]).await?)
196//!     .await
197//!     .build().await?;
198//! ```
199
200mod backend;
201mod builder;
202mod service;
203mod tests;
204
205pub use builder::{McpProxyBuilder, ProxyBuildResult, SkippedBackend, SkippedPhase};
206pub use service::{AddBackendError, BackendHealth, McpProxy};
207
208// Re-export BackendService so users can write layer bounds against it
209pub use backend::BackendService;