Skip to main content

tower_mcp/client/
channel.rs

1//! In-process channel transport for connecting an [`McpClient`] to an [`McpRouter`].
2//!
3//! This transport bridges client and server in the same process without
4//! any network or subprocess overhead. It is useful for testing (e.g.,
5//! proxy tests) and for in-process composition, where a co-located client
6//! (a REPL, an editor integration, an orchestrator) drives a router living
7//! in the same process.
8//!
9//! Server notifications emitted through the router's notification sender
10//! (progress, log messages, list-changed) are serialized into JSON-RPC
11//! notification frames and interleaved into [`recv`](ClientTransport::recv),
12//! so a [`NotificationHandler`](crate::client::NotificationHandler) works
13//! identically to the network transports. Requests are processed
14//! concurrently: a slow tool call does not block other requests on the
15//! transport (the client correlates responses by request id).
16//!
17//! # Example
18//!
19//! ```rust,no_run
20//! use tower_mcp::client::{McpClient, ChannelTransport};
21//! use tower_mcp::McpRouter;
22//!
23//! # async fn example() -> Result<(), tower_mcp::BoxError> {
24//! let router = McpRouter::new().server_info("backend", "1.0.0");
25//! let transport = ChannelTransport::new(router);
26//! let client = McpClient::connect(transport).await?;
27//! client.initialize("my-client", "1.0.0").await?;
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! # Host-pushed notifications
33//!
34//! When the host process wants to push notifications from its own tasks
35//! (mirroring [`HttpTransport::with_notifications`]), it keeps the sender
36//! and hands the receiver to the transport:
37//!
38//! ```rust,no_run
39//! use tower_mcp::client::{McpClient, ChannelTransport};
40//! use tower_mcp::context::notification_channel;
41//! use tower_mcp::McpRouter;
42//!
43//! # async fn example() -> Result<(), tower_mcp::BoxError> {
44//! let (notif_tx, notif_rx) = notification_channel(64);
45//! let router = McpRouter::new()
46//!     .server_info("backend", "1.0.0")
47//!     .with_notification_sender(notif_tx.clone());
48//!
49//! let transport = ChannelTransport::with_notifications(router, notif_rx);
50//! let client = McpClient::connect(transport).await?;
51//!
52//! // Elsewhere in the host process:
53//! // notif_tx.send(ServerNotification::ToolsListChanged).await.ok();
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! [`HttpTransport::with_notifications`]: crate::transport::HttpTransport::with_notifications
59
60use async_trait::async_trait;
61use tokio::sync::mpsc;
62
63#[cfg(feature = "stateless")]
64use std::sync::{Arc, Mutex as StdMutex};
65
66use crate::context::{NotificationReceiver, notification_channel};
67use crate::error::Result;
68use crate::jsonrpc::JsonRpcService;
69use crate::protocol::{JsonRpcRequest, JsonRpcResponse, McpNotification};
70use crate::router::{McpRouter, RouterRequest, RouterResponse};
71use crate::transport::service::{CatchError, InjectAnnotations};
72#[cfg(feature = "stateless")]
73use crate::transport::stdio::{StdioSubscriptionInput, StdioSubscriptions};
74use tower_service::Service;
75
76use super::transport::ClientTransport;
77
78/// An in-process [`ClientTransport`] that connects directly to an [`McpRouter`].
79///
80/// Messages are passed through tokio channels: background tasks feed
81/// incoming JSON-RPC requests to a [`JsonRpcService<McpRouter>`] (one spawned
82/// task per request, so calls run concurrently) and pump server
83/// notifications into the response stream.
84pub struct ChannelTransport {
85    /// Send raw JSON messages to the server task.
86    request_tx: mpsc::Sender<String>,
87    /// Receive raw JSON responses and notification frames from the server tasks.
88    response_rx: mpsc::Receiver<String>,
89    connected: bool,
90}
91
92impl ChannelTransport {
93    /// Create a new channel transport backed by the given router.
94    ///
95    /// Wires an internal notification channel into the router, so
96    /// notifications emitted during request handling (progress, log
97    /// messages, list-changed) are delivered to the client. To push
98    /// notifications from the host process's own tasks, use
99    /// [`with_notifications`](Self::with_notifications) instead.
100    ///
101    /// Note: this overwrites any notification sender previously set on the
102    /// router, matching the transport-owns-the-channel behavior of
103    /// [`HttpTransport::new`](crate::transport::HttpTransport::new).
104    pub fn new(router: McpRouter) -> Self {
105        let (notification_tx, notification_rx) = notification_channel(64);
106        let router = router.with_notification_sender(notification_tx);
107        Self::with_notifications(router, notification_rx)
108    }
109
110    /// Create a channel transport with a caller-owned notification receiver.
111    ///
112    /// Mirrors [`HttpTransport::with_notifications`]: the host process keeps
113    /// the sender (its own clone from [`notification_channel`], or via
114    /// [`McpRouter::notification_sender`]) and pushes
115    /// [`ServerNotification`](crate::context::ServerNotification)s from its
116    /// own tasks; the transport serializes them into JSON-RPC notification
117    /// frames and interleaves them into [`recv`](ClientTransport::recv).
118    ///
119    /// The router passed here should already carry the matching sender (see
120    /// the module-level example) so notifications emitted during request
121    /// handling flow through the same channel.
122    ///
123    /// [`HttpTransport::with_notifications`]: crate::transport::HttpTransport::with_notifications
124    pub fn with_notifications(router: McpRouter, notification_rx: NotificationReceiver) -> Self {
125        let service = JsonRpcService::new(router.clone());
126        Self::spawn_with_service(router, service, notification_rx)
127    }
128
129    /// Create a channel transport whose dispatch runs through a Tower layer.
130    ///
131    /// The channel counterpart of [`StdioTransport::layer`]: the layer wraps
132    /// the router's dispatch service, so standard middleware (timeout, rate
133    /// limit, tracing, audit) observes every JSON-RPC request an
134    /// [`McpClient`](crate::client::McpClient) makes in-process, exactly as
135    /// it would over stdio or HTTP. Layers that produce errors are wrapped
136    /// with [`CatchError`] and tool-annotation injection is preserved.
137    ///
138    /// `subscriptions/listen` remains transport-owned on every transport and
139    /// does not pass through the layer (#1182 tracks that boundary).
140    ///
141    /// [`StdioTransport::layer`]: crate::transport::StdioTransport::layer
142    pub fn layer<L>(router: McpRouter, layer: L) -> Self
143    where
144        L: tower::Layer<McpRouter>,
145        L::Service: Service<RouterRequest, Response = RouterResponse> + Clone + Send + 'static,
146        <L::Service as Service<RouterRequest>>::Error: std::fmt::Display + Send,
147        <L::Service as Service<RouterRequest>>::Future: Send,
148    {
149        let (notification_tx, notification_rx) = notification_channel(64);
150        let router = router.with_notification_sender(notification_tx);
151        Self::layer_with_notifications(router, layer, notification_rx)
152    }
153
154    /// [`layer`](Self::layer) with a caller-owned notification receiver, the
155    /// layered counterpart of [`with_notifications`](Self::with_notifications).
156    pub fn layer_with_notifications<L>(
157        router: McpRouter,
158        layer: L,
159        notification_rx: NotificationReceiver,
160    ) -> Self
161    where
162        L: tower::Layer<McpRouter>,
163        L::Service: Service<RouterRequest, Response = RouterResponse> + Clone + Send + 'static,
164        <L::Service as Service<RouterRequest>>::Error: std::fmt::Display + Send,
165        <L::Service as Service<RouterRequest>>::Future: Send,
166    {
167        let annotations = router.tool_annotations_map();
168        let wrapped = layer.layer(router.clone());
169        let service = InjectAnnotations::new(CatchError::new(wrapped), annotations);
170        Self::spawn_with_service(router, JsonRpcService::new(service), notification_rx)
171    }
172
173    /// Spawn the request and notification loops over an arbitrary dispatch
174    /// service.
175    ///
176    /// The router is retained alongside the service for transport metadata
177    /// only: server identity and tasks opt-in for subscription handling, and
178    /// the `notifications/initialized` forward. Requests dispatch through
179    /// `service`, never through the router directly, so a wrapping layer sees
180    /// every request.
181    fn spawn_with_service<S>(
182        router: McpRouter,
183        service: JsonRpcService<S>,
184        mut notification_rx: NotificationReceiver,
185    ) -> Self
186    where
187        S: Service<RouterRequest, Response = RouterResponse, Error = std::convert::Infallible>
188            + Clone
189            + Send
190            + 'static,
191        S::Future: Send,
192    {
193        let (request_tx, mut request_rx) = mpsc::channel::<String>(64);
194        let (response_tx, response_rx) = mpsc::channel::<String>(64);
195
196        #[cfg(feature = "stateless")]
197        let subscriptions = Arc::new(StdMutex::new(
198            StdioSubscriptions::new(Some(router.implementation()))
199                .with_observer(router.subscription_observer()),
200        ));
201
202        // Notification pump: serialize ServerNotifications into JSON-RPC
203        // notification frames on the shared response stream.
204        let notification_out = response_tx.clone();
205        #[cfg(feature = "stateless")]
206        let notification_subscriptions = subscriptions.clone();
207        tokio::spawn(async move {
208            while let Some(notification) = notification_rx.recv().await {
209                #[cfg(feature = "stateless")]
210                {
211                    let frames = notification_subscriptions
212                        .lock()
213                        .ok()
214                        .and_then(|subscriptions| subscriptions.route_notification(&notification));
215                    if let Some(frames) = frames {
216                        for frame in frames {
217                            if notification_out.send(frame).await.is_err() {
218                                return;
219                            }
220                        }
221                        continue;
222                    }
223                }
224                if let Some(json) = crate::transport::stdio::serialize_notification(&notification)
225                    && notification_out.send(json).await.is_err()
226                {
227                    break; // Client dropped
228                }
229            }
230        });
231
232        tokio::spawn(async move {
233            while let Some(raw_request) = request_rx.recv().await {
234                // Notifications carry no `id`, so they cannot parse as
235                // JsonRpcRequest (whose id is required). Inspect the raw
236                // frame first and handle them by method.
237                let parsed: serde_json::Value = match serde_json::from_str(&raw_request) {
238                    Ok(v) => v,
239                    Err(e) => {
240                        tracing::error!("ChannelTransport: failed to parse frame: {}", e);
241                        continue;
242                    }
243                };
244                #[cfg(feature = "stateless")]
245                {
246                    // The registry lock is never held across an await: the
247                    // dispatch through the service happens between the
248                    // handle_input and complete_listen critical sections.
249                    let handled = subscriptions
250                        .lock()
251                        .ok()
252                        .map(|mut subscriptions| subscriptions.handle_input(&service, &parsed));
253                    match handled {
254                        Some(Ok(StdioSubscriptionInput::Handled(frames))) => {
255                            for frame in frames {
256                                if response_tx.send(frame).await.is_err() {
257                                    return;
258                                }
259                            }
260                            continue;
261                        }
262                        Some(Ok(StdioSubscriptionInput::Dispatch(request))) => {
263                            let request_id = request.id.clone();
264                            let response = crate::transport::stdio::dispatch_listen_request(
265                                &mut service.clone(),
266                                *request,
267                                request_id.clone(),
268                            )
269                            .await;
270                            let frames = subscriptions.lock().ok().map(|mut subscriptions| {
271                                subscriptions.complete_listen(request_id, &response)
272                            });
273                            if let Some(Ok(frames)) = frames {
274                                for frame in frames {
275                                    if response_tx.send(frame).await.is_err() {
276                                        return;
277                                    }
278                                }
279                            }
280                            continue;
281                        }
282                        _ => {}
283                    }
284                }
285                if parsed.get("id").is_none() {
286                    if parsed.get("method").and_then(|m| m.as_str())
287                        == Some("notifications/initialized")
288                    {
289                        router.handle_notification(McpNotification::Initialized);
290                    }
291                    // No response for notifications
292                    continue;
293                }
294
295                let req: JsonRpcRequest = match serde_json::from_value(parsed) {
296                    Ok(r) => r,
297                    Err(e) => {
298                        tracing::error!("ChannelTransport: failed to parse request: {}", e);
299                        continue;
300                    }
301                };
302
303                // Process each request in its own task so a slow call does
304                // not block the transport. The client correlates responses
305                // by request id, so completion order does not matter.
306                let mut service = service.clone();
307                let response_out = response_tx.clone();
308                tokio::spawn(async move {
309                    let response = service.call_single(req).await;
310
311                    let json = match response {
312                        Ok(resp) => match serde_json::to_string(&resp) {
313                            Ok(j) => j,
314                            Err(e) => {
315                                tracing::error!(
316                                    "ChannelTransport: failed to serialize response: {}",
317                                    e
318                                );
319                                return;
320                            }
321                        },
322                        Err(e) => {
323                            // Convert error to a JSON-RPC error response
324                            let err_resp = JsonRpcResponse::error(
325                                None,
326                                tower_mcp_types::JsonRpcError::internal_error(e.to_string()),
327                            );
328                            match serde_json::to_string(&err_resp) {
329                                Ok(j) => j,
330                                Err(_) => return,
331                            }
332                        }
333                    };
334
335                    // Best effort: if the client dropped, the send fails and
336                    // the task simply ends.
337                    let _ = response_out.send(json).await;
338                });
339            }
340
341            // The client dropped its sender: any registered streams die with
342            // the transport and cannot receive a terminal frame.
343            #[cfg(feature = "stateless")]
344            if let Ok(mut subscriptions) = subscriptions.lock() {
345                subscriptions.drain_disconnected();
346            }
347        });
348
349        Self {
350            request_tx,
351            response_rx,
352            connected: true,
353        }
354    }
355}
356
357#[async_trait]
358impl ClientTransport for ChannelTransport {
359    async fn send(&mut self, message: &str) -> Result<()> {
360        self.request_tx
361            .send(message.to_string())
362            .await
363            .map_err(|_| crate::error::Error::internal("ChannelTransport: server task dropped"))?;
364        Ok(())
365    }
366
367    async fn recv(&mut self) -> Result<Option<String>> {
368        match self.response_rx.recv().await {
369            Some(msg) => Ok(Some(msg)),
370            None => {
371                self.connected = false;
372                Ok(None)
373            }
374        }
375    }
376
377    fn is_connected(&self) -> bool {
378        self.connected
379    }
380
381    async fn close(&mut self) -> Result<()> {
382        self.connected = false;
383        Ok(())
384    }
385}