use std::future::Future;
use std::sync::Arc;
use futures_util::SinkExt;
use rmcp::RoleServer;
use rmcp::service::{RxJsonRpcMessage, TxJsonRpcMessage};
use rmcp::transport::Transport;
use rmcp::transport::async_rw::JsonRpcMessageCodec;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
use tokio::sync::Mutex;
use tokio_util::bytes::BytesMut;
use tokio_util::codec::{Decoder, FramedWrite};
pub(crate) const MAX_STDIO_LINE_BYTES: usize = 4 * 1024 * 1024;
const READ_CHUNK_BYTES: usize = 8 * 1024;
type Codec = JsonRpcMessageCodec<RxJsonRpcMessage<RoleServer>>;
type Writer<W> = FramedWrite<W, JsonRpcMessageCodec<TxJsonRpcMessage<RoleServer>>>;
pub(crate) struct BoundedStdioTransport<R, W> {
read: R,
buf: BytesMut,
codec: Codec,
write: Arc<Mutex<Option<Writer<W>>>>,
}
impl<R, W> BoundedStdioTransport<R, W>
where
R: AsyncRead + Send + Unpin,
W: AsyncWrite + Send + Unpin + 'static,
{
pub(crate) fn new(read: R, write: W) -> BoundedStdioTransport<R, W> {
BoundedStdioTransport::with_max_line(read, write, MAX_STDIO_LINE_BYTES)
}
pub(crate) fn with_max_line(read: R, write: W, max_line: usize) -> BoundedStdioTransport<R, W> {
let write = FramedWrite::new(
write,
JsonRpcMessageCodec::<TxJsonRpcMessage<RoleServer>>::default(),
);
BoundedStdioTransport {
read,
buf: BytesMut::new(),
codec: Codec::new_with_max_length(max_line),
write: Arc::new(Mutex::new(Some(write))),
}
}
}
impl<R, W> Transport<RoleServer> for BoundedStdioTransport<R, W>
where
R: AsyncRead + Send + Unpin,
W: AsyncWrite + Send + Unpin + 'static,
{
type Error = std::io::Error;
fn send(
&mut self,
item: TxJsonRpcMessage<RoleServer>,
) -> impl Future<Output = Result<(), Self::Error>> + Send + 'static {
let lock = Arc::clone(&self.write);
async move {
let mut guard = lock.lock().await;
match guard.as_mut() {
Some(write) => write.send(item).await.map_err(std::io::Error::from),
None => Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"stdio transport is closed",
)),
}
}
}
async fn receive(&mut self) -> Option<RxJsonRpcMessage<RoleServer>> {
loop {
match self.codec.decode(&mut self.buf) {
Ok(Some(message)) => return Some(message),
Ok(None) => {}
Err(error) => {
tracing::debug!(
"stdio transport dropped an over-cap or malformed frame: {error}"
);
continue;
}
}
self.buf.reserve(READ_CHUNK_BYTES);
match self.read.read_buf(&mut self.buf).await {
Ok(0) => {
return match self.codec.decode_eof(&mut self.buf) {
Ok(message) => message,
Err(error) => {
tracing::debug!("stdio transport dropped a trailing frame: {error}");
None
}
};
}
Ok(_) => {}
Err(error) => {
tracing::error!("stdio transport read failed: {error}");
return None;
}
}
}
}
async fn close(&mut self) -> Result<(), Self::Error> {
self.write.lock().await.take();
Ok(())
}
}
#[cfg(test)]
mod tests {
use rmcp::transport::Transport;
use serde_json::Value;
use super::BoundedStdioTransport;
fn line(json: &str) -> Vec<u8> {
let mut bytes = json.as_bytes().to_vec();
bytes.push(b'\n');
bytes
}
fn method_of(message: &rmcp::service::RxJsonRpcMessage<rmcp::RoleServer>) -> String {
serde_json::to_value(message)
.expect("a received message serializes")
.get("method")
.and_then(Value::as_str)
.expect("a request carries a method")
.to_owned()
}
#[tokio::test]
async fn a_normal_line_is_delivered() {
let input = line(r#"{"jsonrpc":"2.0","method":"ping","id":1}"#);
let mut transport =
BoundedStdioTransport::with_max_line(&input[..], tokio::io::sink(), 1024);
let message = transport.receive().await.expect("the ping is delivered");
assert_eq!(method_of(&message), "ping");
}
#[tokio::test]
async fn an_over_cap_line_is_dropped_and_the_next_message_survives() {
let cap = 1024;
let mut input = vec![b'x'; cap * 64];
input.push(b'\n');
input.extend_from_slice(&line(r#"{"jsonrpc":"2.0","method":"ping","id":1}"#));
let mut transport =
BoundedStdioTransport::with_max_line(&input[..], tokio::io::sink(), cap);
let message = transport
.receive()
.await
.expect("the ping after the over-cap line is delivered");
assert_eq!(
method_of(&message),
"ping",
"the over-cap line was drained, not buffered, and did not kill the session"
);
}
#[tokio::test]
async fn end_of_input_ends_the_session() {
let input: &[u8] = b"";
let mut transport = BoundedStdioTransport::with_max_line(input, tokio::io::sink(), 1024);
assert!(
transport.receive().await.is_none(),
"a closed peer ends the stream rather than looping"
);
}
}