Skip to main content

tower_mcp/proxy/
backend.rs

1//! Backend connection management.
2//!
3//! Each backend wraps an [`McpClient`] with cached capability discovery,
4//! namespace prefixing, and a Tower service for dispatching routed requests.
5
6use std::convert::Infallible;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10use std::task::{Context, Poll};
11
12use tokio::sync::{RwLock, mpsc};
13use tower_service::Service;
14
15use crate::client::{ClientTransport, McpClient, NotificationHandler};
16use crate::protocol::{
17    McpRequest, McpResponse, PromptDefinition, ResourceDefinition, ResourceTemplateDefinition,
18    ToolDefinition,
19};
20use crate::router::{RouterRequest, RouterResponse};
21use tower_mcp_types::JsonRpcError;
22
23type Result<T> = std::result::Result<T, crate::error::Error>;
24
25/// Cached capabilities from a backend server.
26#[derive(Debug, Default, Clone)]
27pub(crate) struct CachedCapabilities {
28    pub tools: Vec<ToolDefinition>,
29    pub resources: Vec<ResourceDefinition>,
30    pub resource_templates: Vec<ResourceTemplateDefinition>,
31    pub prompts: Vec<PromptDefinition>,
32}
33
34/// What kind of capability list changed.
35#[derive(Debug, Clone, Copy)]
36pub(crate) enum ListChanged {
37    Tools,
38    Resources,
39    Prompts,
40}
41
42/// A connected backend MCP server with namespace and cached capabilities.
43pub(crate) struct Backend {
44    /// Namespace prefix for this backend's capabilities.
45    pub namespace: String,
46    /// Separator between namespace and name (default: `_`).
47    pub separator: String,
48    /// The connected MCP client (shared with BackendService).
49    pub client: Arc<McpClient>,
50    /// Cached capabilities, refreshed on list-changed notifications.
51    pub cache: Arc<RwLock<CachedCapabilities>>,
52    /// Instructions from the backend's initialize response.
53    pub instructions: Option<String>,
54}
55
56impl Backend {
57    /// Connect a transport and create a backend with a notification handler
58    /// that sends invalidation signals on the provided channel.
59    pub async fn connect(
60        namespace: impl Into<String>,
61        transport: impl ClientTransport,
62        separator: String,
63        invalidation_tx: mpsc::Sender<ListChanged>,
64    ) -> Result<Self> {
65        let tools_tx = invalidation_tx.clone();
66        let resources_tx = invalidation_tx.clone();
67        let prompts_tx = invalidation_tx;
68
69        let handler = NotificationHandler::new()
70            .on_tools_changed(move || {
71                let _ = tools_tx.try_send(ListChanged::Tools);
72            })
73            .on_resources_changed(move || {
74                let _ = resources_tx.try_send(ListChanged::Resources);
75            })
76            .on_prompts_changed(move || {
77                let _ = prompts_tx.try_send(ListChanged::Prompts);
78            });
79
80        let client = McpClient::connect_with_handler(transport, handler).await?;
81        let cache = Arc::new(RwLock::new(CachedCapabilities::default()));
82
83        Ok(Self {
84            namespace: namespace.into(),
85            separator,
86            client: Arc::new(client),
87            cache,
88            instructions: None,
89        })
90    }
91
92    /// Create a backend from an already-connected client (no notification forwarding).
93    pub fn from_client(namespace: impl Into<String>, client: McpClient, separator: String) -> Self {
94        let instructions = client
95            .server_info_blocking()
96            .and_then(|info| info.instructions.clone());
97        Self {
98            namespace: namespace.into(),
99            separator,
100            client: Arc::new(client),
101            cache: Arc::new(RwLock::new(CachedCapabilities::default())),
102            instructions,
103        }
104    }
105
106    /// Create a [`BackendService`] for dispatching routed requests to this backend.
107    pub fn service(&self) -> BackendService {
108        BackendService {
109            client: Arc::clone(&self.client),
110        }
111    }
112
113    /// Initialize the backend: run MCP initialize handshake and discover capabilities.
114    ///
115    /// Returns the backend's instructions (if any) from the initialize response.
116    pub async fn initialize(
117        &self,
118        proxy_name: &str,
119        proxy_version: &str,
120    ) -> Result<Option<String>> {
121        let result = self.client.initialize(proxy_name, proxy_version).await?;
122        let instructions = result.instructions.clone();
123        self.refresh_capabilities().await?;
124        Ok(instructions)
125    }
126
127    /// Refresh cached capabilities from the backend.
128    pub async fn refresh_capabilities(&self) -> Result<()> {
129        let (tools, resources, templates, prompts) = tokio::join!(
130            self.client.list_all_tools(),
131            self.client.list_all_resources(),
132            self.client.list_all_resource_templates(),
133            self.client.list_all_prompts(),
134        );
135
136        let mut cache = self.cache.write().await;
137        cache.tools = tools.unwrap_or_default();
138        cache.resources = resources.unwrap_or_default();
139        cache.resource_templates = templates.unwrap_or_default();
140        cache.prompts = prompts.unwrap_or_default();
141
142        Ok(())
143    }
144
145    /// Refresh only the tools cache.
146    pub async fn refresh_tools(&self) {
147        if let Ok(tools) = self.client.list_all_tools().await {
148            self.cache.write().await.tools = tools;
149        }
150    }
151
152    /// Refresh only the resources cache.
153    pub async fn refresh_resources(&self) {
154        let (resources, templates) = tokio::join!(
155            self.client.list_all_resources(),
156            self.client.list_all_resource_templates(),
157        );
158        let mut cache = self.cache.write().await;
159        if let Ok(r) = resources {
160            cache.resources = r;
161        }
162        if let Ok(t) = templates {
163            cache.resource_templates = t;
164        }
165    }
166
167    /// Refresh only the prompts cache.
168    pub async fn refresh_prompts(&self) {
169        if let Ok(prompts) = self.client.list_all_prompts().await {
170            self.cache.write().await.prompts = prompts;
171        }
172    }
173}
174
175// =============================================================================
176// BackendService
177// =============================================================================
178
179/// A Tower [`Service`] that dispatches routed requests to a backend [`McpClient`].
180///
181/// This service handles individual requests after the proxy has already
182/// determined which backend they belong to and stripped the namespace prefix.
183/// It converts `McpClient` method calls into the `Service<RouterRequest>`
184/// interface, allowing standard Tower middleware to wrap per-backend dispatch.
185///
186/// Created via `Backend::service()` and typically wrapped with middleware
187/// before being type-erased into the proxy's backend entry.
188#[derive(Clone)]
189pub struct BackendService {
190    client: Arc<McpClient>,
191}
192
193impl Service<RouterRequest> for BackendService {
194    type Response = RouterResponse;
195    type Error = Infallible;
196    type Future =
197        Pin<Box<dyn Future<Output = std::result::Result<RouterResponse, Infallible>> + Send>>;
198
199    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
200        Poll::Ready(Ok(()))
201    }
202
203    fn call(&mut self, req: RouterRequest) -> Self::Future {
204        let client = Arc::clone(&self.client);
205        let request_id = req.id.clone();
206
207        Box::pin(async move {
208            let result = dispatch_to_client(&client, req.inner).await;
209            Ok(RouterResponse {
210                id: request_id,
211                inner: result,
212            })
213        })
214    }
215}
216
217/// Dispatch a single (namespace-stripped) MCP request to the backend client.
218async fn dispatch_to_client(
219    client: &McpClient,
220    request: McpRequest,
221) -> std::result::Result<McpResponse, JsonRpcError> {
222    match request {
223        McpRequest::CallTool(params) => {
224            let result = client
225                .call_tool(&params.name, params.arguments)
226                .await
227                .map_err(|e| JsonRpcError::internal_error(format!("Backend error: {}", e)))?;
228            Ok(McpResponse::CallTool(result))
229        }
230        McpRequest::ReadResource(params) => {
231            let result = client
232                .read_resource(&params.uri)
233                .await
234                .map_err(|e| JsonRpcError::internal_error(format!("Backend error: {}", e)))?;
235            Ok(McpResponse::ReadResource(result))
236        }
237        McpRequest::GetPrompt(params) => {
238            let args = if params.arguments.is_empty() {
239                None
240            } else {
241                Some(params.arguments)
242            };
243            let result = client
244                .get_prompt(&params.name, args)
245                .await
246                .map_err(|e| JsonRpcError::internal_error(format!("Backend error: {}", e)))?;
247            Ok(McpResponse::GetPrompt(result))
248        }
249        _ => Err(JsonRpcError::method_not_found(
250            "Method not routable to backend",
251        )),
252    }
253}