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}