Skip to main content

tower_mcp/proxy/
service.rs

1//! Core proxy service implementing `Service<RouterRequest>`.
2
3use std::convert::Infallible;
4use std::fmt;
5use std::future::Future;
6use std::pin::Pin;
7use std::sync::{Arc, Mutex};
8use std::task::{Context, Poll};
9
10use tokio::sync::{RwLock, mpsc};
11use tower::ServiceExt;
12use tower::util::BoxCloneService;
13use tower_service::Service;
14
15use crate::client::ClientTransport;
16use crate::protocol::{
17    CallToolParams, GetPromptParams, Implementation, InitializeResult, ListPromptsResult,
18    ListResourceTemplatesResult, ListResourcesResult, ListToolsResult, McpRequest, McpResponse,
19    PromptDefinition, ReadResourceParams, RequestId, ResourceDefinition,
20    ResourceTemplateDefinition, ServerCapabilities, ToolDefinition, ToolsCapability,
21};
22use crate::router::{Extensions, RouterRequest, RouterResponse};
23use crate::transport::CatchError;
24use tower_mcp_types::JsonRpcError;
25
26use super::backend::{Backend, BackendService, CachedCapabilities, ListChanged};
27
28/// A backend entry in the proxy, combining cached capabilities with a
29/// type-erased service for dispatching routed requests.
30#[derive(Clone)]
31pub(crate) struct BackendEntry {
32    pub namespace: String,
33    pub separator: String,
34    pub cache: Arc<RwLock<CachedCapabilities>>,
35    /// Type-erased service for dispatching call_tool, read_resource, get_prompt.
36    /// Middleware layers are applied before type-erasure.
37    pub service: BoxCloneService<RouterRequest, RouterResponse, Infallible>,
38}
39
40impl BackendEntry {
41    /// Create from a Backend using its default BackendService (no middleware).
42    pub fn from_backend(backend: &Backend) -> Self {
43        Self {
44            namespace: backend.namespace.clone(),
45            separator: backend.separator.clone(),
46            cache: Arc::clone(&backend.cache),
47            service: BoxCloneService::new(backend.service()),
48        }
49    }
50
51    /// Create from a Backend with a custom (already-layered) service.
52    pub fn from_backend_with_service(
53        backend: &Backend,
54        service: BoxCloneService<RouterRequest, RouterResponse, Infallible>,
55    ) -> Self {
56        Self {
57            namespace: backend.namespace.clone(),
58            separator: backend.separator.clone(),
59            cache: Arc::clone(&backend.cache),
60            service,
61        }
62    }
63
64    /// Strip the namespace prefix from a name, if it matches.
65    fn strip_prefix<'a>(&self, name: &'a str) -> Option<&'a str> {
66        let prefix = format!("{}{}", self.namespace, self.separator);
67        name.strip_prefix(&prefix)
68    }
69
70    /// Strip the namespace prefix from a URI.
71    fn strip_uri_prefix<'a>(&self, uri: &'a str) -> Option<&'a str> {
72        let prefix = format!("{}{}", self.namespace, self.separator);
73        uri.strip_prefix(&prefix)
74    }
75}
76
77/// An MCP proxy that aggregates multiple backend servers.
78///
79/// Implements `Service<RouterRequest>` so it can be used with any tower-mcp
80/// transport (HTTP, WebSocket, stdio) and composed with tower middleware.
81///
82/// Each backend's capabilities are namespaced to avoid collisions, and
83/// individual backends can have their own Tower middleware stack applied
84/// via [`McpProxyBuilder::backend_layer()`](super::McpProxyBuilder::backend_layer).
85///
86/// Backends can be added dynamically at runtime via [`add_backend()`](Self::add_backend).
87/// All clones of the proxy share the same backend list, so additions are
88/// immediately visible to all request handlers.
89#[derive(Clone)]
90pub struct McpProxy {
91    pub(super) shared: Arc<McpProxyShared>,
92    /// Per-backend entries with type-erased services. Shared across all clones
93    /// via `Arc<Mutex<_>>`. The mutex is only held during synchronous routing
94    /// and cloning operations (microseconds), never across await points.
95    pub(super) entries: Arc<Mutex<Vec<BackendEntry>>>,
96}
97
98/// Shared, `Send + Sync` state for the proxy (no `BoxCloneService`).
99pub(super) struct McpProxyShared {
100    name: String,
101    version: String,
102    pub(super) backends: RwLock<Vec<Backend>>,
103    /// Optional sender for forwarding list-changed notifications downstream.
104    pub(super) notification_tx: Option<crate::context::NotificationSender>,
105    /// Aggregated or custom instructions for the initialize response.
106    instructions: Option<String>,
107    /// Separator used for namespace prefixing.
108    separator: String,
109}
110
111/// Health status of a single backend.
112#[derive(Debug, Clone)]
113pub struct BackendHealth {
114    /// The backend's namespace.
115    pub namespace: String,
116    /// Whether the backend responded to a ping.
117    pub healthy: bool,
118}
119
120/// Namespace + cache extracted from a `BackendEntry` for `Send`-safe async use.
121/// Both fields are `Send + Sync`, so futures holding these can cross thread boundaries.
122struct EntryInfo {
123    namespace: String,
124    separator: String,
125    cache: Arc<RwLock<CachedCapabilities>>,
126}
127
128/// Error returned when adding a dynamic backend fails.
129#[derive(Debug)]
130pub enum AddBackendError {
131    /// The namespace is already in use by an existing backend.
132    DuplicateNamespace(String),
133    /// The namespace would create an ambiguous prefix with an existing backend.
134    AmbiguousPrefix {
135        /// Namespace requested for the backend being added.
136        new_namespace: String,
137        /// Existing namespace whose prefix would conflict.
138        existing_namespace: String,
139    },
140    /// Failed to connect the transport.
141    Connect(crate::error::Error),
142    /// Failed during MCP initialization handshake.
143    Initialize(crate::error::Error),
144}
145
146impl fmt::Display for AddBackendError {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        match self {
149            Self::DuplicateNamespace(ns) => write!(f, "namespace \"{}\" already exists", ns),
150            Self::AmbiguousPrefix {
151                new_namespace,
152                existing_namespace,
153            } => write!(
154                f,
155                "namespace \"{}\" creates ambiguous prefix with \"{}\"",
156                new_namespace, existing_namespace
157            ),
158            Self::Connect(e) => write!(f, "failed to connect: {}", e),
159            Self::Initialize(e) => write!(f, "failed to initialize: {}", e),
160        }
161    }
162}
163
164impl std::error::Error for AddBackendError {}
165
166impl McpProxy {
167    /// Create a builder for configuring the proxy.
168    pub fn builder(name: impl Into<String>, version: impl Into<String>) -> super::McpProxyBuilder {
169        super::McpProxyBuilder::new(name, version)
170    }
171
172    /// Create a new proxy with the given backends and entries (called by builder).
173    pub(crate) fn new(
174        name: String,
175        version: String,
176        backends: Vec<Backend>,
177        entries: Vec<BackendEntry>,
178        notification_tx: Option<crate::context::NotificationSender>,
179        instructions: Option<String>,
180        separator: String,
181    ) -> Self {
182        Self {
183            shared: Arc::new(McpProxyShared {
184                name,
185                version,
186                backends: RwLock::new(backends),
187                notification_tx,
188                instructions,
189                separator,
190            }),
191            entries: Arc::new(Mutex::new(entries)),
192        }
193    }
194
195    /// Add a backend dynamically from a [`ClientTransport`].
196    ///
197    /// The transport is connected, the MCP initialize handshake runs, and
198    /// capabilities are discovered. The new backend is immediately available
199    /// to all clones of this proxy.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if:
204    /// - The namespace is already in use
205    /// - The namespace creates an ambiguous prefix with an existing backend
206    /// - The transport fails to connect
207    /// - The MCP initialize handshake fails
208    ///
209    /// # Example
210    ///
211    /// ```rust,ignore
212    /// proxy.add_backend("new-db", StdioClientTransport::spawn("db-server", &[]).await?).await?;
213    /// // New backend is immediately available for requests
214    /// ```
215    pub async fn add_backend(
216        &self,
217        namespace: impl Into<String>,
218        transport: impl ClientTransport,
219    ) -> Result<(), AddBackendError> {
220        let namespace = namespace.into();
221        let separator = self.shared.separator.clone();
222
223        // Validate namespace uniqueness and prefix ambiguity
224        {
225            let entries = self.entries.lock().expect("entries lock poisoned");
226            self.validate_namespace(&namespace, &separator, &entries)?;
227        }
228
229        // Connect and initialize (no lock held during async operations)
230        let (invalidation_tx, invalidation_rx) = mpsc::channel(16);
231        let backend = Backend::connect(namespace.clone(), transport, separator, invalidation_tx)
232            .await
233            .map_err(AddBackendError::Connect)?;
234
235        backend
236            .initialize(&self.shared.name, &self.shared.version)
237            .await
238            .map_err(AddBackendError::Initialize)?;
239
240        let entry = BackendEntry::from_backend(&backend);
241
242        // Add to shared state
243        {
244            let mut entries = self.entries.lock().expect("entries lock poisoned");
245            entries.push(entry);
246        }
247
248        // Spawn invalidation watcher
249        let backend_idx = {
250            let mut backends = self.shared.backends.write().await;
251            let idx = backends.len();
252            backends.push(backend);
253            idx
254        };
255
256        self.spawn_invalidation_watcher(backend_idx, invalidation_rx);
257
258        // Notify downstream clients that capabilities changed
259        self.notify_all_changed().await;
260
261        Ok(())
262    }
263
264    /// Add a backend dynamically with a custom middleware-wrapped service.
265    ///
266    /// Like [`add_backend()`](Self::add_backend), but applies a Tower layer
267    /// to the backend's dispatch service before adding it.
268    ///
269    /// # Example
270    ///
271    /// ```rust,ignore
272    /// use std::time::Duration;
273    /// use tower::timeout::TimeoutLayer;
274    ///
275    /// proxy.add_backend_with_layer(
276    ///     "slow-api",
277    ///     transport,
278    ///     TimeoutLayer::new(Duration::from_secs(60)),
279    /// ).await?;
280    /// ```
281    pub async fn add_backend_with_layer<L>(
282        &self,
283        namespace: impl Into<String>,
284        transport: impl ClientTransport,
285        layer: L,
286    ) -> Result<(), AddBackendError>
287    where
288        L: tower::Layer<BackendService> + Send + 'static,
289        L::Service: Service<RouterRequest, Response = RouterResponse> + Clone + Send + 'static,
290        <L::Service as Service<RouterRequest>>::Error: fmt::Display + Send,
291        <L::Service as Service<RouterRequest>>::Future: Send,
292    {
293        let namespace = namespace.into();
294        let separator = self.shared.separator.clone();
295
296        {
297            let entries = self.entries.lock().expect("entries lock poisoned");
298            self.validate_namespace(&namespace, &separator, &entries)?;
299        }
300
301        let (invalidation_tx, invalidation_rx) = mpsc::channel(16);
302        let backend = Backend::connect(namespace.clone(), transport, separator, invalidation_tx)
303            .await
304            .map_err(AddBackendError::Connect)?;
305
306        backend
307            .initialize(&self.shared.name, &self.shared.version)
308            .await
309            .map_err(AddBackendError::Initialize)?;
310
311        // Apply middleware layer
312        let base = backend.service();
313        let layered = layer.layer(base);
314        let caught = CatchError::new(layered);
315        let service = BoxCloneService::new(caught);
316        let entry = BackendEntry::from_backend_with_service(&backend, service);
317
318        {
319            let mut entries = self.entries.lock().expect("entries lock poisoned");
320            entries.push(entry);
321        }
322
323        let backend_idx = {
324            let mut backends = self.shared.backends.write().await;
325            let idx = backends.len();
326            backends.push(backend);
327            idx
328        };
329
330        self.spawn_invalidation_watcher(backend_idx, invalidation_rx);
331
332        if let Some(tx) = &self.shared.notification_tx {
333            let _ = tx
334                .send(crate::context::ServerNotification::ToolsListChanged)
335                .await;
336            let _ = tx
337                .send(crate::context::ServerNotification::ResourcesListChanged)
338                .await;
339            let _ = tx
340                .send(crate::context::ServerNotification::PromptsListChanged)
341                .await;
342        }
343
344        Ok(())
345    }
346
347    /// Validate that a namespace can be added without conflicts.
348    fn validate_namespace(
349        &self,
350        namespace: &str,
351        separator: &str,
352        entries: &[BackendEntry],
353    ) -> Result<(), AddBackendError> {
354        let new_prefix = format!("{}{}", namespace, separator);
355
356        for entry in entries {
357            if entry.namespace == namespace {
358                return Err(AddBackendError::DuplicateNamespace(namespace.to_string()));
359            }
360            let existing_prefix = format!("{}{}", entry.namespace, entry.separator);
361            if new_prefix.starts_with(&existing_prefix) || existing_prefix.starts_with(&new_prefix)
362            {
363                return Err(AddBackendError::AmbiguousPrefix {
364                    new_namespace: namespace.to_string(),
365                    existing_namespace: entry.namespace.clone(),
366                });
367            }
368        }
369        Ok(())
370    }
371
372    /// Spawn a background task that watches for list-changed notifications
373    /// from a backend and refreshes the cache.
374    pub(super) fn spawn_invalidation_watcher(
375        &self,
376        backend_idx: usize,
377        mut rx: mpsc::Receiver<ListChanged>,
378    ) {
379        let shared = Arc::clone(&self.shared);
380        tokio::spawn(async move {
381            while let Some(changed) = rx.recv().await {
382                let backends = shared.backends.read().await;
383                let Some(backend) = backends.get(backend_idx) else {
384                    break;
385                };
386                tracing::debug!(
387                    namespace = %backend.namespace,
388                    kind = ?changed,
389                    "Backend list changed, refreshing cache"
390                );
391                match changed {
392                    ListChanged::Tools => {
393                        backend.refresh_tools().await;
394                        if let Some(tx) = &shared.notification_tx {
395                            let _ = tx
396                                .send(crate::context::ServerNotification::ToolsListChanged)
397                                .await;
398                        }
399                    }
400                    ListChanged::Resources => {
401                        backend.refresh_resources().await;
402                        if let Some(tx) = &shared.notification_tx {
403                            let _ = tx
404                                .send(crate::context::ServerNotification::ResourcesListChanged)
405                                .await;
406                        }
407                    }
408                    ListChanged::Prompts => {
409                        backend.refresh_prompts().await;
410                        if let Some(tx) = &shared.notification_tx {
411                            let _ = tx
412                                .send(crate::context::ServerNotification::PromptsListChanged)
413                                .await;
414                        }
415                    }
416                }
417            }
418        });
419    }
420
421    /// Remove a backend by namespace name.
422    ///
423    /// The backend's tools, resources, and prompts are immediately removed from
424    /// aggregated lists. Downstream clients are notified via list-changed
425    /// notifications.
426    ///
427    /// Returns `true` if the backend was found and removed, `false` if no
428    /// backend with that namespace exists.
429    ///
430    /// # Example
431    ///
432    /// ```rust,ignore
433    /// if proxy.remove_backend("old-db").await {
434    ///     println!("Backend removed");
435    /// }
436    /// ```
437    pub async fn remove_backend(&self, namespace: &str) -> bool {
438        // Remove from entries (synchronous)
439        let found = {
440            let mut entries = self.entries.lock().expect("entries lock poisoned");
441            let before = entries.len();
442            entries.retain(|e| e.namespace != namespace);
443            entries.len() < before
444        };
445
446        if !found {
447            return false;
448        }
449
450        // Remove from backends (async)
451        // Dropping the Backend drops its invalidation_tx sender, which
452        // causes the spawned invalidation watcher to exit naturally.
453        {
454            let mut backends = self.shared.backends.write().await;
455            backends.retain(|b| b.namespace != namespace);
456        }
457
458        // Notify downstream clients that capabilities changed
459        self.notify_all_changed().await;
460
461        tracing::info!(namespace, "Backend removed");
462        true
463    }
464
465    /// Replace a backend by removing the old one and adding a new one with
466    /// the same namespace.
467    ///
468    /// This is a convenience for `remove_backend()` + `add_backend()`. If
469    /// the add fails, the old backend is already gone (no rollback).
470    ///
471    /// # Example
472    ///
473    /// ```rust,ignore
474    /// proxy.replace_backend("db", new_transport).await?;
475    /// ```
476    pub async fn replace_backend(
477        &self,
478        namespace: impl Into<String>,
479        transport: impl ClientTransport,
480    ) -> Result<(), AddBackendError> {
481        let namespace = namespace.into();
482        self.remove_backend(&namespace).await;
483        self.add_backend(namespace, transport).await
484    }
485
486    /// Return the namespaces of all currently registered backends.
487    ///
488    /// # Example
489    ///
490    /// ```rust,ignore
491    /// let namespaces = proxy.backend_namespaces();
492    /// println!("Backends: {:?}", namespaces);
493    /// ```
494    pub fn backend_namespaces(&self) -> Vec<String> {
495        let entries = self.entries.lock().expect("entries lock poisoned");
496        entries.iter().map(|e| e.namespace.clone()).collect()
497    }
498
499    /// Return the number of currently registered backends.
500    pub fn backend_count(&self) -> usize {
501        self.entries.lock().expect("entries lock poisoned").len()
502    }
503
504    /// Check the health of all backends by pinging them concurrently.
505    ///
506    /// Returns a map of namespace to health status. Backends that respond
507    /// to ping within a reasonable time are considered healthy.
508    pub async fn health_check(&self) -> Vec<BackendHealth> {
509        let backends = self.shared.backends.read().await;
510        let futures: Vec<_> = backends
511            .iter()
512            .map(|backend| {
513                let client = Arc::clone(&backend.client);
514                let namespace = backend.namespace.clone();
515                async move {
516                    let healthy = client.ping().await.is_ok();
517                    BackendHealth { namespace, healthy }
518                }
519            })
520            .collect();
521        drop(backends);
522
523        futures::future::join_all(futures).await
524    }
525
526    /// Notify downstream clients that all capability lists have changed.
527    async fn notify_all_changed(&self) {
528        if let Some(tx) = &self.shared.notification_tx {
529            let _ = tx
530                .send(crate::context::ServerNotification::ToolsListChanged)
531                .await;
532            let _ = tx
533                .send(crate::context::ServerNotification::ResourcesListChanged)
534                .await;
535            let _ = tx
536                .send(crate::context::ServerNotification::PromptsListChanged)
537                .await;
538        }
539    }
540
541    /// Extract Send-safe info from entries (synchronous, no borrow across await).
542    fn entry_infos(entries: &[BackendEntry]) -> Vec<EntryInfo> {
543        entries
544            .iter()
545            .map(|e| EntryInfo {
546                namespace: e.namespace.clone(),
547                separator: e.separator.clone(),
548                cache: Arc::clone(&e.cache),
549            })
550            .collect()
551    }
552
553    /// Route a namespaced name to the correct backend index + stripped name.
554    fn route_by_prefix(entries: &[BackendEntry], name: &str) -> Option<(usize, String)> {
555        for (i, entry) in entries.iter().enumerate() {
556            if let Some(stripped) = entry.strip_prefix(name) {
557                return Some((i, stripped.to_string()));
558            }
559        }
560        None
561    }
562
563    /// Route a namespaced URI to the correct backend index + stripped URI.
564    fn route_by_uri_prefix(entries: &[BackendEntry], uri: &str) -> Option<(usize, String)> {
565        for (i, entry) in entries.iter().enumerate() {
566            if let Some(stripped) = entry.strip_uri_prefix(uri) {
567                return Some((i, stripped.to_string()));
568            }
569        }
570        None
571    }
572}
573
574impl EntryInfo {
575    fn prefixed_name(&self, name: &str) -> String {
576        format!("{}{}{}", self.namespace, self.separator, name)
577    }
578
579    fn prefixed_uri(&self, uri: &str) -> String {
580        format!("{}{}{}", self.namespace, self.separator, uri)
581    }
582}
583
584// =========================================================================
585// Async handlers that only touch Send+Sync data (no BackendEntry references
586// held across .await points).
587// =========================================================================
588
589async fn handle_initialize(
590    name: String,
591    version: String,
592    instructions: Option<String>,
593) -> Result<McpResponse, JsonRpcError> {
594    Ok(McpResponse::Initialize(InitializeResult {
595        protocol_version: "2025-11-25".to_string(),
596        server_info: Implementation {
597            name,
598            version,
599            title: None,
600            description: None,
601            icons: None,
602            website_url: None,
603            meta: None,
604        },
605        capabilities: ServerCapabilities {
606            tools: Some(ToolsCapability { list_changed: true }),
607            resources: None,
608            prompts: None,
609            logging: None,
610            tasks: None,
611            completions: None,
612            experimental: None,
613            extensions: None,
614        },
615        instructions,
616        meta: None,
617    }))
618}
619
620async fn handle_list_tools(infos: Vec<EntryInfo>) -> Result<McpResponse, JsonRpcError> {
621    let mut tools = Vec::new();
622    for info in &infos {
623        let cache = info.cache.read().await;
624        for t in &cache.tools {
625            let mut def: ToolDefinition = t.clone();
626            def.name = info.prefixed_name(&def.name);
627            tools.push(def);
628        }
629    }
630    Ok(McpResponse::ListTools(ListToolsResult {
631        tools,
632        next_cursor: None,
633        ttl_ms: None,
634        cache_scope: None,
635        meta: None,
636    }))
637}
638
639async fn handle_call_tool(
640    service: BoxCloneService<RouterRequest, RouterResponse, Infallible>,
641    stripped_name: String,
642    params: CallToolParams,
643    id: RequestId,
644    extensions: Extensions,
645) -> Result<McpResponse, JsonRpcError> {
646    let inner_request = McpRequest::CallTool(CallToolParams {
647        name: stripped_name,
648        arguments: params.arguments,
649        input_responses: params.input_responses,
650        request_state: params.request_state,
651        meta: params.meta,
652        task: params.task,
653    });
654
655    let router_req = RouterRequest {
656        id,
657        inner: inner_request,
658        extensions,
659    };
660
661    let resp = service.oneshot(router_req).await.expect("infallible");
662    resp.inner
663}
664
665async fn handle_list_resources(infos: Vec<EntryInfo>) -> Result<McpResponse, JsonRpcError> {
666    let mut resources = Vec::new();
667    for info in &infos {
668        let cache = info.cache.read().await;
669        for r in &cache.resources {
670            let mut def: ResourceDefinition = r.clone();
671            def.uri = info.prefixed_uri(&def.uri);
672            def.name = info.prefixed_name(&def.name);
673            resources.push(def);
674        }
675    }
676    Ok(McpResponse::ListResources(ListResourcesResult {
677        resources,
678        next_cursor: None,
679        ttl_ms: None,
680        cache_scope: None,
681        meta: None,
682    }))
683}
684
685async fn handle_list_resource_templates(
686    infos: Vec<EntryInfo>,
687) -> Result<McpResponse, JsonRpcError> {
688    let mut resource_templates = Vec::new();
689    for info in &infos {
690        let cache = info.cache.read().await;
691        for rt in &cache.resource_templates {
692            let mut def: ResourceTemplateDefinition = rt.clone();
693            def.uri_template = info.prefixed_uri(&def.uri_template);
694            def.name = info.prefixed_name(&def.name);
695            resource_templates.push(def);
696        }
697    }
698    Ok(McpResponse::ListResourceTemplates(
699        ListResourceTemplatesResult {
700            resource_templates,
701            next_cursor: None,
702            ttl_ms: None,
703            cache_scope: None,
704            meta: None,
705        },
706    ))
707}
708
709async fn handle_read_resource(
710    service: BoxCloneService<RouterRequest, RouterResponse, Infallible>,
711    stripped_uri: String,
712    params: ReadResourceParams,
713    id: RequestId,
714    extensions: Extensions,
715) -> Result<McpResponse, JsonRpcError> {
716    let inner_request = McpRequest::ReadResource(ReadResourceParams {
717        uri: stripped_uri,
718        input_responses: params.input_responses,
719        request_state: params.request_state,
720        meta: params.meta,
721    });
722
723    let router_req = RouterRequest {
724        id,
725        inner: inner_request,
726        extensions,
727    };
728
729    let resp = service.oneshot(router_req).await.expect("infallible");
730    resp.inner
731}
732
733async fn handle_list_prompts(infos: Vec<EntryInfo>) -> Result<McpResponse, JsonRpcError> {
734    let mut prompts = Vec::new();
735    for info in &infos {
736        let cache = info.cache.read().await;
737        for p in &cache.prompts {
738            let mut def: PromptDefinition = p.clone();
739            def.name = info.prefixed_name(&def.name);
740            prompts.push(def);
741        }
742    }
743    Ok(McpResponse::ListPrompts(ListPromptsResult {
744        prompts,
745        next_cursor: None,
746        ttl_ms: None,
747        cache_scope: None,
748        meta: None,
749    }))
750}
751
752async fn handle_get_prompt(
753    service: BoxCloneService<RouterRequest, RouterResponse, Infallible>,
754    stripped_name: String,
755    params: GetPromptParams,
756    id: RequestId,
757    extensions: Extensions,
758) -> Result<McpResponse, JsonRpcError> {
759    let inner_request = McpRequest::GetPrompt(GetPromptParams {
760        name: stripped_name,
761        arguments: params.arguments,
762        input_responses: params.input_responses,
763        request_state: params.request_state,
764        meta: params.meta,
765    });
766
767    let router_req = RouterRequest {
768        id,
769        inner: inner_request,
770        extensions,
771    };
772
773    let resp = service.oneshot(router_req).await.expect("infallible");
774    resp.inner
775}
776
777// =========================================================================
778// Service implementation
779// =========================================================================
780
781impl Service<RouterRequest> for McpProxy {
782    type Response = RouterResponse;
783    type Error = Infallible;
784    type Future = Pin<Box<dyn Future<Output = Result<RouterResponse, Infallible>> + Send>>;
785
786    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
787        Poll::Ready(Ok(()))
788    }
789
790    fn call(&mut self, req: RouterRequest) -> Self::Future {
791        let request_id = req.id.clone();
792        let extensions = req.extensions.clone();
793
794        // Lock entries for synchronous routing and data extraction.
795        // The lock is held only for cloning services and extracting EntryInfo —
796        // no await points occur while the lock is held.
797        let entries = self.entries.lock().expect("entries lock poisoned");
798
799        let result_future: Pin<Box<dyn Future<Output = Result<McpResponse, JsonRpcError>> + Send>> =
800            match req.inner {
801                McpRequest::Initialize(_params) => {
802                    let name = self.shared.name.clone();
803                    let version = self.shared.version.clone();
804                    let instructions = self.shared.instructions.clone();
805                    Box::pin(handle_initialize(name, version, instructions))
806                }
807                McpRequest::Ping => Box::pin(async { Ok(McpResponse::Pong(Default::default())) }),
808                McpRequest::ListTools(_params) => {
809                    let infos = Self::entry_infos(&entries);
810                    Box::pin(handle_list_tools(infos))
811                }
812                McpRequest::CallTool(params) => {
813                    match Self::route_by_prefix(&entries, &params.name) {
814                        Some((idx, stripped)) => {
815                            let service = entries[idx].service.clone();
816                            Box::pin(handle_call_tool(
817                                service,
818                                stripped,
819                                params,
820                                request_id.clone(),
821                                extensions.clone(),
822                            ))
823                        }
824                        None => Box::pin(async move {
825                            Err(JsonRpcError::invalid_params(format!(
826                                "Unknown tool: {}",
827                                params.name
828                            )))
829                        }),
830                    }
831                }
832                McpRequest::ListResources(_params) => {
833                    let infos = Self::entry_infos(&entries);
834                    Box::pin(handle_list_resources(infos))
835                }
836                McpRequest::ListResourceTemplates(_params) => {
837                    let infos = Self::entry_infos(&entries);
838                    Box::pin(handle_list_resource_templates(infos))
839                }
840                McpRequest::ReadResource(params) => {
841                    match Self::route_by_uri_prefix(&entries, &params.uri) {
842                        Some((idx, stripped)) => {
843                            let service = entries[idx].service.clone();
844                            Box::pin(handle_read_resource(
845                                service,
846                                stripped,
847                                params,
848                                request_id.clone(),
849                                extensions.clone(),
850                            ))
851                        }
852                        None => Box::pin(async move {
853                            Err(JsonRpcError::invalid_params(format!(
854                                "Unknown resource: {}",
855                                params.uri
856                            )))
857                        }),
858                    }
859                }
860                McpRequest::ListPrompts(_params) => {
861                    let infos = Self::entry_infos(&entries);
862                    Box::pin(handle_list_prompts(infos))
863                }
864                McpRequest::GetPrompt(params) => {
865                    match Self::route_by_prefix(&entries, &params.name) {
866                        Some((idx, stripped)) => {
867                            let service = entries[idx].service.clone();
868                            Box::pin(handle_get_prompt(
869                                service,
870                                stripped,
871                                params,
872                                request_id.clone(),
873                                extensions.clone(),
874                            ))
875                        }
876                        None => Box::pin(async move {
877                            Err(JsonRpcError::invalid_params(format!(
878                                "Unknown prompt: {}",
879                                params.name
880                            )))
881                        }),
882                    }
883                }
884                _ => Box::pin(async {
885                    Err(JsonRpcError::method_not_found(
886                        "Method not supported by proxy",
887                    ))
888                }),
889            };
890
891        // Drop the lock before returning the future
892        drop(entries);
893
894        Box::pin(async move {
895            let result = result_future.await;
896            Ok(RouterResponse {
897                id: request_id,
898                inner: result,
899            })
900        })
901    }
902}