tower_mcp/client/transport.rs
1//! Client transport trait for raw JSON message I/O.
2//!
3//! This module defines the [`ClientTransport`] trait, the low-level abstraction
4//! for sending and receiving JSON-RPC messages over a transport mechanism.
5//!
6//! Unlike the previous `ClientTransport` which bundled request/response
7//! correlation, this trait provides raw message I/O. The [`McpClient`](super::McpClient)
8//! handles correlation, multiplexing, and dispatch in its background task.
9
10use async_trait::async_trait;
11
12use crate::error::Result;
13use crate::protocol::RequestId;
14
15/// Low-level transport for sending and receiving raw JSON-RPC messages.
16///
17/// Implementations handle the physical I/O (stdio, HTTP, WebSocket) while
18/// the [`McpClient`](super::McpClient) handles JSON-RPC framing, request/response
19/// correlation, and server-initiated request dispatch.
20///
21/// # Implementing a Custom Transport
22///
23/// ```rust,ignore
24/// use async_trait::async_trait;
25/// use tower_mcp::client::ClientTransport;
26/// use tower_mcp::error::Result;
27///
28/// struct MyTransport { /* ... */ }
29///
30/// #[async_trait]
31/// impl ClientTransport for MyTransport {
32/// async fn send(&mut self, message: &str) -> Result<()> {
33/// // Write message to the transport
34/// Ok(())
35/// }
36///
37/// async fn recv(&mut self) -> Result<Option<String>> {
38/// // Read next message, None on EOF
39/// Ok(None)
40/// }
41///
42/// fn is_connected(&self) -> bool { true }
43///
44/// async fn close(&mut self) -> Result<()> { Ok(()) }
45/// }
46/// ```
47#[async_trait]
48pub trait ClientTransport: Send + 'static {
49 /// Send a raw JSON message to the server.
50 ///
51 /// The message is a complete JSON-RPC request, response, or notification
52 /// serialized as a string. The transport adds any necessary framing
53 /// (e.g., newline for stdio).
54 async fn send(&mut self, message: &str) -> Result<()>;
55
56 /// Receive the next raw JSON message from the server.
57 ///
58 /// Returns `Ok(Some(json))` for a message, `Ok(None)` for clean EOF/close,
59 /// or `Err(...)` for transport errors.
60 ///
61 /// Both non-message returns end the connection: the client's message loop
62 /// stops reading and fails every pending request. Reserve them for the
63 /// connection actually being over. A single message that arrives damaged,
64 /// such as one the transport cannot decode, is not that; log it, drop it,
65 /// and return the next one. [`StdioClientTransport`](super::StdioClientTransport)
66 /// documents the reasoning in full.
67 async fn recv(&mut self) -> Result<Option<String>>;
68
69 /// Check if the transport is still connected.
70 fn is_connected(&self) -> bool;
71
72 /// Close the transport gracefully.
73 ///
74 /// After calling this, `recv()` should return `Ok(None)`.
75 async fn close(&mut self) -> Result<()>;
76
77 /// Reset the transport's session state for re-initialization.
78 ///
79 /// Called when the server indicates the session has expired. The
80 /// transport should clear its session ID, stop any SSE streams,
81 /// and prepare for a new `initialize` handshake.
82 ///
83 /// The default implementation is a no-op (for transports like stdio
84 /// that don't have sessions).
85 async fn reset_session(&mut self) {}
86
87 /// Cancel one in-flight request.
88 ///
89 /// Message-oriented transports such as stdio use the protocol's
90 /// `notifications/cancelled` notification. HTTP overrides this method to
91 /// close only the response stream belonging to the request.
92 async fn cancel_request(&mut self, request_id: &RequestId) -> Result<()> {
93 self.send(
94 &serde_json::json!({
95 "jsonrpc": "2.0",
96 "method": "notifications/cancelled",
97 "params": {
98 "requestId": request_id,
99 }
100 })
101 .to_string(),
102 )
103 .await
104 }
105
106 /// Whether this transport supports automatic session recovery.
107 ///
108 /// When true, the client will attempt to re-initialize and retry
109 /// failed operations when the server returns a session expired error.
110 ///
111 /// Default: `false`.
112 fn supports_session_recovery(&self) -> bool {
113 false
114 }
115}