runx-runtime 0.6.19

Native Rust runtime for local runx execution, adapters, harness replay, receipts, and sandboxing.
Documentation
use std::future::Future;
use std::marker::PhantomData;
use std::sync::{Arc, Mutex};

use super::framing::{content_length, find_header_end};

pub(super) struct RmcpContentLengthTransport<R, W, Role> {
    read: R,
    write: Arc<tokio::sync::Mutex<W>>,
    buffer: Vec<u8>,
    max_message_bytes: usize,
    error_state: RmcpTransportErrorState,
    role: PhantomData<Role>,
}

#[derive(Clone, Default)]
pub(super) struct RmcpTransportErrorState {
    message: Arc<Mutex<Option<String>>>,
}

impl RmcpTransportErrorState {
    pub(super) fn record(&self, error: std::io::Error) {
        if let Ok(mut message) = self.message.lock() {
            *message = Some(error.to_string());
        }
    }

    pub(super) fn take(&self) -> Option<String> {
        self.message
            .lock()
            .ok()
            .and_then(|mut message| message.take())
    }
}

impl<R, W, Role> RmcpContentLengthTransport<R, W, Role> {
    pub(super) fn new(
        read: R,
        write: W,
        max_message_bytes: usize,
        error_state: RmcpTransportErrorState,
    ) -> Self {
        Self {
            read,
            write: Arc::new(tokio::sync::Mutex::new(write)),
            buffer: Vec::new(),
            max_message_bytes,
            error_state,
            role: PhantomData,
        }
    }
}

impl<R, W, Role> rmcp::transport::Transport<Role> for RmcpContentLengthTransport<R, W, Role>
where
    R: tokio::io::AsyncRead + Send + Unpin + 'static,
    W: tokio::io::AsyncWrite + Send + Unpin + 'static,
    Role: rmcp::service::ServiceRole,
{
    type Error = std::io::Error;

    fn send(
        &mut self,
        item: rmcp::service::TxJsonRpcMessage<Role>,
    ) -> impl Future<Output = Result<(), Self::Error>> + Send + 'static {
        let write = Arc::clone(&self.write);
        async move {
            let body = serde_json::to_vec(&item).map_err(std::io::Error::other)?;
            let header = format!("Content-Length: {}\r\n\r\n", body.len());
            let mut write = write.lock().await;
            tokio::io::AsyncWriteExt::write_all(&mut *write, header.as_bytes()).await?;
            tokio::io::AsyncWriteExt::write_all(&mut *write, &body).await?;
            tokio::io::AsyncWriteExt::flush(&mut *write).await
        }
    }

    async fn receive(&mut self) -> Option<rmcp::service::RxJsonRpcMessage<Role>> {
        match next_rmcp_framed_message::<R, Role>(
            &mut self.read,
            &mut self.buffer,
            self.max_message_bytes,
        )
        .await
        {
            Ok(Some(message)) => Some(message),
            Ok(None) => None,
            Err(error) => {
                self.error_state.record(error);
                None
            }
        }
    }

    async fn close(&mut self) -> Result<(), Self::Error> {
        let mut write = self.write.lock().await;
        tokio::io::AsyncWriteExt::shutdown(&mut *write).await
    }
}

async fn next_rmcp_framed_message<R, Role>(
    read: &mut R,
    buffer: &mut Vec<u8>,
    max_message_bytes: usize,
) -> Result<Option<rmcp::service::RxJsonRpcMessage<Role>>, std::io::Error>
where
    R: tokio::io::AsyncRead + Unpin,
    Role: rmcp::service::ServiceRole,
{
    loop {
        if let Some(message) = parse_next_rmcp_framed_message::<Role>(buffer, max_message_bytes)? {
            return Ok(Some(message));
        }
        if buffer.len() > max_message_bytes {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "MCP message exceeded size limit.",
            ));
        }
        let mut chunk = [0_u8; 8192];
        let read = tokio::io::AsyncReadExt::read(read, &mut chunk).await?;
        if read == 0 {
            return Ok(None);
        }
        buffer.extend_from_slice(&chunk[..read]);
    }
}

fn parse_next_rmcp_framed_message<Role>(
    buffer: &mut Vec<u8>,
    max_message_bytes: usize,
) -> Result<Option<rmcp::service::RxJsonRpcMessage<Role>>, std::io::Error>
where
    Role: rmcp::service::ServiceRole,
{
    let Some(header_end) = find_header_end(buffer) else {
        return Ok(None);
    };
    if header_end > max_message_bytes {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "MCP message exceeded size limit.",
        ));
    }
    let header = std::str::from_utf8(&buffer[..header_end])
        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
    let content_length = content_length(header).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "MCP message missing Content-Length.",
        )
    })?;
    if content_length > max_message_bytes {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "MCP message exceeded size limit.",
        ));
    }
    let body_start = header_end + 4;
    let body_end = body_start.saturating_add(content_length);
    if buffer.len() < body_end {
        return Ok(None);
    }
    let body = buffer[body_start..body_end].to_vec();
    buffer.drain(..body_end);
    serde_json::from_slice(&body)
        .map(Some)
        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
}