use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, ReadBuf};
pub const MAX_MCP_LINE_BYTES: usize = 1024 * 1024;
pub const MAX_TOOL_ARGUMENT_BYTES: usize = 256 * 1024;
pub const MAX_REQUEST_STATE_BYTES: usize = 1024;
pub const MAX_INPUT_RESPONSES_BYTES: usize = 64 * 1024;
const MAX_DROP_NOTICES: u64 = 8;
const CHUNK: usize = 8 * 1024;
pub struct LineLimited<R> {
inner: R,
buf: Box<[u8]>,
fill: usize,
scanned: usize,
total: usize,
phase: Phase,
scratch: Box<[u8]>,
limit: usize,
dropped: u64,
}
#[derive(Clone, Copy)]
enum Phase {
Filling,
Draining { drain: usize },
Discarding,
Eof,
}
impl<R: AsyncRead> LineLimited<R> {
pub fn new(inner: R) -> Self {
Self::with_limit(inner, MAX_MCP_LINE_BYTES)
}
pub fn with_limit(inner: R, limit: usize) -> Self {
let scratch_len = CHUNK.min(limit + 1);
Self {
inner,
buf: vec![0u8; limit + 1].into_boxed_slice(),
fill: 0,
scanned: 0,
total: 0,
phase: Phase::Filling,
scratch: vec![0u8; scratch_len].into_boxed_slice(),
limit,
dropped: 0,
}
}
}
impl<R: AsyncRead + Unpin> AsyncRead for LineLimited<R> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
out: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
if out.remaining() == 0 {
return Poll::Ready(Ok(()));
}
let this = &mut *self;
loop {
match this.phase {
Phase::Eof => return Poll::Ready(Ok(())),
Phase::Draining { drain } => {
let n = (this.total - drain).min(out.remaining());
out.put_slice(&this.buf[drain..drain + n]);
if drain + n == this.total {
let leftover = this.fill - this.total;
if leftover > 0 {
this.buf.copy_within(this.total..this.fill, 0);
}
this.fill = leftover;
this.scanned = 0;
this.phase = Phase::Filling;
} else {
this.phase = Phase::Draining { drain: drain + n };
}
return Poll::Ready(Ok(()));
}
Phase::Discarding => {
let mut rb = ReadBuf::new(this.scratch.as_mut());
match Pin::new(&mut this.inner).poll_read(cx, &mut rb) {
Poll::Ready(Ok(())) => {
let n = rb.filled().len();
if n == 0 {
this.phase = Phase::Eof;
continue;
}
match rb.filled().iter().position(|b| *b == b'\n') {
Some(i) => {
let leftover = n - (i + 1);
if leftover > 0 {
this.scratch.copy_within(i + 1..n, 0);
this.buf[..leftover]
.copy_from_slice(&this.scratch[..leftover]);
}
this.fill = leftover;
this.scanned = 0;
this.phase = Phase::Filling;
}
None => continue,
}
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
Phase::Filling => {
if this.scanned < this.fill {
if let Some(i) = this.buf[this.scanned..this.fill]
.iter()
.position(|b| *b == b'\n')
{
this.total = this.scanned + i + 1;
this.phase = Phase::Draining { drain: 0 };
continue;
}
this.scanned = this.fill;
}
if this.fill > this.limit {
this.dropped += 1;
if this.dropped <= MAX_DROP_NOTICES {
eprintln!(
"[sequel-mcp] dropped oversized stdin line (limit {} bytes)",
this.limit
);
}
this.fill = 0;
this.scanned = 0;
this.phase = Phase::Discarding;
continue;
}
let mut rb = ReadBuf::new(&mut this.buf[this.fill..]);
match Pin::new(&mut this.inner).poll_read(cx, &mut rb) {
Poll::Ready(Ok(())) => {
let n = rb.filled().len();
if n == 0 {
if this.fill > 0 {
this.total = this.fill;
this.phase = Phase::Draining { drain: 0 };
} else {
this.phase = Phase::Eof;
}
continue;
}
this.fill += n;
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
}
}
}
}
pub fn limited_stdio() -> (LineLimited<tokio::io::Stdin>, tokio::io::Stdout) {
(LineLimited::new(tokio::io::stdin()), tokio::io::stdout())
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn run_adapter(input: Vec<u8>, limit: usize) -> Vec<u8> {
let (mut client, server) = tokio::io::duplex(8 * CHUNK);
let mut adapter = LineLimited::with_limit(server, limit);
let writer = tokio::spawn(async move {
client.write_all(&input).await.unwrap();
client.shutdown().await.unwrap();
});
let mut out = Vec::new();
adapter.read_to_end(&mut out).await.unwrap();
writer.await.unwrap();
out
}
#[tokio::test]
async fn small_lines_pass_through_verbatim() {
let input = b"{\"a\":1}\n{\"b\":2}\n".to_vec();
let out = run_adapter(input.clone(), 64).await;
assert_eq!(out, input);
}
#[tokio::test]
async fn line_at_exact_limit_passes_with_terminator() {
let body = "x".repeat(63);
let input = format!("{body}\nnext\n").into_bytes();
let out = run_adapter(input.clone(), 64).await;
assert_eq!(out, input, "line of exactly limit bytes passes");
}
#[tokio::test]
async fn oversized_line_is_dropped_and_stream_recovers() {
let before = "ok1\n";
let big = format!("{}\n", "y".repeat(200));
let after = "ok2\n";
let input = format!("{before}{big}{after}").into_bytes();
let out = run_adapter(input, 64).await;
assert_eq!(out, b"ok1\nok2\n".to_vec());
}
#[tokio::test]
async fn no_newline_oversized_then_newline_then_valid() {
let input = format!("{}\nvalid\n", "z".repeat(500)).into_bytes();
let out = run_adapter(input, 64).await;
assert_eq!(out, b"valid\n".to_vec());
}
#[tokio::test]
async fn many_oversized_lines_in_sequence() {
let mut input = String::new();
for i in 0..5 {
input.push_str(&format!("{}{}\n", "w".repeat(100), i));
}
input.push_str("done\n");
let out = run_adapter(input.into_bytes(), 64).await;
assert_eq!(out, b"done\n".to_vec());
}
#[tokio::test]
async fn slow_byte_by_byte_oversized_sender_stays_bounded() {
let (mut client, server) = tokio::io::duplex(64);
let mut adapter = LineLimited::with_limit(server, 32);
let writer = tokio::spawn(async move {
for _ in 0..200 {
client.write_all(b"q").await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
client.write_all(b"\nrecover\n").await.unwrap();
client.shutdown().await.unwrap();
});
let mut out = Vec::new();
adapter.read_to_end(&mut out).await.unwrap();
writer.await.unwrap();
assert_eq!(out, b"recover\n".to_vec());
}
#[tokio::test]
async fn carried_over_line_is_forwarded_without_new_input() {
let (mut client, server) = tokio::io::duplex(64);
let mut adapter = LineLimited::with_limit(server, 64);
client.write_all(b"first\nsecond\n").await.unwrap();
let mut out1 = [0u8; 64];
let n1 = tokio::time::timeout(Duration::from_secs(2), adapter.read(&mut out1))
.await
.expect("first line must not stall")
.unwrap();
assert_eq!(&out1[..n1], b"first\n");
let mut out2 = [0u8; 64];
let n2 = tokio::time::timeout(Duration::from_secs(2), adapter.read(&mut out2))
.await
.expect("carried-over line must not wait for new input")
.unwrap();
assert_eq!(&out2[..n2], b"second\n");
let _ = &mut client;
}
#[tokio::test]
async fn chunk_boundary_lines_are_framed_correctly() {
let mut input = String::new();
for i in 0..40 {
input.push_str(&format!("line-{i:02}-{}\n", "p".repeat(500)));
}
let out = run_adapter(input.clone().into_bytes(), 1024).await;
assert_eq!(out, input.into_bytes());
}
}