use std::collections::HashMap;
use std::io;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use serde::Deserialize;
use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
use tokio::sync::{Semaphore, mpsc};
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use crate::context::TransportContext;
use crate::error::McpError;
use crate::jsonrpc::{self, Message, Rejection, Response};
use crate::legacy::{ChannelPeer, LegacySession, PendingRequests};
use crate::notify::NotifyChannel;
use crate::server::{McpRequest, Server};
use crate::spec::{RequestId, methods};
const SHUTDOWN_GRACE: Duration = Duration::from_secs(1);
type InFlight = Arc<Mutex<HashMap<RequestId, CancellationToken>>>;
impl Server {
pub async fn serve_stdio(self) -> io::Result<()> {
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
explain_interactive_use();
}
serve_io(self, tokio::io::stdin(), tokio::io::stdout()).await
}
}
#[allow(deprecated)] fn set_log_level(
legacy: &Mutex<Option<LegacySession>>,
params: Option<&serde_json::value::RawValue>,
) -> Result<(), McpError> {
#[derive(Deserialize)]
struct Params {
level: crate::spec::LoggingLevel,
}
let params: Params = serde_json::from_str(params.map_or("{}", |p| p.get()))
.map_err(|_| McpError::invalid_params("Invalid log level"))?;
if let Some(session) = legacy
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_mut()
{
session.log_level = Some(params.level);
}
Ok(())
}
#[allow(clippy::print_stderr)]
fn explain_interactive_use() {
let program = std::env::current_exe()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.unwrap_or_else(|| "this-server".to_owned());
eprintln!(
"This is an MCP server: it reads JSON-RPC messages on stdin and is meant to be \
started by an MCP client.\n \
= help: try it with the MCP Inspector:\n \
npx @modelcontextprotocol/inspector --cli path/to/{program} --protocol-era modern \
--method tools/list\n\
(Waiting for messages on stdin; press Ctrl+C to stop.)"
);
}
pub async fn serve_io<R, W>(server: Server, reader: R, writer: W) -> io::Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin + Send + 'static,
{
let limits = server.limits().clone();
let (tx, rx) = mpsc::channel::<Vec<u8>>(limits.max_concurrent_calls.max(1));
let writer_task = tokio::spawn(write_loop(writer, rx));
let in_flight: InFlight = Arc::default();
let legacy: Arc<Mutex<Option<LegacySession>>> = Arc::default();
let peer = ChannelPeer {
pending: Arc::new(PendingRequests::default()),
owner: 0,
out: tx.clone(),
timeout: limits.tool_timeout,
};
let permits = Arc::new(Semaphore::new(limits.max_concurrent_calls.max(1)));
let mut tasks = JoinSet::new();
let mut reader = BufReader::with_capacity(READ_BUFFER_BYTES, reader);
let mut line = Vec::new();
let read_result = loop {
while tasks.try_join_next().is_some() {}
line.clear();
match read_line(&mut reader, &mut line, limits.max_message_bytes).await {
Ok(Line::Eof) => break Ok(()),
Ok(Line::TooLong) => {
let response =
Response::error(None, McpError::invalid_request("Message too large"));
if tx.send(response.to_vec()).await.is_err() {
break Ok(());
}
continue;
}
Ok(Line::Complete) => {}
Err(e) => break Err(e),
}
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
let message = match jsonrpc::parse(&line, &limits) {
Ok(message) => message,
Err(Rejection::Error { id, error }) => {
if tx.send(Response::error(id, error).to_vec()).await.is_err() {
break Ok(());
}
continue;
}
Err(Rejection::UnexpectedResponse) => {
let routed = jsonrpc::parse_client_response(&line)
.is_some_and(|response| peer.pending.complete(response, 0));
if !routed {
tracing::debug!("ignoring a JSON-RPC response that answers no pending request");
}
continue;
}
};
let request = match message {
Message::Notification(n) => {
if n.method == methods::NOTIFICATIONS_CANCELLED {
cancel(&in_flight, n.params.as_deref());
}
continue;
}
Message::Request(request) => request,
};
if request.method == "initialize" && server.legacy_clients_enabled() {
let response = match server.legacy_initialize(request.params.as_deref()) {
Ok((result, session)) => {
*legacy
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(session);
Response::Result {
id: request.id,
result,
}
}
Err(error) => Response::error(Some(request.id), error),
};
if tx.send(response.to_vec()).await.is_err() {
break Ok(());
}
continue;
}
let session = legacy
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if request.method == "logging/setLevel" && session.is_some() {
let response = match set_log_level(&legacy, request.params.as_deref()) {
Ok(()) => serde_json::value::to_raw_value(&serde_json::json!({}))
.map(|result| Response::Result {
id: request.id.clone(),
result,
})
.unwrap_or_else(|_| {
Response::error(Some(request.id.clone()), McpError::internal())
}),
Err(error) => Response::error(Some(request.id), error),
};
if tx.send(response.to_vec()).await.is_err() {
break Ok(());
}
continue;
}
let mut transport = TransportContext::default();
transport.insert(NotifyChannel(tx.clone()));
let token = CancellationToken::new();
let duplicate = match lock(&in_flight).entry(request.id.clone()) {
std::collections::hash_map::Entry::Occupied(_) => true,
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert(token.clone());
false
}
};
if duplicate {
let response = Response::error(
Some(request.id),
McpError::invalid_request("Duplicate request id"),
);
if tx.send(response.to_vec()).await.is_err() {
break Ok(());
}
continue;
}
let Ok(permit) = Arc::clone(&permits).acquire_owned().await else {
break Ok(());
};
let server = server.clone();
let tx = tx.clone();
let in_flight = Arc::clone(&in_flight);
let peer = peer.clone();
tasks.spawn(async move {
let id = request.id.clone();
let response = match session {
Some(session) => {
server
.handle_legacy(request, &session, transport, token, &peer)
.await
}
None => {
server
.handle(McpRequest {
message: Message::Request(request),
transport,
cancellation: token,
})
.await
}
};
lock(&in_flight).remove(&id);
if let Some(response) = response {
let _ = tx.send(response.to_vec()).await;
}
drop(permit);
});
};
let deadline = limits.tool_timeout + SHUTDOWN_GRACE;
if tokio::time::timeout(deadline, async {
while tasks.join_next().await.is_some() {}
})
.await
.is_err()
{
tracing::warn!("in-flight requests did not finish before shutdown; aborting them");
tasks.abort_all();
while tasks.join_next().await.is_some() {}
}
drop(peer);
drop(tx);
let write_result = writer_task
.await
.unwrap_or_else(|e| Err(io::Error::other(e)));
read_result.and(write_result)
}
const READ_BUFFER_BYTES: usize = 64 * 1024;
const WRITE_BATCH_BYTES: usize = 64 * 1024;
async fn write_loop<W: AsyncWrite + Unpin>(
mut writer: W,
mut rx: mpsc::Receiver<Vec<u8>>,
) -> io::Result<()> {
let mut batch = Vec::new();
while let Some(message) = rx.recv().await {
batch.clear();
batch.extend_from_slice(&message);
batch.push(b'\n');
while batch.len() < WRITE_BATCH_BYTES {
let Ok(more) = rx.try_recv() else { break };
batch.extend_from_slice(&more);
batch.push(b'\n');
}
writer.write_all(&batch).await?;
writer.flush().await?;
if batch.capacity() > 4 * WRITE_BATCH_BYTES {
batch = Vec::new();
}
}
writer.shutdown().await.or(Ok(()))
}
#[derive(Deserialize)]
struct CancelledParams {
#[serde(rename = "requestId")]
request_id: RequestId,
}
fn cancel(in_flight: &InFlight, params: Option<&serde_json::value::RawValue>) {
let Some(params) = params else { return };
let Ok(params) = serde_json::from_str::<CancelledParams>(params.get()) else {
return;
};
if let Some(token) = lock(in_flight).get(¶ms.request_id) {
token.cancel();
}
}
fn lock(in_flight: &InFlight) -> std::sync::MutexGuard<'_, HashMap<RequestId, CancellationToken>> {
in_flight
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
enum Line {
Complete,
TooLong,
Eof,
}
async fn read_line<R: AsyncBufRead + Unpin>(
reader: &mut R,
buf: &mut Vec<u8>,
max: usize,
) -> io::Result<Line> {
let mut too_long = false;
loop {
let available = reader.fill_buf().await?;
if available.is_empty() {
return Ok(if too_long {
Line::TooLong
} else if buf.is_empty() {
Line::Eof
} else {
Line::Complete
});
}
let (chunk, found, used) = match available.iter().position(|&b| b == b'\n') {
Some(i) => (available.get(..i).unwrap_or_default(), true, i + 1),
None => (available, false, available.len()),
};
if !too_long {
if buf.len() + chunk.len() > max {
too_long = true;
buf.clear();
} else {
buf.extend_from_slice(chunk);
}
}
reader.consume(used);
if found {
if too_long {
return Ok(Line::TooLong);
}
if buf.last() == Some(&b'\r') {
buf.pop();
}
return Ok(Line::Complete);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn reads_bounded_lines() {
let input: &[u8] = b"short\r\nthis-line-is-too-long\nok\nlast";
let mut reader = BufReader::with_capacity(4, input);
let mut buf = Vec::new();
let mut lines = Vec::new();
loop {
buf.clear();
match read_line(&mut reader, &mut buf, 8).await.unwrap() {
Line::Complete => lines.push(String::from_utf8(buf.clone()).unwrap()),
Line::TooLong => lines.push("<too long>".to_owned()),
Line::Eof => break,
}
}
assert_eq!(lines, ["short", "<too long>", "ok", "last"]);
}
}