Skip to main content

oxicode_agent/mcp/transport/
mod.rs

1//! MCP transport layer abstraction.
2//!
3//! [`McpTransport`] decouples MCP message I/O from [`crate::mcp::client::McpClient`].
4//! Transports are responsible for framing, stream parsing, and the
5//! request/response correlation over their own I/O channel. The client owns
6//! the id counter and uses `request` / `notify` to talk to the server.
7//!
8//! v2 redesign (D-rev1): the previous `send`/`recv` model was stdio-shaped and
9//! forced HTTP transports to either buffer full HTTP round-trips inside
10//! `send` or maintain a parallel reader. The new `request`/`notify` model
11//! (mirroring the OMP MCP transport) puts correlation inside the transport
12//! and exposes a single `set_inbound_handler` for notifications and
13//! server→client requests that may arrive between responses.
14//!
15//! v2.0: [`stdio::StdioTransport`] (JSONL framing, inline read loop).
16//! v2.1: [`http::StreamableHttpTransport`] (Streamable HTTP + SSE responses).
17
18pub mod http;
19pub mod stdio;
20
21use crate::mcp::types::RawJsonRpcMessage;
22use anyhow::Result;
23use async_trait::async_trait;
24
25/// Handler invoked by a transport for inbound messages that are not the
26/// currently awaited response.
27///
28/// - **Notifications** (no `id`): the handler is called for side-effect;
29///   the return value is ignored (notifications have no reply).
30/// - **Server→client requests** (`method` + `id`, id not matching the
31///   pending request): the handler may return `Some(value)` to send back
32///   a JSON-RPC response; the transport serializes and writes it. Return
33///   `None` to leave the request unanswered (rare; usually a bug).
34///
35/// `Send + Sync` so the same trait object can be used from any task
36/// that needs to dispatch inbound messages (e.g. the HTTP POST-SSE
37/// drain in [`http::StreamableHttpTransport`]).
38pub type InboundHandler =
39    Box<dyn FnMut(RawJsonRpcMessage) -> Option<serde_json::Value> + Send + Sync>;
40
41/// MCP transport layer.
42///
43/// Implementations own the raw I/O channel (stdio pipes, HTTP+SSE streams,
44/// ...) and the framing specific to that channel. They correlate outgoing
45/// requests with incoming responses and surface anything else (notifications,
46/// server→client requests) to the installed [`InboundHandler`].
47#[async_trait]
48pub trait McpTransport: Send + Sync {
49    /// Send a JSON-RPC request and await the matching response.
50    ///
51    /// `id` is the JSON-RPC request id and `json` is the already-serialized
52    /// JSON-RPC request body. The transport writes `json` to the channel
53    /// and returns the next message whose `id` equals the one supplied.
54    /// Messages that arrive in the meantime (notifications, server→client
55    /// requests) are dispatched to the installed [`InboundHandler`].
56    ///
57    /// Implementations SHOULD apply a per-request timeout and return
58    /// `Err` on timeout.
59    async fn request(&mut self, id: u64, json: &str) -> Result<RawJsonRpcMessage>;
60
61    /// Send a JSON-RPC notification (no response expected).
62    async fn notify(&mut self, json: &str) -> Result<()>;
63
64    /// Install (or replace) the inbound handler. Called by the client before
65    /// the first `request` so that any peer-sent message arriving during the
66    /// handshake is dispatched.
67    fn set_inbound_handler(&mut self, handler: InboundHandler);
68
69    /// Close the transport gracefully. Default is a no-op for transports
70    /// that close on drop.
71    async fn close(&mut self) -> Result<()> {
72        Ok(())
73    }
74
75    /// Whether the transport is currently connected.
76    fn is_connected(&self) -> bool;
77}