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
63use crate::context::{NotificationReceiver, notification_channel};
64use crate::error::Result;
65use crate::jsonrpc::JsonRpcService;
66use crate::protocol::{JsonRpcRequest, JsonRpcResponse, McpNotification};
67use crate::router::McpRouter;
68
69use super::transport::ClientTransport;
70
71/// An in-process [`ClientTransport`] that connects directly to an [`McpRouter`].
72///
73/// Messages are passed through tokio channels: background tasks feed
74/// incoming JSON-RPC requests to a [`JsonRpcService<McpRouter>`] (one spawned
75/// task per request, so calls run concurrently) and pump server
76/// notifications into the response stream.
77pub struct ChannelTransport {
78    /// Send raw JSON messages to the server task.
79    request_tx: mpsc::Sender<String>,
80    /// Receive raw JSON responses and notification frames from the server tasks.
81    response_rx: mpsc::Receiver<String>,
82    connected: bool,
83}
84
85impl ChannelTransport {
86    /// Create a new channel transport backed by the given router.
87    ///
88    /// Wires an internal notification channel into the router, so
89    /// notifications emitted during request handling (progress, log
90    /// messages, list-changed) are delivered to the client. To push
91    /// notifications from the host process's own tasks, use
92    /// [`with_notifications`](Self::with_notifications) instead.
93    ///
94    /// Note: this overwrites any notification sender previously set on the
95    /// router, matching the transport-owns-the-channel behavior of
96    /// [`HttpTransport::new`](crate::transport::HttpTransport::new).
97    pub fn new(router: McpRouter) -> Self {
98        let (notification_tx, notification_rx) = notification_channel(64);
99        let router = router.with_notification_sender(notification_tx);
100        Self::with_notifications(router, notification_rx)
101    }
102
103    /// Create a channel transport with a caller-owned notification receiver.
104    ///
105    /// Mirrors [`HttpTransport::with_notifications`]: the host process keeps
106    /// the sender (its own clone from [`notification_channel`], or via
107    /// [`McpRouter::notification_sender`]) and pushes
108    /// [`ServerNotification`](crate::context::ServerNotification)s from its
109    /// own tasks; the transport serializes them into JSON-RPC notification
110    /// frames and interleaves them into [`recv`](ClientTransport::recv).
111    ///
112    /// The router passed here should already carry the matching sender (see
113    /// the module-level example) so notifications emitted during request
114    /// handling flow through the same channel.
115    ///
116    /// [`HttpTransport::with_notifications`]: crate::transport::HttpTransport::with_notifications
117    pub fn with_notifications(
118        router: McpRouter,
119        mut notification_rx: NotificationReceiver,
120    ) -> Self {
121        let (request_tx, mut request_rx) = mpsc::channel::<String>(64);
122        let (response_tx, response_rx) = mpsc::channel::<String>(64);
123
124        // Notification pump: serialize ServerNotifications into JSON-RPC
125        // notification frames on the shared response stream.
126        let notification_out = response_tx.clone();
127        tokio::spawn(async move {
128            while let Some(notification) = notification_rx.recv().await {
129                if let Some(json) = crate::transport::stdio::serialize_notification(&notification)
130                    && notification_out.send(json).await.is_err()
131                {
132                    break; // Client dropped
133                }
134            }
135        });
136
137        let service = JsonRpcService::new(router.clone());
138
139        tokio::spawn(async move {
140            while let Some(raw_request) = request_rx.recv().await {
141                // Notifications carry no `id`, so they cannot parse as
142                // JsonRpcRequest (whose id is required). Inspect the raw
143                // frame first and handle them by method.
144                let parsed: serde_json::Value = match serde_json::from_str(&raw_request) {
145                    Ok(v) => v,
146                    Err(e) => {
147                        tracing::error!("ChannelTransport: failed to parse frame: {}", e);
148                        continue;
149                    }
150                };
151                if parsed.get("id").is_none() {
152                    if parsed.get("method").and_then(|m| m.as_str())
153                        == Some("notifications/initialized")
154                    {
155                        router.handle_notification(McpNotification::Initialized);
156                    }
157                    // No response for notifications
158                    continue;
159                }
160
161                let req: JsonRpcRequest = match serde_json::from_value(parsed) {
162                    Ok(r) => r,
163                    Err(e) => {
164                        tracing::error!("ChannelTransport: failed to parse request: {}", e);
165                        continue;
166                    }
167                };
168
169                // Process each request in its own task so a slow call does
170                // not block the transport. The client correlates responses
171                // by request id, so completion order does not matter.
172                let mut service = service.clone();
173                let response_out = response_tx.clone();
174                tokio::spawn(async move {
175                    let response = service.call_single(req).await;
176
177                    let json = match response {
178                        Ok(resp) => match serde_json::to_string(&resp) {
179                            Ok(j) => j,
180                            Err(e) => {
181                                tracing::error!(
182                                    "ChannelTransport: failed to serialize response: {}",
183                                    e
184                                );
185                                return;
186                            }
187                        },
188                        Err(e) => {
189                            // Convert error to a JSON-RPC error response
190                            let err_resp = JsonRpcResponse::error(
191                                None,
192                                tower_mcp_types::JsonRpcError::internal_error(e.to_string()),
193                            );
194                            match serde_json::to_string(&err_resp) {
195                                Ok(j) => j,
196                                Err(_) => return,
197                            }
198                        }
199                    };
200
201                    // Best effort: if the client dropped, the send fails and
202                    // the task simply ends.
203                    let _ = response_out.send(json).await;
204                });
205            }
206        });
207
208        Self {
209            request_tx,
210            response_rx,
211            connected: true,
212        }
213    }
214}
215
216#[async_trait]
217impl ClientTransport for ChannelTransport {
218    async fn send(&mut self, message: &str) -> Result<()> {
219        self.request_tx
220            .send(message.to_string())
221            .await
222            .map_err(|_| crate::error::Error::internal("ChannelTransport: server task dropped"))?;
223        Ok(())
224    }
225
226    async fn recv(&mut self) -> Result<Option<String>> {
227        match self.response_rx.recv().await {
228            Some(msg) => Ok(Some(msg)),
229            None => {
230                self.connected = false;
231                Ok(None)
232            }
233        }
234    }
235
236    fn is_connected(&self) -> bool {
237        self.connected
238    }
239
240    async fn close(&mut self) -> Result<()> {
241        self.connected = false;
242        Ok(())
243    }
244}