Skip to main content

flymcprust_core/transport/
stdio.rs

1//! stdio transport: newline-delimited JSON-RPC over stdin/stdout.
2//!
3//! - Lines longer than the message limit are rejected and skipped up to the
4//!   next newline, without buffering them.
5//! - Requests run concurrently (bounded); responses are written by a single
6//!   task, one message per line, so stdout only ever carries MCP messages.
7//! - `notifications/cancelled` cancels the referenced in-flight request,
8//!   which then produces no response.
9//! - End of input stops reading; in-flight requests get a grace period to
10//!   finish before the transport returns.
11//! - Legacy clients (2025-11-25, 2025-06-18) open with `initialize`; the
12//!   connection then remembers what they declared, and input requests of
13//!   multi round-trip handlers are sent to them as server-to-client requests
14//!   whose responses are routed back here. Server requests wait at most the
15//!   tool timeout: if every request slot is taken by handlers waiting for
16//!   the client while the client sends yet another request first, reading
17//!   pauses until those waits time out.
18
19use std::collections::HashMap;
20use std::io;
21use std::sync::{Arc, Mutex};
22use std::time::Duration;
23
24use serde::Deserialize;
25use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
26use tokio::sync::{Semaphore, mpsc};
27use tokio::task::JoinSet;
28use tokio_util::sync::CancellationToken;
29
30use crate::context::TransportContext;
31use crate::error::McpError;
32use crate::jsonrpc::{self, Message, Rejection, Response};
33use crate::legacy::{ChannelPeer, LegacySession, PendingRequests};
34use crate::notify::NotifyChannel;
35use crate::server::{McpRequest, Server};
36use crate::spec::{RequestId, methods};
37
38/// Extra time granted to in-flight requests after end of input, on top of
39/// the tool timeout.
40const SHUTDOWN_GRACE: Duration = Duration::from_secs(1);
41
42type InFlight = Arc<Mutex<HashMap<RequestId, CancellationToken>>>;
43
44impl Server {
45    /// Serves requests over the process's stdin and stdout until stdin is
46    /// closed.
47    ///
48    /// When stdin is an interactive terminal (someone ran the server by
49    /// hand), a short explanation is printed on stderr: an MCP server waits
50    /// silently for JSON-RPC messages, which looks like a hang.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error when reading stdin or writing stdout fails.
55    pub async fn serve_stdio(self) -> io::Result<()> {
56        if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
57            explain_interactive_use();
58        }
59        serve_io(self, tokio::io::stdin(), tokio::io::stdout()).await
60    }
61}
62
63/// Applies a legacy `logging/setLevel` to the connection's session.
64#[allow(deprecated)] // Logging is deprecated (SEP-2577) but legacy clients use it.
65fn set_log_level(
66    legacy: &Mutex<Option<LegacySession>>,
67    params: Option<&serde_json::value::RawValue>,
68) -> Result<(), McpError> {
69    #[derive(Deserialize)]
70    struct Params {
71        level: crate::spec::LoggingLevel,
72    }
73    let params: Params = serde_json::from_str(params.map_or("{}", |p| p.get()))
74        .map_err(|_| McpError::invalid_params("Invalid log level"))?;
75    if let Some(session) = legacy
76        .lock()
77        .unwrap_or_else(std::sync::PoisonError::into_inner)
78        .as_mut()
79    {
80        session.log_level = Some(params.level);
81    }
82    Ok(())
83}
84
85/// Tells someone who started the server in a terminal what is going on.
86// stderr is safe here: stdout is the protocol channel, stderr is not.
87#[allow(clippy::print_stderr)]
88fn explain_interactive_use() {
89    let program = std::env::current_exe()
90        .ok()
91        .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
92        .unwrap_or_else(|| "this-server".to_owned());
93    eprintln!(
94        "This is an MCP server: it reads JSON-RPC messages on stdin and is meant to be \
95         started by an MCP client.\n  \
96         = help: try it with the MCP Inspector:\n          \
97         npx @modelcontextprotocol/inspector --cli path/to/{program} --protocol-era modern \
98         --method tools/list\n\
99         (Waiting for messages on stdin; press Ctrl+C to stop.)"
100    );
101}
102
103/// Serves requests over any byte stream pair using the stdio framing (for
104/// example a Unix socket, a TCP connection, or an in-memory pipe in tests).
105///
106/// # Errors
107///
108/// Returns an error when reading or writing fails.
109pub async fn serve_io<R, W>(server: Server, reader: R, writer: W) -> io::Result<()>
110where
111    R: AsyncRead + Unpin,
112    W: AsyncWrite + Unpin + Send + 'static,
113{
114    let limits = server.limits().clone();
115    let (tx, rx) = mpsc::channel::<Vec<u8>>(limits.max_concurrent_calls.max(1));
116    let writer_task = tokio::spawn(write_loop(writer, rx));
117
118    let in_flight: InFlight = Arc::default();
119    // Legacy protocol: what the client declared at `initialize`, and the
120    // server-to-client requests waiting for an answer.
121    let legacy: Arc<Mutex<Option<LegacySession>>> = Arc::default();
122    let peer = ChannelPeer {
123        pending: Arc::new(PendingRequests::default()),
124        owner: 0,
125        out: tx.clone(),
126        timeout: limits.tool_timeout,
127    };
128    let permits = Arc::new(Semaphore::new(limits.max_concurrent_calls.max(1)));
129    let mut tasks = JoinSet::new();
130    // Large reads: tokio reads stdin on its blocking thread pool, one round
131    // trip per read.
132    let mut reader = BufReader::with_capacity(READ_BUFFER_BYTES, reader);
133    let mut line = Vec::new();
134
135    let read_result = loop {
136        // Reap finished tasks so the set does not grow without bound.
137        while tasks.try_join_next().is_some() {}
138
139        line.clear();
140        match read_line(&mut reader, &mut line, limits.max_message_bytes).await {
141            Ok(Line::Eof) => break Ok(()),
142            Ok(Line::TooLong) => {
143                let response =
144                    Response::error(None, McpError::invalid_request("Message too large"));
145                if tx.send(response.to_vec()).await.is_err() {
146                    break Ok(());
147                }
148                continue;
149            }
150            Ok(Line::Complete) => {}
151            Err(e) => break Err(e),
152        }
153        if line.iter().all(u8::is_ascii_whitespace) {
154            continue;
155        }
156
157        let message = match jsonrpc::parse(&line, &limits) {
158            Ok(message) => message,
159            Err(Rejection::Error { id, error }) => {
160                if tx.send(Response::error(id, error).to_vec()).await.is_err() {
161                    break Ok(());
162                }
163                continue;
164            }
165            Err(Rejection::UnexpectedResponse) => {
166                // A legacy client answering one of our requests.
167                let routed = jsonrpc::parse_client_response(&line)
168                    .is_some_and(|response| peer.pending.complete(response, 0));
169                if !routed {
170                    tracing::debug!("ignoring a JSON-RPC response that answers no pending request");
171                }
172                continue;
173            }
174        };
175
176        let request = match message {
177            Message::Notification(n) => {
178                if n.method == methods::NOTIFICATIONS_CANCELLED {
179                    cancel(&in_flight, n.params.as_deref());
180                }
181                // Other client notifications have no effect.
182                continue;
183            }
184            Message::Request(request) => request,
185        };
186
187        if request.method == "initialize" && server.legacy_clients_enabled() {
188            let response = match server.legacy_initialize(request.params.as_deref()) {
189                Ok((result, session)) => {
190                    *legacy
191                        .lock()
192                        .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(session);
193                    Response::Result {
194                        id: request.id,
195                        result,
196                    }
197                }
198                Err(error) => Response::error(Some(request.id), error),
199            };
200            if tx.send(response.to_vec()).await.is_err() {
201                break Ok(());
202            }
203            continue;
204        }
205        let session = legacy
206            .lock()
207            .unwrap_or_else(std::sync::PoisonError::into_inner)
208            .clone();
209
210        // Legacy log level: kept for the rest of the connection.
211        if request.method == "logging/setLevel" && session.is_some() {
212            let response = match set_log_level(&legacy, request.params.as_deref()) {
213                Ok(()) => serde_json::value::to_raw_value(&serde_json::json!({}))
214                    .map(|result| Response::Result {
215                        id: request.id.clone(),
216                        result,
217                    })
218                    .unwrap_or_else(|_| {
219                        Response::error(Some(request.id.clone()), McpError::internal())
220                    }),
221                Err(error) => Response::error(Some(request.id), error),
222            };
223            if tx.send(response.to_vec()).await.is_err() {
224                break Ok(());
225            }
226            continue;
227        }
228        // Progress and log messages go on the connection, before the response.
229        let mut transport = TransportContext::default();
230        transport.insert(NotifyChannel(tx.clone()));
231
232        let token = CancellationToken::new();
233        let duplicate = match lock(&in_flight).entry(request.id.clone()) {
234            std::collections::hash_map::Entry::Occupied(_) => true,
235            std::collections::hash_map::Entry::Vacant(slot) => {
236                slot.insert(token.clone());
237                false
238            }
239        };
240        if duplicate {
241            let response = Response::error(
242                Some(request.id),
243                McpError::invalid_request("Duplicate request id"),
244            );
245            if tx.send(response.to_vec()).await.is_err() {
246                break Ok(());
247            }
248            continue;
249        }
250
251        // Backpressure: stop reading while too many requests are in flight.
252        let Ok(permit) = Arc::clone(&permits).acquire_owned().await else {
253            break Ok(());
254        };
255        let server = server.clone();
256        let tx = tx.clone();
257        let in_flight = Arc::clone(&in_flight);
258        let peer = peer.clone();
259        tasks.spawn(async move {
260            let id = request.id.clone();
261            let response = match session {
262                Some(session) => {
263                    server
264                        .handle_legacy(request, &session, transport, token, &peer)
265                        .await
266                }
267                None => {
268                    server
269                        .handle(McpRequest {
270                            message: Message::Request(request),
271                            transport,
272                            cancellation: token,
273                        })
274                        .await
275                }
276            };
277            lock(&in_flight).remove(&id);
278            if let Some(response) = response {
279                // The writer only stops when output is closed; nothing to do then.
280                let _ = tx.send(response.to_vec()).await;
281            }
282            drop(permit);
283        });
284    };
285
286    // End of input: let in-flight requests finish, within a deadline.
287    let deadline = limits.tool_timeout + SHUTDOWN_GRACE;
288    if tokio::time::timeout(deadline, async {
289        while tasks.join_next().await.is_some() {}
290    })
291    .await
292    .is_err()
293    {
294        tracing::warn!("in-flight requests did not finish before shutdown; aborting them");
295        tasks.abort_all();
296        // Aborted tasks release their copies of the writer channel when they
297        // are dropped.
298        while tasks.join_next().await.is_some() {}
299    }
300    // The writer stops once every sender is gone, including the legacy
301    // peer's copy: otherwise end of input would never end the transport.
302    drop(peer);
303    drop(tx);
304    let write_result = writer_task
305        .await
306        .unwrap_or_else(|e| Err(io::Error::other(e)));
307    read_result.and(write_result)
308}
309
310/// Size of the stdin read buffer.
311const READ_BUFFER_BYTES: usize = 64 * 1024;
312
313/// Messages are coalesced into writes of up to this many bytes.
314const WRITE_BATCH_BYTES: usize = 64 * 1024;
315
316async fn write_loop<W: AsyncWrite + Unpin>(
317    mut writer: W,
318    mut rx: mpsc::Receiver<Vec<u8>>,
319) -> io::Result<()> {
320    // Messages ready at the same time go out in one write and one flush:
321    // tokio's stdout performs each write on its blocking thread pool, so
322    // writing them one by one costs two round trips per message.
323    let mut batch = Vec::new();
324    while let Some(message) = rx.recv().await {
325        batch.clear();
326        batch.extend_from_slice(&message);
327        batch.push(b'\n');
328        while batch.len() < WRITE_BATCH_BYTES {
329            let Ok(more) = rx.try_recv() else { break };
330            batch.extend_from_slice(&more);
331            batch.push(b'\n');
332        }
333        writer.write_all(&batch).await?;
334        writer.flush().await?;
335        if batch.capacity() > 4 * WRITE_BATCH_BYTES {
336            // Do not keep the memory of one very large message.
337            batch = Vec::new();
338        }
339    }
340    writer.shutdown().await.or(Ok(()))
341}
342
343#[derive(Deserialize)]
344struct CancelledParams {
345    #[serde(rename = "requestId")]
346    request_id: RequestId,
347}
348
349fn cancel(in_flight: &InFlight, params: Option<&serde_json::value::RawValue>) {
350    // Invalid cancellation notifications are ignored, as the spec requires.
351    let Some(params) = params else { return };
352    let Ok(params) = serde_json::from_str::<CancelledParams>(params.get()) else {
353        return;
354    };
355    if let Some(token) = lock(in_flight).get(&params.request_id) {
356        token.cancel();
357    }
358}
359
360fn lock(in_flight: &InFlight) -> std::sync::MutexGuard<'_, HashMap<RequestId, CancellationToken>> {
361    // A poisoned map is still structurally valid; keep serving.
362    in_flight
363        .lock()
364        .unwrap_or_else(std::sync::PoisonError::into_inner)
365}
366
367enum Line {
368    Complete,
369    TooLong,
370    Eof,
371}
372
373/// Reads one `\n`-terminated line into `buf` (without the terminator or a
374/// trailing `\r`). A line longer than `max` is consumed and discarded.
375async fn read_line<R: AsyncBufRead + Unpin>(
376    reader: &mut R,
377    buf: &mut Vec<u8>,
378    max: usize,
379) -> io::Result<Line> {
380    let mut too_long = false;
381    loop {
382        let available = reader.fill_buf().await?;
383        if available.is_empty() {
384            // EOF: a final unterminated line still counts.
385            return Ok(if too_long {
386                Line::TooLong
387            } else if buf.is_empty() {
388                Line::Eof
389            } else {
390                Line::Complete
391            });
392        }
393        let (chunk, found, used) = match available.iter().position(|&b| b == b'\n') {
394            Some(i) => (available.get(..i).unwrap_or_default(), true, i + 1),
395            None => (available, false, available.len()),
396        };
397        if !too_long {
398            if buf.len() + chunk.len() > max {
399                too_long = true;
400                buf.clear();
401            } else {
402                buf.extend_from_slice(chunk);
403            }
404        }
405        reader.consume(used);
406        if found {
407            if too_long {
408                return Ok(Line::TooLong);
409            }
410            if buf.last() == Some(&b'\r') {
411                buf.pop();
412            }
413            return Ok(Line::Complete);
414        }
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    #[tokio::test]
423    async fn reads_bounded_lines() {
424        let input: &[u8] = b"short\r\nthis-line-is-too-long\nok\nlast";
425        let mut reader = BufReader::with_capacity(4, input);
426        let mut buf = Vec::new();
427        let mut lines = Vec::new();
428        loop {
429            buf.clear();
430            match read_line(&mut reader, &mut buf, 8).await.unwrap() {
431                Line::Complete => lines.push(String::from_utf8(buf.clone()).unwrap()),
432                Line::TooLong => lines.push("<too long>".to_owned()),
433                Line::Eof => break,
434            }
435        }
436        assert_eq!(lines, ["short", "<too long>", "ok", "last"]);
437    }
438}