Skip to main content

jev_repl/
serve.rs

1//! The MCP server on a pipe: newline-delimited JSON-RPC in on stdin, the same out on stdout.
2//!
3//! Nothing but the protocol may be written to stdout — a stray `println!` is what breaks a
4//! hand-written MCP server — so everything the server wants to say goes to stderr.
5
6use std::io::{BufRead, Write};
7
8use tokio::sync::mpsc;
9use tokio::task::JoinSet;
10
11use crate::mcp::{self, Host};
12
13/// Answer messages until stdin closes.
14///
15/// Requests are answered as they finish rather than in the order they arrived: a `jev_eval` over a
16/// hundred cases must not hold up the `jev_check` behind it, and JSON-RPC matches replies by id.
17/// Stdin is read on a blocking thread, since a pipe that never closes would otherwise hold a
18/// runtime worker for the life of the process.
19pub async fn serve(host: Host) {
20    let (lines, mut incoming) = mpsc::unbounded_channel::<String>();
21    let reader = tokio::task::spawn_blocking(move || {
22        for line in std::io::stdin().lock().lines() {
23            let Ok(line) = line else { return };
24            if lines.send(line).is_err() {
25                return;
26            }
27        }
28    });
29
30    let mut answering = JoinSet::new();
31    let mut open = true;
32    // A reply goes out the moment it is ready, not when the next line happens to arrive: the
33    // client sends `initialize` and waits on the answer before it says anything else.
34    while open || !answering.is_empty() {
35        tokio::select! {
36            line = incoming.recv(), if open => match line {
37                Some(line) => {
38                    let host = host.clone();
39                    answering.spawn(async move { mcp::handle_line(&line, &host).await });
40                }
41                None => open = false,
42            },
43            Some(done) = answering.join_next(), if !answering.is_empty() => {
44                write(done.ok().flatten());
45            }
46        }
47    }
48    let _ = reader.await;
49}
50
51/// One reply on stdout, flushed: the client is waiting on a line, not on a buffer.
52fn write(reply: Option<String>) {
53    let Some(reply) = reply else { return };
54    let mut out = std::io::stdout().lock();
55    let _ = writeln!(out, "{reply}");
56    let _ = out.flush();
57}