Skip to main content

protocol_droid/adapters/primary/stdio/
serve.rs

1//! The loop: read a frame, route it, answer it, until the peer leaves.
2//!
3//! # What this is
4//!
5//! The half of the stdio transport that knows there is a conversation. It owns
6//! the read/answer/again cycle, decides what a frame that will not decode means
7//! for the connection, and is the only place in the crate that both holds a
8//! peer's bytes and decides what they mean. Everything below it —
9//! [`StdioSource`], [`StdioSink`], [`Diagnostics`] — is a byte layer that knows
10//! nothing about JSON-RPC, and the reply document itself lives in
11//! [`reply`].
12//!
13//! # stdout carries messages and nothing else
14//!
15//! The specification requires that a stdio server's standard output contain
16//! MCP messages only, because the peer is parsing that stream: anything else on
17//! it arrives as a frame that does not decode. Every human-readable fact this
18//! loop has — what it refused, why, and that the peer left cleanly — goes to
19//! [`Diagnostics`], which the constructor takes as an argument precisely so
20//! that it is a decision rather than an accident.
21//!
22//! The failure this prevents is silent, which is why
23//! `stdout_carries_nothing_but_replies` exists. A `println!` anywhere in this
24//! path corrupts a real peer's stream and no unit test notices, because a unit
25//! test asserting "the reply is correct" never looks at the bytes around the
26//! reply. That test reads the whole of stdout, splits it into frames, and
27//! requires every one of them to be a well-formed reply.
28//!
29//! # A malformed frame is answered, not fatal
30//!
31//! The decision this module had to make, and the one a reviewer will look for:
32//! **a frame that arrives complete but does not decode is answered with the
33//! JSON-RPC error [`DecodeError::code`] names, and the loop continues.**
34//!
35//! The alternative — drop the connection — is defensible, and for a stream
36//! protocol with length prefixes it is usually the right call. It is the wrong
37//! call *here*, for one reason: on a newline-delimited transport a frame that
38//! reached [`decode`] has already been delimited. The framing layer found its
39//! terminator, so the next frame starts at a byte offset this loop already
40//! knows. There is no desynchronisation to recover from, and nothing about the
41//! failure says the peer's *next* message will be any worse — one malformed
42//! frame is a bug in one sender, and a server that hangs up on it converts a
43//! recoverable mistake into a dead session for every later, well-formed
44//! request.
45//!
46//! What is fatal is the other failure: a [`SourceError`]. That is the stream
47//! itself, not a message in it, and [`StdioSource`] has already latched it —
48//! the bytes that would let us continue are gone.
49//!
50//! # The id is the peer's, all the way through
51//!
52//! The inbound frame is decoded to a [`WireId`], that same value is what
53//! [`RequestId`] is built from for the handler's [`RequestContext`], and the
54//! outbound document is built from the [`WireId`] again. Nothing re-derives it,
55//! nothing stringifies it on the way, and [`reply`] re-escapes it as JSON
56//! rather than debug-printing it. A reply whose id differs from its request's
57//! by even a character is a request the peer can never match to an answer.
58//!
59//! # Protocol neutrality
60//!
61//! This module names no method. `ping`, `tools/list` and `tools/call` all
62//! arrive as an opaque `&str` and are handed to
63//! [`Dispatcher::dispatch`](crate::application::server::Dispatcher::dispatch),
64//! which is the MCP-facing layer. Batching is the one protocol *shape* decision
65//! taken here, and it is delegated rather than made: the loop holds a
66//! [`BatchPolicy`] and asks it against the session's profile, because whether
67//! a batch is legal is a negotiation outcome and not a transport fact.
68
69use std::collections::{HashMap, VecDeque};
70use std::sync::Arc;
71
72use serde_json::Value;
73use tokio::io::{AsyncRead, AsyncWrite};
74
75use super::exchange::{StdioExchange, answer_of};
76use super::{Diagnostics, StdioConfig, StdioError, StdioSink, StdioSource};
77use crate::application::jsonrpc::{BatchPolicy, SourceError};
78use crate::application::mcp::request_meta::progress_token;
79use crate::application::server::{DispatchError, DynExchange};
80use crate::application::server::{
81    Dispatcher, ListKind, ListSubscription, PeerSession, RequestContext,
82};
83use crate::application::session::DynFrameSink;
84use crate::domain::RequestId;
85use crate::domain::ir::{IrErrorCode, JsonObject};
86use crate::domain::jsonrpc::reply::{self, ReplyError};
87use crate::domain::jsonrpc::{DecodeError, Envelope, Frame, WireId, decode};
88use crate::domain::session::{AbortReason, CancellationToken};
89
90/// The answer to a batch the session never negotiated.
91const BATCHING_REFUSED: &str = "batching was not negotiated for this session";
92
93/// The answer to a request whose `params` are not a JSON object.
94const PARAMS_NOT_AN_OBJECT: &str = "the request's params are not a JSON object";
95
96/// The answer to a handler result that could not be read back as JSON.
97const RESULT_UNREADABLE: &str = "the handler's result could not be serialised";
98
99/// What is said when a peer sends a reply shape. A reply is the answer to a
100/// request we made, and this loop wires no callback exchange yet, so there is
101/// nothing to hand it to.
102const IGNORED_REPLY: &str = "ignored a reply-shaped frame: no callback exchange is wired";
103
104/// What is said when the peer closes the stream cleanly.
105const PEER_CLOSED: &str = "the peer closed the stream; the session is over";
106
107mod stateless;
108
109/// Reads a peer's frames, routes them, and writes the answers back.
110///
111/// Generic over both streams so that the bytes can come from a child process,
112/// a pipe, a socket, or a test, and over the diagnostics channel so that
113/// "somewhere that is not stdout" is a decision made by the caller rather than
114/// hard-wired here. See the module docs for what the loop guarantees.
115pub struct ServeLoop<R, W, DW, P> {
116    source: StdioSource<R>,
117    /// Shared with the peer, so a handler's notifications — progress, log
118    /// records, list changes — go out on the same line-framed stream as the
119    /// replies. The sink's own lock serialises the two, one whole line at a
120    /// time, so a notification can never land inside a reply.
121    sink: Arc<StdioSink<W>>,
122    diagnostics: Diagnostics<DW>,
123    dispatcher: Arc<Dispatcher>,
124    peer: PeerSession,
125    policy: P,
126    /// This connection's standing interest in list changes, taken once the
127    /// handshake has settled what the peer negotiated, and dropped with the
128    /// loop.
129    lists: Option<ListSubscription>,
130    /// Requests this server has sent the peer and is waiting to hear back on.
131    exchange: Arc<StdioExchange<W>>,
132    /// Requests that arrived while a handler was waiting on the peer, served
133    /// in arrival order once it finishes.
134    backlog: VecDeque<String>,
135    /// How the input ended while a handler was running, if it did: `Ok` for a
136    /// clean close, `Err` for a failure, reported once the backlog is served.
137    ended: Option<Result<(), SourceError>>,
138    /// Open `2026-07-28` listen streams, by their request id as JSON text.
139    /// Each guard ends its subscription when the client cancels it or the
140    /// loop ends.
141    listens: HashMap<String, ListSubscription>,
142    /// The last session id handed to a stateless request.
143    stateless_ids: u64,
144}
145
146impl<R, W, DW, P> core::fmt::Debug for ServeLoop<R, W, DW, P> {
147    /// Prints the session and the routing table, and nothing about the
148    /// streams.
149    ///
150    /// The streams are `Debug`-able in practice and useless in a log: a peer's
151    /// buffered bytes and a `DuplexStream`'s internals say nothing about which
152    /// session is misbehaving, and requiring `W: Debug` to print the one field
153    /// that would is a bound nobody wants.
154    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
155        f.debug_struct("ServeLoop")
156            .field("peer", &self.peer)
157            .field("dispatcher", &self.dispatcher)
158            .finish_non_exhaustive()
159    }
160}
161
162impl<R, W, DW, P> ServeLoop<R, W, DW, P> {
163    /// The session every request on this connection is gated by.
164    ///
165    /// Read-only, and the only way to see it: the handshake *replaces* the
166    /// session's profile with the one resolved for the revision the peer
167    /// negotiated, so a caller who read the session before serving began would
168    /// be holding the ceiling rather than the answer.
169    #[must_use]
170    pub const fn peer(&self) -> &PeerSession {
171        &self.peer
172    }
173
174    /// Binds a peer's streams, a routing table, and a diagnostics channel.
175    ///
176    /// Both halves of the transport are built here from one [`StdioConfig`],
177    /// and that is the point: a caller who assembled a [`StdioSource`] and a
178    /// [`StdioSink`] separately could give them different ceilings, and
179    /// [`StdioConfig`] exists to make that impossible. This is the only
180    /// constructor, so there is no way to bypass it.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use std::sync::Arc;
186    ///
187    /// use protocol_droid::adapters::primary::stdio::{Diagnostics, ServeLoop, StdioConfig};
188    /// use protocol_droid::domain::MaxFrameSize;
189    ///
190    /// # fn example<R, W, DW, P>(
191    /// #     reader: R,
192    /// #     writer: W,
193    /// #     diagnostics: Diagnostics<DW>,
194    /// #     dispatcher: Arc<protocol_droid::application::server::Dispatcher>,
195    /// #     peer: protocol_droid::application::server::PeerSession,
196    /// #     policy: P,
197    /// # ) -> Result<(), Box<dyn std::error::Error>>
198    /// # where
199    /// #     W: tokio::io::AsyncWrite + Unpin + Send + 'static,
200    /// # {
201    /// let config = StdioConfig::new(MaxFrameSize::new(1024)?);
202    /// let server = ServeLoop::new(reader, writer, diagnostics, config, dispatcher, peer, policy);
203    /// # let _ = server;
204    /// # Ok(())
205    /// # }
206    /// ```
207    ///
208    /// The peer's sink is replaced with this loop's own. A handler that
209    /// publishes on any other sink is publishing somewhere the peer at the
210    /// other end of this pipe will never read.
211    #[must_use]
212    pub fn new(
213        reader: R,
214        writer: W,
215        diagnostics: Diagnostics<DW>,
216        config: StdioConfig,
217        dispatcher: Arc<Dispatcher>,
218        peer: PeerSession,
219        policy: P,
220    ) -> Self
221    where
222        W: AsyncWrite + Unpin + Send + 'static,
223    {
224        let sink = Arc::new(StdioSink::new(writer, config));
225        let exchange = Arc::new(StdioExchange::new(Arc::clone(&sink)));
226        let peer = peer
227            .with_sink(Arc::clone(&sink) as Arc<dyn DynFrameSink>)
228            .with_exchange(Arc::clone(&exchange) as Arc<dyn DynExchange>);
229        Self {
230            source: StdioSource::new(reader, config),
231            sink,
232            diagnostics,
233            dispatcher,
234            peer,
235            policy,
236            lists: None,
237            exchange,
238            backlog: VecDeque::new(),
239            ended: None,
240            listens: HashMap::new(),
241            // Clear of the handshake session's id, so a stateless request's
242            // idempotency keys can never coincide with the session's.
243            stateless_ids: 1 << 32,
244        }
245    }
246}
247
248impl<R, W, DW, P> ServeLoop<R, W, DW, P>
249where
250    R: AsyncRead + Unpin + Send,
251    W: AsyncWrite + Unpin + Send + 'static,
252    DW: AsyncWrite + Unpin + Send,
253    P: BatchPolicy + Send + Sync,
254{
255    /// Serves the peer until it closes the stream.
256    ///
257    /// Returns `Ok` when the peer closed and `Err` when the transport failed.
258    /// Those are different events and the distinction is the point: a peer that
259    /// exits normally and a pipe that breaks are the same `Ok(0)` from the
260    /// reader's point of view, and only [`StdioSource`] knows which one it
261    /// saw. Collapsing them here would leave a caller unable to tell a clean
262    /// shutdown from a crash.
263    ///
264    /// # Errors
265    ///
266    /// [`ServeError::Source`] when the peer's stream failed, which is latched
267    /// and unrecoverable. [`ServeError::Reply`] when a reply could not be
268    /// written, including a reply the configured ceiling refused —
269    /// [`StdioError::ReplyTooLarge`](StdioError::ReplyTooLarge) — and
270    /// [`ServeError::Diagnostics`] when the log could not be written. All three
271    /// are terminal: continuing to serve a peer we can no longer talk to, or
272    /// describe, would be a loop that fails silently.
273    ///
274    /// # Examples
275    ///
276    /// ```no_run
277    /// # use std::sync::Arc;
278    /// # use protocol_droid::adapters::primary::stdio::{Diagnostics, ServeLoop, ServeReport, StdioConfig};
279    /// # use protocol_droid::domain::MaxFrameSize;
280    /// # async fn example<R, W, P>(
281    /// #     reader: R,
282    /// #     writer: W,
283    /// #     dispatcher: Arc<protocol_droid::application::server::Dispatcher>,
284    /// #     peer: protocol_droid::application::server::PeerSession,
285    /// #     policy: P,
286    /// # ) -> Result<ServeReport, Box<dyn std::error::Error>>
287    /// # where
288    /// #     R: tokio::io::AsyncRead + Unpin + Send,
289    /// #     W: tokio::io::AsyncWrite + Unpin + Send + 'static,
290    /// #     P: protocol_droid::application::jsonrpc::BatchPolicy + Send + Sync,
291    /// # {
292    /// let server = ServeLoop::new(
293    ///     reader,
294    ///     writer,
295    ///     Diagnostics::stderr(),
296    ///     StdioConfig::default(),
297    ///     dispatcher,
298    ///     peer,
299    ///     policy,
300    /// );
301    ///
302    /// let mut server = server;
303    /// let report = server.serve().await?;
304    /// // The report is returned rather than logged. This transport's stdout
305    /// // carries MCP messages and nothing else, so a caller that wants the
306    /// // numbers goes to its own log — the loop never chooses a sink.
307    /// # Ok(report)
308    /// # }
309    /// ```
310    pub async fn serve(&mut self) -> Result<ServeReport, ServeError> {
311        let mut report = ServeReport::default();
312        loop {
313            // `Ok(None)` here is the peer closing, and nothing else. The
314            // decoder's "no terminator yet" is a different `None` and never
315            // reaches this call — see the `stdio` module docs for why
316            // forwarding it would be a quiet way to drop half of every
317            // conversation.
318            let frame = if let Some(queued) = self.backlog.pop_front() {
319                queued
320            } else {
321                match self.ended.take() {
322                    Some(Err(error)) => return Err(ServeError::Source(error)),
323                    Some(Ok(())) => {
324                        self.note(PEER_CLOSED).await?;
325                        return Ok(report);
326                    }
327                    None => {}
328                }
329                let next = self.source.next_frame().await;
330                let Some(frame) = next.map_err(ServeError::Source)? else {
331                    self.exchange.close();
332                    self.note(PEER_CLOSED).await?;
333                    return Ok(report);
334                };
335                frame
336            };
337            report.frames += 1;
338            if self.take_reply(&frame) {
339                continue;
340            }
341            self.serve_frame(&frame, &mut report).await?;
342        }
343    }
344
345    /// Serves one whole frame, and writes at most one document back.
346    async fn serve_frame(
347        &mut self,
348        frame: &str,
349        report: &mut ServeReport,
350    ) -> Result<(), ServeError> {
351        let decoded = match decode(frame.as_bytes()) {
352            Ok(decoded) => decoded,
353            // Answered, not fatal. The frame was complete — the framing layer
354            // found its terminator — so the next one starts at an offset this
355            // loop already knows. See the module docs.
356            Err(error) => return self.answer_undecodable(error, report).await,
357        };
358
359        let document = match decoded {
360            Frame::Single(envelope) => self.answer(&envelope, report).await?,
361            Frame::Batch(envelopes) => self.answer_batch(&envelopes, report).await?,
362        };
363
364        let Some(document) = document else {
365            return Ok(());
366        };
367        // Serialised here, once, and handed to the sink as text. The sink
368        // checks the ceiling against these bytes, so the check has to see the
369        // document that is actually written rather than the members that built
370        // it.
371        let document = document.to_string();
372        self.sink.send(&document).await.map_err(ServeError::Reply)?;
373        report.replies += 1;
374        Ok(())
375    }
376
377    /// Answers a frame that is a complete frame and not a JSON-RPC message.
378    ///
379    /// The id is `null` because there was no id to read, and the loop carries
380    /// on — both of which are the decision the module docs argue for.
381    async fn answer_undecodable(
382        &self,
383        error: DecodeError,
384        report: &mut ServeReport,
385    ) -> Result<(), ServeError> {
386        let code = error.code();
387        let message = error.to_string();
388        // The message is this crate's own, not the peer's bytes: `decode`
389        // reports a static description and never echoes the input, which is
390        // what makes a log line that a peer cannot end.
391        self.note(&format!("undecodable frame: {message}")).await?;
392        let document = reply::unidentified_error_reply(code, &message).to_string();
393        self.sink.send(&document).await.map_err(ServeError::Reply)?;
394        report.replies += 1;
395        Ok(())
396    }
397
398    /// Serves every message in a batch, and collects their answers.
399    ///
400    /// A batch is answered with one document carrying one answer per
401    /// request-bearing message, which is what JSON-RPC 2.0 requires. A batch
402    /// of nothing but notifications produces no document at all: there is no
403    /// request to answer, and writing an empty array would be a message the
404    /// peer never asked for.
405    async fn answer_batch(
406        &mut self,
407        envelopes: &[Envelope<'_>],
408        report: &mut ServeReport,
409    ) -> Result<Option<Value>, ServeError> {
410        // Delegated, not decided here. Whether a batch is legal is a
411        // negotiation outcome, and the profile is the session's — a loop that
412        // answered batches from its own belief about the revision would be
413        // exactly the peer-side version of resolving a profile from the wrong
414        // inputs.
415        if !self.policy.allows_batch(&self.peer.profile()) {
416            self.note(BATCHING_REFUSED).await?;
417            return Ok(Some(reply::unidentified_error_reply(
418                IrErrorCode::InvalidRequest,
419                BATCHING_REFUSED,
420            )));
421        }
422        let mut answers = Vec::with_capacity(envelopes.len());
423        for envelope in envelopes {
424            if let Some(answer) = self.answer(envelope, report).await? {
425                answers.push(answer);
426            }
427        }
428        Ok((!answers.is_empty()).then_some(Value::Array(answers)))
429    }
430
431    /// Serves one message, and returns the document that answers it.
432    ///
433    /// `None` means the message wanted no answer: a notification, a reply
434    /// shape, or — for a batch — a message inside a batch that turned out not
435    /// to be a request.
436    async fn answer(
437        &mut self,
438        envelope: &Envelope<'_>,
439        report: &mut ServeReport,
440    ) -> Result<Option<Value>, ServeError> {
441        // A reply shape is the peer answering a request of ours. This loop
442        // wires no callback exchange, so there is nowhere to put it; the frame
443        // is noted and dropped rather than treated as an error, because the
444        // peer did nothing wrong.
445        if envelope.routeable().is_none() {
446            self.note(IGNORED_REPLY).await?;
447            return Ok(None);
448        }
449        report.requests += 1;
450
451        let params = match envelope.params() {
452            None => JsonObject::default(),
453            // Decoded here, at the boundary, into the type the dispatcher
454            // takes. A `params` that is valid JSON but not an object is a
455            // well-formed request with a bad argument, which is `-32602` and
456            // not a parse failure.
457            Some(raw) => match JsonObject::new(raw.get()) {
458                Ok(params) => params,
459                Err(_) => {
460                    return Ok(refusal(
461                        envelope.id(),
462                        IrErrorCode::InvalidParams,
463                        PARAMS_NOT_AN_OBJECT,
464                    ));
465                }
466            },
467        };
468
469        let method: &str = envelope.method_str().unwrap_or_default();
470
471        // A cancellation that names an open listen stream ends it. Any other
472        // cancellation names a request that is not in flight — the loop only
473        // reaches here between requests — so it is a no-op, as the
474        // specification asks.
475        if method == "notifications/cancelled" {
476            let _ = self.cancel_listen(&params);
477            return Ok(None);
478        }
479
480        // SEP-2575: a request that names its own revision in `_meta` is served
481        // from that alone, with no handshake behind it.
482        if names_own_revision(&params) {
483            return self.answer_stateless(envelope.id(), method, &params).await;
484        }
485
486        // The id is carried through untouched: the peer's `WireId` becomes
487        // the handler's `RequestId` and is written back from the same value.
488        let context = RequestContext::for_request_with_state(
489            &self.peer,
490            RequestId::from_wire(envelope.id()),
491            CancellationToken::new(),
492            progress_token(&params),
493            Arc::clone(self.dispatcher.state()),
494        );
495
496        // `initialize` is the one method that changes the session rather than
497        // being answered from it. The handshake resolves a profile for the
498        // revision the client offered, and every gate after this point has to
499        // read *that* one — a peer that negotiated `2024-11-05` must not be
500        // gated on a profile resolved for whatever the process was built with.
501        if method == "initialize" {
502            let outcome = self.dispatcher.handshake(&context, &params);
503
504            // A notification is served and never answered, exactly as below.
505            let Some(id) = envelope.id() else {
506                return Ok(None);
507            };
508            return Ok(Some(match outcome {
509                Ok((reply, narrowed)) => {
510                    // Subscribed against the negotiated profile, so a peer that
511                    // did not negotiate `listChanged` for a kind is never sent
512                    // one. Replacing an earlier subscription drops it.
513                    self.lists = Some(self.dispatcher.server().handle().subscribe(
514                        narrowed.sink(),
515                        narrowed.profile(),
516                        &ListKind::ALL,
517                        None,
518                    ));
519                    self.peer = narrowed;
520                    match reply::result_reply(id, &reply) {
521                        Ok(document) => document,
522                        Err(ReplyError::UnreadableResult) => {
523                            reply::error_reply(id, IrErrorCode::InternalError, RESULT_UNREADABLE)
524                        }
525                    }
526                }
527                Err(error) => {
528                    let (code, message) = error.as_wire();
529                    self.note(&format!("refused a handshake: {message}"))
530                        .await?;
531                    reply::error_reply(id, code, &message)
532                }
533            }));
534        }
535
536        let (outcome, cancelled) = self.run_handler(&context, method, &params).await;
537        // A cancelled request is answered with nothing: the specification has
538        // the receiver send no further messages for it, and the client has
539        // already stopped waiting.
540        if cancelled {
541            return Ok(None);
542        }
543
544        // A notification is served and never answered — not even with an
545        // error, which is what JSON-RPC 2.0 requires, and which is also what
546        // stops a peer that floods notifications from filling stdout with
547        // messages nobody is waiting for.
548        let Some(id) = envelope.id() else {
549            return Ok(None);
550        };
551
552        Ok(Some(match outcome {
553            Ok(result) => match reply::result_reply(id, &result) {
554                Ok(document) => document,
555                Err(ReplyError::UnreadableResult) => {
556                    reply::error_reply(id, IrErrorCode::InternalError, RESULT_UNREADABLE)
557                }
558            },
559            Err(error) => {
560                let (code, message) = error.as_wire();
561                // The message is the dispatcher's own text, never the method
562                // name the peer chose — see `note`.
563                self.note(&format!("refused a request: {message}")).await?;
564                reply::error_reply(id, code, &message)
565            }
566        }))
567    }
568
569    /// Runs a handler while still listening to the peer.
570    ///
571    /// A handler that asks the peer to sample or elicit is waiting on an answer
572    /// that arrives on stdin, so the loop cannot simply await it: nothing would
573    /// read the answer. Instead it races the handler against the next frame. A
574    /// frame that answers one of this server's requests is delivered to the
575    /// exchange; any other frame is queued and served after this handler, so
576    /// replies still leave in the order their requests arrived.
577    async fn run_handler(
578        &mut self,
579        context: &RequestContext,
580        method: &str,
581        params: &JsonObject,
582    ) -> (Result<JsonObject, DispatchError>, bool) {
583        let dispatcher = Arc::clone(&self.dispatcher);
584        let call = dispatcher.dispatch(context, method, params);
585        tokio::pin!(call);
586        let mut cancelled = false;
587        loop {
588            let listening = self.ended.is_none();
589            let step = tokio::select! {
590                biased;
591                outcome = &mut call => Step::Done(outcome),
592                frame = self.source.next_frame(), if listening => Step::Frame(frame),
593            };
594            match step {
595                Step::Done(outcome) => return (outcome, cancelled),
596                Step::Frame(Ok(Some(frame))) => {
597                    if self.take_reply(&frame) {
598                        continue;
599                    }
600                    if cancels(&frame, context) {
601                        context.cancellation().cancel(AbortReason::Cancelled(None));
602                        cancelled = true;
603                        continue;
604                    }
605                    self.backlog.push_back(frame);
606                }
607                Step::Frame(Ok(None)) => {
608                    self.exchange.close();
609                    self.ended = Some(Ok(()));
610                }
611                Step::Frame(Err(error)) => {
612                    self.exchange.close();
613                    self.ended = Some(Err(error));
614                }
615            }
616        }
617    }
618
619    /// Delivers `frame` to the exchange if it answers one of this server's
620    /// requests, and reports whether it did.
621    ///
622    /// A reply is a JSON object with an `id`, no `method`, and a `result` or
623    /// an `error`. Recognised here, before decoding, because the request-side
624    /// decoder has no shape for a reply and would answer it as a bad request.
625    fn take_reply(&self, frame: &str) -> bool {
626        let Ok(serde_json::Value::Object(members)) = serde_json::from_str::<Value>(frame) else {
627            return false;
628        };
629        if members.contains_key("method")
630            || !(members.contains_key("result") || members.contains_key("error"))
631        {
632            return false;
633        }
634        if let Some(id) = members.get("id").and_then(Value::as_i64) {
635            self.exchange
636                .resolve(id, answer_of(&Value::Object(members)));
637        }
638        true
639    }
640
641    /// Writes one line to the diagnostics channel.
642    ///
643    /// Nothing a peer said is ever passed here. A method name is a JSON string
644    /// the peer chose, and JSON permits a newline inside one, so logging it
645    /// would let a peer close this log line and open one of its own. Every
646    /// string this loop logs is a constant or a `thiserror` message, which is
647    /// the only reason the channel can be read as a log at all.
648    async fn note(&self, line: &str) -> Result<(), ServeError> {
649        let mut buffered = String::with_capacity(line.len() + 1);
650        buffered.push_str(line);
651        buffered.push('\n');
652        self.diagnostics
653            .write_all(buffered.as_bytes())
654            .await
655            .map_err(ServeError::Diagnostics)
656    }
657}
658
659/// The reply for a request, or nothing at all for a notification.
660///
661/// One place decides that, because the rule is easy to get wrong in either
662/// direction: answering a notification is a protocol violation, and staying
663/// silent on a request leaves a peer waiting forever for an answer that will
664/// never come.
665fn refusal(id: Option<&WireId<'_>>, code: IrErrorCode, message: &str) -> Option<Value> {
666    id.map(|id| reply::error_reply(id, code, message))
667}
668
669/// The handler's view of the peer's id.
670///
671/// A notification has none, and [`RequestContext`] insists on one because a
672/// handler may be handed a progress token or a cancellation token and needs
673/// something to correlate against. A notification can be given neither, so
674/// nothing it does can reach the peer and the value is unobservable — it
675/// exists only so the context can be built at all.
676///
677/// The mapping itself is [`RequestId::from_wire`], in the domain, and the
678/// refusal mapping is [`DispatchError::as_wire`], in the application layer:
679/// both are properties of a value rather than of the way this loop writes it
680/// down, and the HTTP router needs them for the same reasons this loop does.
681///
682/// What a finished session did.
683///
684/// Counted, not merely logged, so that "the loop ran once and served nothing"
685/// is a number a caller can assert on rather than an absence nobody notices.
686#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
687pub struct ServeReport {
688    /// Frames read off the peer's stream, including ones that did not decode.
689    pub frames: usize,
690    /// Messages routed to a handler, including notifications and refusals.
691    ///
692    /// Counts the *messages*, so a batch of three raises this by three.
693    pub requests: usize,
694    /// Documents written to the peer's stream.
695    ///
696    /// Counts the *documents*, so a batch of three requests raises this by
697    /// one. The two counts differ by design: they answer different questions,
698    /// and a report that used one number for both would be answering neither.
699    pub replies: usize,
700}
701
702/// Why the session ended when it was not the peer ending it.
703///
704/// Every variant is terminal. The loop cannot tell a peer that has gone how it
705/// went, and carrying on would mean serving a conversation nobody is in.
706#[derive(Debug, thiserror::Error)]
707pub enum ServeError {
708    /// The peer's stream could not be read.
709    ///
710    /// Latched by [`StdioSource`], so this is where the session ends rather
711    /// than a frame that will be tried again.
712    #[error("the peer's stream could not be read: {0}")]
713    Source(#[source] SourceError),
714
715    /// A reply could not be written.
716    ///
717    /// Includes [`StdioError::ReplyTooLarge`](StdioError::ReplyTooLarge), which
718    /// is refused *before* any byte reaches the stream — so a peer never sees a
719    /// truncated frame, which on a newline protocol it could not detect.
720    #[error("a reply could not be written: {0}")]
721    Reply(#[source] StdioError),
722
723    /// The diagnostics channel could not be written.
724    ///
725    /// Fatal rather than ignored, which is the opposite of the advice
726    /// [`Diagnostics::write_all`] offers a caller. The loop is the top of the
727    /// process: if it cannot say what it is doing, it should stop doing it,
728    /// rather than serve requests from a state only it knows about.
729    #[error("the diagnostics channel could not be written: {0}")]
730    Diagnostics(#[source] StdioError),
731}
732
733/// Whether `params` names its own revision in `_meta` (SEP-2575).
734fn names_own_revision(params: &JsonObject) -> bool {
735    serde_json::from_str::<Value>(params.as_str())
736        .is_ok_and(|parsed| crate::application::mcp::request_meta::declares_version(&parsed))
737}
738
739/// Whether `frame` is a `notifications/cancelled` naming the request `context`
740/// is serving.
741fn cancels(frame: &str, context: &RequestContext) -> bool {
742    let Ok(parsed) = serde_json::from_str::<Value>(frame) else {
743        return false;
744    };
745    parsed.get("method").and_then(Value::as_str) == Some("notifications/cancelled")
746        && parsed.pointer("/params/requestId").is_some_and(|target| {
747            serde_json::from_str::<Value>(&context.request_id().to_json())
748                .is_ok_and(|id| id == *target)
749        })
750}
751
752/// What one turn of [`ServeLoop::run_handler`] produced.
753enum Step<T> {
754    /// The handler finished.
755    Done(T),
756    /// A frame arrived first.
757    Frame(Result<Option<String>, SourceError>),
758}
759
760#[cfg(test)]
761mod tests {
762    #![expect(
763        clippy::unused_async_trait_impl,
764        reason = "FrameSink::send is an async trait method, so a do-nothing sink carries \
765                  the async signature whether or not it awaits anything"
766    )]
767    use std::collections::VecDeque;
768    use std::future::Future;
769    use std::io;
770    use std::pin::Pin;
771    use std::sync::Arc;
772    use std::task::{Context, Poll};
773
774    use assert_matches::assert_matches;
775    use serde_json::{Map, Value};
776    use tokio::io::{AsyncRead, ReadBuf};
777
778    use super::{ServeError, ServeLoop, ServeReport};
779    use crate::adapters::primary::stdio::testing::Capture;
780    use crate::adapters::primary::stdio::{Diagnostics, StdioConfig, StdioError};
781    use crate::application::jsonrpc::{AllowBatching, BatchPolicy, DenyBatching, SourceError};
782    use crate::application::server::{
783        CallbackExchange, Dispatcher, ExchangeError, PeerSession, Server, ToolHandler,
784    };
785    use crate::application::session::{FrameSink, SinkError};
786    use crate::domain::Feature;
787    use crate::domain::feature::{Negotiation, resolve};
788    use crate::domain::ir::{JsonObject, JsonObjectError};
789    use crate::domain::registry::Tool;
790    use crate::domain::session::SessionId;
791    use crate::domain::{
792        CapabilitySet, MaxFrameSize, MaxFrameSizeError, ProtocolVersion, TransportKind,
793    };
794
795    /// The tool every session in this file can see.
796    const ECHO: &str = "echo";
797
798    /// A ceiling comfortably above every reply these tests produce.
799    const ROOMY: u32 = 64 * 1024;
800
801    /// What a test here can fail with, so a fallible call still propagates
802    /// instead of unwrapping.
803    #[derive(Debug)]
804    enum Failure {
805        /// A fixture was not a JSON object.
806        Object(JsonObjectError),
807        /// A ceiling the test asked for was refused.
808        Limit(MaxFrameSizeError),
809        /// The loop failed, or stdout carried something that is not JSON.
810        /// Both are failures of the same claim: what reached the peer is a
811        /// sequence of well-formed frames.
812        Json(String),
813        /// A test needed a frame or a member and there was none.
814        Missing(&'static str),
815    }
816
817    impl std::fmt::Display for Failure {
818        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
819            match self {
820                Self::Object(error) => write!(f, "bad fixture: {error}"),
821                Self::Limit(error) => write!(f, "bad ceiling: {error}"),
822                Self::Json(reason) => write!(f, "not a JSON frame: {reason}"),
823                Self::Missing(what) => write!(f, "expected {what}"),
824            }
825        }
826    }
827
828    impl std::error::Error for Failure {}
829
830    impl From<JsonObjectError> for Failure {
831        fn from(error: JsonObjectError) -> Self {
832            Self::Object(error)
833        }
834    }
835
836    impl From<MaxFrameSizeError> for Failure {
837        fn from(error: MaxFrameSizeError) -> Self {
838            Self::Limit(error)
839        }
840    }
841
842    impl From<ServeError> for Failure {
843        fn from(error: ServeError) -> Self {
844            Self::Json(error.to_string())
845        }
846    }
847
848    /// A reader that hands over one scripted chunk per read, then EOF.
849    ///
850    /// A pipe splits wherever it likes, so a fixture that delivered everything
851    /// in one read would never exercise the loop re-entering a half-built
852    /// frame, and one that delivered a frame per read would never exercise a
853    /// frame the decoder had already seen the end of. Both shapes are used
854    /// below, which is why the fixture is scripted rather than a buffer.
855    struct Trickle {
856        chunks: VecDeque<Vec<u8>>,
857    }
858
859    impl Trickle {
860        /// Serves `chunks` in order, one read each.
861        fn new(chunks: &[&str]) -> Self {
862            Self {
863                chunks: chunks
864                    .iter()
865                    .map(|chunk| chunk.as_bytes().to_vec())
866                    .collect(),
867            }
868        }
869    }
870
871    impl AsyncRead for Trickle {
872        fn poll_read(
873            self: Pin<&mut Self>,
874            _cx: &mut Context<'_>,
875            buf: &mut ReadBuf<'_>,
876        ) -> Poll<io::Result<()>> {
877            // Putting nothing in the buffer is how `AsyncRead` spells `Ok(0)`,
878            // which is how this fixture reports end-of-stream.
879            let Some(chunk) = self.get_mut().chunks.pop_front() else {
880                return Poll::Ready(Ok(()));
881            };
882            buf.put_slice(chunk.as_slice());
883            Poll::Ready(Ok(()))
884        }
885    }
886
887    /// A reader that fails on the first read.
888    ///
889    /// The counterpart to [`Trickle`]. Without it, "a broken stream is not a
890    /// clean close" could only be tested with a real pipe, and a test that
891    /// needs a real pipe fails on a platform nobody remembered to check.
892    struct Rupture;
893
894    impl AsyncRead for Rupture {
895        fn poll_read(
896            self: Pin<&mut Self>,
897            _cx: &mut Context<'_>,
898            _buf: &mut ReadBuf<'_>,
899        ) -> Poll<io::Result<()>> {
900            Poll::Ready(Err(io::Error::other("injected read failure")))
901        }
902    }
903
904    /// An exchange that refuses every callback.
905    ///
906    /// This loop wires none, so nothing should ever reach it; the fixture
907    /// exists so that a handler which did try would fail loudly rather than
908    /// hang.
909    struct NoExchange;
910
911    impl CallbackExchange for NoExchange {
912        fn exchange(
913            &self,
914            _method: &str,
915            _params: &JsonObject,
916        ) -> impl Future<Output = Result<JsonObject, ExchangeError>> + Send {
917            core::future::ready(Err(ExchangeError::new("no callbacks in this test")))
918        }
919    }
920
921    /// A sink the session can hold but this loop never writes to.
922    ///
923    /// The loop's replies go through its own [`StdioSink`](super::StdioSink).
924    /// This one is only here because a [`PeerSession`] wants somewhere to put
925    /// a server-initiated request, and a real stream would make the test
926    /// depend on the order two writers happen to run in.
927    struct NoSink;
928
929    impl FrameSink for NoSink {
930        async fn send(&self, _frame: &str) -> Result<(), SinkError> {
931            Ok(())
932        }
933    }
934
935    /// A handler that echoes its arguments back, so an assertion about the
936    /// answer is a claim about the hop and not about a `format!` agreeing with
937    /// itself.
938    fn echo_handler() -> ToolHandler {
939        Arc::new(|_ctx, arguments| Box::pin(async move { arguments }))
940    }
941
942    /// A routing table with one tool on it.
943    fn dispatcher() -> Result<Arc<Dispatcher>, Failure> {
944        let tool = Tool::new(ECHO, r#"{"type":"object"}"#)?;
945        let server = Server::builder("serve-test")
946            .tool(tool, echo_handler())
947            .build();
948        Ok(Arc::new(Dispatcher::new(Arc::new(server))))
949    }
950
951    /// A session with every feature its revision has.
952    ///
953    /// A server advertising less would make a gate look like a routing failure
954    /// in any test that tripped over it.
955    fn peer() -> PeerSession {
956        let version = ProtocolVersion::V2024_11_05;
957        PeerSession::new(
958            version,
959            resolve(Negotiation {
960                version,
961                client_caps: CapabilitySet::all_for(version),
962                server_caps: CapabilitySet::all_for(version),
963                transport: TransportKind::Stdio,
964            }),
965            SessionId::new(1),
966            TransportKind::Stdio,
967            Arc::new(NoExchange),
968            Arc::new(NoSink),
969        )
970    }
971
972    /// The loop, plus a handle on each of its two output streams.
973    type Harness<P> = (ServeLoop<Trickle, Capture, Capture, P>, Capture, Capture);
974
975    /// A loop over `chunks` with a ceiling of `limit`.
976    fn harness<P>(chunks: &[&str], policy: P, limit: u32) -> Result<Harness<P>, Failure> {
977        let stdout = Capture::shared();
978        let stderr = Capture::shared();
979        let server = ServeLoop::new(
980            Trickle::new(chunks),
981            stdout.clone(),
982            Diagnostics::new(stderr.clone()),
983            StdioConfig::new(MaxFrameSize::new(limit)?),
984            dispatcher()?,
985            peer(),
986            policy,
987        );
988        Ok((server, stdout, stderr))
989    }
990
991    /// Runs a loop to its end, and hands back what it wrote to each stream.
992    async fn run<P>(
993        chunks: &[&str],
994        policy: P,
995    ) -> Result<(Result<ServeReport, ServeError>, Capture, Capture), Failure>
996    where
997        P: BatchPolicy + Send + Sync,
998    {
999        run_under(chunks, policy, ROOMY).await
1000    }
1001
1002    /// [`run`] with the ceiling spelled out, for the one test that needs a
1003    /// reply to be refused.
1004    async fn run_under<P>(
1005        chunks: &[&str],
1006        policy: P,
1007        limit: u32,
1008    ) -> Result<(Result<ServeReport, ServeError>, Capture, Capture), Failure>
1009    where
1010        P: BatchPolicy + Send + Sync,
1011    {
1012        let (mut server, stdout, stderr) = harness(chunks, policy, limit)?;
1013        let report = server.serve().await;
1014        Ok((report, stdout, stderr))
1015    }
1016
1017    /// Every frame the loop wrote to the peer's stream, parsed.
1018    ///
1019    /// A line here that does not parse is the whole point of the purity test:
1020    /// it is what a `println!` anywhere in the serve path looks like from the
1021    /// peer's side, and it is invisible to any test that only checks the reply
1022    /// it asked for.
1023    fn written(stdout: &Capture) -> Result<Vec<Value>, Failure> {
1024        stdout
1025            .contents()
1026            .split('\n')
1027            .filter(|line| !line.is_empty())
1028            .map(|line| {
1029                serde_json::from_str::<Value>(line)
1030                    .map_err(|error| Failure::Json(error.to_string()))
1031            })
1032            .collect()
1033    }
1034
1035    /// The `code` member of a frame's `error`.
1036    fn error_code(frame: &Value) -> Result<i64, Failure> {
1037        frame
1038            .get("error")
1039            .and_then(|error| error.get("code"))
1040            .and_then(Value::as_i64)
1041            .ok_or(Failure::Missing("an error code"))
1042    }
1043
1044    /// The `id` member of a frame.
1045    fn id_of(frame: &Value) -> Result<Value, Failure> {
1046        frame.get("id").cloned().ok_or(Failure::Missing("an id"))
1047    }
1048
1049    /// One request in, one reply out, and the reply belongs to the request.
1050    #[tokio::test]
1051    async fn a_request_in_produces_a_reply_out() -> Result<(), Failure> {
1052        let (served, stdout, stderr) = run(
1053            &["{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n"],
1054            AllowBatching,
1055        )
1056        .await?;
1057        let report = served?;
1058        let frames = written(&stdout)?;
1059
1060        assert_eq!(frames.len(), 1);
1061        assert_eq!(
1062            report,
1063            ServeReport {
1064                frames: 1,
1065                requests: 1,
1066                replies: 1
1067            }
1068        );
1069        let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
1070        assert_eq!(id_of(reply)?, Value::from(1));
1071        assert_eq!(reply.get("result"), Some(&Value::Object(Map::default())));
1072        // The log really was written to, so the purity claim is not passing
1073        // because the loop happens to be silent.
1074        assert!(
1075            stderr.contents().contains("closed"),
1076            "{}",
1077            stderr.contents()
1078        );
1079        Ok(())
1080    }
1081
1082    /// A frame is a line, and a line is only a line once its terminator has
1083    /// arrived. The chunk here is a whole JSON-RPC message and the loop still
1084    /// reads nothing, because the framing layer cannot tell a peer that is
1085    /// mid-message from a peer that has gone away. The report is therefore the
1086    /// same one a session that received no bytes at all produces, and that is
1087    /// the documented contract rather than an accident of this fixture.
1088    #[tokio::test]
1089    async fn an_unterminated_line_is_not_a_frame() -> Result<(), Failure> {
1090        let (served, stdout, stderr) = run(
1091            &[r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#],
1092            AllowBatching,
1093        )
1094        .await?;
1095        let report = served?;
1096
1097        assert_eq!(
1098            report,
1099            ServeReport::default(),
1100            "a message that has not finished arriving is not served"
1101        );
1102        assert_eq!(stdout.contents(), "", "nothing was answered");
1103        assert!(
1104            stderr.contents().contains("closed"),
1105            "{}",
1106            stderr.contents()
1107        );
1108        Ok(())
1109    }
1110
1111    /// A loop that serves one message and returns passes every other test in
1112    /// this file. The first chunk here holds two whole frames and the second a
1113    /// third, which also covers a frame the decoder had already seen the end of
1114    /// before the loop asked again.
1115    #[tokio::test]
1116    async fn three_requests_in_sequence_each_get_an_answer() -> Result<(), Failure> {
1117        let (served, stdout, _stderr) = run(
1118            &[
1119                "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n\
1120                 {\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"ping\"}\n",
1121                "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"ping\"}\n",
1122            ],
1123            AllowBatching,
1124        )
1125        .await?;
1126        let report = served?;
1127        let frames = written(&stdout)?;
1128
1129        assert_eq!(
1130            report.frames, 3,
1131            "three frames arrived in two reads, and the report counts frames"
1132        );
1133        assert_eq!(report.requests, 3);
1134        assert_eq!(report.replies, 3);
1135        let ids: Vec<Value> = frames.iter().map(id_of).collect::<Result<_, _>>()?;
1136        assert_eq!(ids, vec![Value::from(1), Value::from(2), Value::from(3)]);
1137        Ok(())
1138    }
1139
1140    /// A pipe splits wherever it likes, so a request arrives in pieces. The
1141    /// two reads are one frame, and the loop must not answer either of them.
1142    #[tokio::test]
1143    async fn a_request_split_across_two_reads_is_one_frame() -> Result<(), Failure> {
1144        let (served, stdout, _stderr) = run(
1145            &["{\"jsonrpc\":\"2.0\",\"id\":", "42,\"method\":\"ping\"}\n"],
1146            AllowBatching,
1147        )
1148        .await?;
1149        let report = served?;
1150        let frames = written(&stdout)?;
1151
1152        assert_eq!(report.frames, 1, "two reads are one frame, not two");
1153        assert_eq!(frames.len(), 1);
1154        let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
1155        assert_eq!(id_of(reply)?, Value::from(42));
1156        assert!(reply.get("result").is_some(), "{reply}");
1157        Ok(())
1158    }
1159
1160    /// The rule this transport exists to keep: stdout is MCP messages and
1161    /// nothing else. Observed, not asserted in a comment — the whole of stdout
1162    /// is read, split into frames, and every frame must be a well-formed reply
1163    /// carrying the version, an id, and exactly one outcome. A `println!`
1164    /// anywhere in the serve path puts a line here that does not parse.
1165    #[tokio::test]
1166    async fn stdout_carries_nothing_but_replies() -> Result<(), Failure> {
1167        let (served, stdout, stderr) = run(
1168            &[
1169                // A result.
1170                "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n",
1171                // A refusal the dispatcher produced.
1172                "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"no/such/method\"}\n",
1173                // A frame the decoder refused, which the loop still answers.
1174                "this is not json at all\n",
1175                // A notification, which earns no reply.
1176                "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}\n",
1177            ],
1178            AllowBatching,
1179        )
1180        .await?;
1181        let report = served?;
1182        let frames = written(&stdout)?;
1183
1184        assert_eq!(frames.len(), 3, "the notification earns none: {frames:?}");
1185        for frame in &frames {
1186            assert_eq!(frame.get("jsonrpc"), Some(&Value::from("2.0")), "{frame}");
1187            assert!(
1188                frame.get("id").is_some(),
1189                "every reply is addressed: {frame}"
1190            );
1191            let result = frame.get("result").is_some();
1192            let error = frame.get("error").is_some();
1193            assert!(result ^ error, "exactly one outcome: {frame}");
1194        }
1195        // Both channels really were used, so the check above is not passing
1196        // because the loop wrote nothing anywhere.
1197        let log = stderr.contents();
1198        assert!(log.contains("undecodable frame"), "{log}");
1199        assert!(log.contains("refused a request"), "{log}");
1200        assert_eq!(report.replies, 3);
1201        Ok(())
1202    }
1203
1204    /// The decision this module exists to make: a frame that arrived whole and
1205    /// did not decode is answered with `-32700` and the session continues. The
1206    /// second request in the same session is the part that would fail if the
1207    /// loop dropped the connection.
1208    #[tokio::test]
1209    async fn a_malformed_frame_is_answered_and_serving_continues() -> Result<(), Failure> {
1210        let (served, stdout, _stderr) = run(
1211            &[
1212                "{not json at all\n",
1213                "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"ping\"}\n",
1214            ],
1215            AllowBatching,
1216        )
1217        .await?;
1218        let report = served?;
1219        let frames = written(&stdout)?;
1220
1221        assert_eq!(report.frames, 2, "both frames were read");
1222        assert_eq!(report.replies, 2);
1223        let (first, rest) = frames
1224            .split_first()
1225            .ok_or(Failure::Missing("a parse error"))?;
1226        let second = rest.first().ok_or(Failure::Missing("a second reply"))?;
1227        assert_eq!(id_of(first)?, Value::Null, "no id was ever read");
1228        assert_eq!(error_code(first)?, -32700);
1229        assert_eq!(id_of(second)?, Value::from(2), "the session carried on");
1230        assert!(second.get("result").is_some(), "{second}");
1231        Ok(())
1232    }
1233
1234    /// A frame that is JSON but not a JSON-RPC message is a different failure
1235    /// from one that is not JSON, and the specification gives them different
1236    /// codes. Collapsing them would tell the peer to fix the wrong thing.
1237    #[tokio::test]
1238    async fn a_well_formed_document_that_is_not_a_message_is_an_invalid_request()
1239    -> Result<(), Failure> {
1240        let (served, stdout, _stderr) = run(&["{\"hello\":\"world\"}\n"], AllowBatching).await?;
1241        served?;
1242        let frames = written(&stdout)?;
1243        let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
1244
1245        assert_eq!(error_code(reply)?, -32600);
1246        Ok(())
1247    }
1248
1249    /// A peer that closes is not a failure. `serve` answers `Ok`, and the
1250    /// report says the session did nothing rather than pretending it never
1251    /// happened.
1252    #[tokio::test]
1253    async fn a_clean_close_is_a_clean_close() -> Result<(), Failure> {
1254        let (served, stdout, stderr) = run(&[], AllowBatching).await?;
1255
1256        let report = served?;
1257        assert_eq!(report, ServeReport::default());
1258        assert_eq!(stdout.contents(), "");
1259        assert!(
1260            stderr.contents().contains("closed"),
1261            "{}",
1262            stderr.contents()
1263        );
1264        Ok(())
1265    }
1266
1267    /// The distinction the last test's `Ok` is only worth anything because of:
1268    /// a stream that breaks is a different event from a stream that ends, and
1269    /// a caller has to be able to tell them apart.
1270    #[tokio::test]
1271    async fn a_broken_stream_is_not_a_clean_close() -> Result<(), Failure> {
1272        let stdout = Capture::shared();
1273        let mut broken = ServeLoop::new(
1274            Rupture,
1275            stdout.clone(),
1276            Diagnostics::new(Capture::shared()),
1277            StdioConfig::new(MaxFrameSize::new(ROOMY)?),
1278            dispatcher()?,
1279            peer(),
1280            AllowBatching,
1281        );
1282
1283        let served = broken.serve().await;
1284
1285        assert_matches!(served, Err(ServeError::Source(SourceError::Transport)));
1286        assert_eq!(stdout.contents(), "", "nothing was answered");
1287        Ok(())
1288    }
1289
1290    /// A reply the ceiling refuses is never written. `StdioConfig` is one value
1291    /// for both directions, so the ceiling here sits above the 45-byte request
1292    /// and below the listing that comes back.
1293    #[tokio::test]
1294    async fn an_oversized_reply_is_refused_and_nothing_reaches_the_wire() -> Result<(), Failure> {
1295        let (served, stdout, _stderr) = run_under(
1296            &["{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}\n"],
1297            AllowBatching,
1298            64,
1299        )
1300        .await?;
1301
1302        assert_matches!(
1303            served,
1304            Err(ServeError::Reply(StdioError::ReplyTooLarge { .. }))
1305        );
1306        assert_eq!(
1307            stdout.contents(),
1308            "",
1309            "a refused reply must not be truncated onto the stream",
1310        );
1311        Ok(())
1312    }
1313
1314    /// A notification is served and never answered — not even with an error,
1315    /// which is what JSON-RPC 2.0 requires and what stops a peer that floods
1316    /// them from filling stdout with messages nobody is waiting for.
1317    #[tokio::test]
1318    async fn a_notification_is_served_and_never_answered() -> Result<(), Failure> {
1319        let (served, stdout, stderr) = run(
1320            &["{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}\n"],
1321            AllowBatching,
1322        )
1323        .await?;
1324        let report = served?;
1325
1326        assert_eq!(report.requests, 1, "the method was still routed");
1327        assert_eq!(report.replies, 0);
1328        assert_eq!(stdout.contents(), "");
1329        assert!(stderr.contents().contains("closed"));
1330        Ok(())
1331    }
1332
1333    /// A batch is answered with one document carrying one answer per request,
1334    /// which is what the specification requires — and not with one document per
1335    /// message, which a peer would read as three unrequested replies.
1336    #[tokio::test]
1337    async fn a_batch_is_answered_with_one_document() -> Result<(), Failure> {
1338        let (served, stdout, _stderr) = run(
1339            &["[{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"},\
1340                {\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"ping\"}]\n"],
1341            AllowBatching,
1342        )
1343        .await?;
1344        let report = served?;
1345        let frames = written(&stdout)?;
1346
1347        assert_eq!(frames.len(), 1, "a batch is one document");
1348        assert_eq!(report.requests, 2);
1349        assert_eq!(report.replies, 1, "one document, not two");
1350        let answers = frames
1351            .first()
1352            .and_then(Value::as_array)
1353            .ok_or(Failure::Missing("an array of answers"))?;
1354        assert_eq!(answers.len(), 2);
1355        Ok(())
1356    }
1357
1358    /// A peer is gated on the revision it negotiated, not the one the loop was
1359    /// constructed with.
1360    ///
1361    /// The loop here starts on a session whose profile carries `2025-06-18`
1362    /// features. The peer offers `2024-11-05`, and `completions/complete` did
1363    /// not exist until `2025-03-26`. If the handshake did not narrow the
1364    /// session, the call would be served — a peer on a revision whose schema
1365    /// has nowhere to put the answer.
1366    #[tokio::test]
1367    async fn the_session_is_narrowed_to_the_negotiated_revision() -> Result<(), Failure> {
1368        let (mut server, _stdout, _stderr) = harness(&[], AllowBatching, ROOMY)?;
1369
1370        let handshake = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}"#;
1371        let completion = r#"{"jsonrpc":"2.0","id":2,"method":"completions/complete","params":{"ref":{"type":"ref/prompt","name":"p"},"argument":{"name":"a","value":""}}}"#;
1372        let mut report = ServeReport::default();
1373        server
1374            .serve_frame(&format!("{handshake}\n"), &mut report)
1375            .await?;
1376        server
1377            .serve_frame(&format!("{completion}\n"), &mut report)
1378            .await?;
1379
1380        assert!(
1381            !server.peer.profile().allows(Feature::Completions),
1382            "a 2024-11-05 peer must not keep a 2025-03-26 feature"
1383        );
1384        Ok(())
1385    }
1386
1387    /// A batch the session never negotiated is refused whole, and nothing
1388    /// inside it is served. The decision belongs to the policy, not to the
1389    /// loop: a loop that answered batches from its own belief about the
1390    /// revision would be the peer-side version of resolving a profile from the
1391    /// wrong inputs.
1392    #[tokio::test]
1393    async fn a_batch_the_session_never_negotiated_is_refused() -> Result<(), Failure> {
1394        let (served, stdout, stderr) = run(
1395            &["[{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}]\n"],
1396            DenyBatching,
1397        )
1398        .await?;
1399        let report = served?;
1400        let frames = written(&stdout)?;
1401
1402        assert_eq!(frames.len(), 1);
1403        assert_eq!(
1404            error_code(frames.first().ok_or(Failure::Missing("a reply"))?)?,
1405            -32600
1406        );
1407        assert_eq!(
1408            report.requests, 0,
1409            "nothing inside a refused batch is served"
1410        );
1411        assert!(
1412            stderr.contents().contains("batching"),
1413            "{}",
1414            stderr.contents()
1415        );
1416        Ok(())
1417    }
1418
1419    /// The id the peer sent is the id the peer gets back, byte for byte, even
1420    /// when it contains characters only JSON knows how to escape. A reply whose
1421    /// id differs by one character is an answer no peer can match to its
1422    /// request.
1423    #[tokio::test]
1424    async fn the_request_id_survives_the_round_trip_escaped() -> Result<(), Failure> {
1425        let (served, stdout, _stderr) = run(
1426            &["{\"jsonrpc\":\"2.0\",\"id\":\"a\\u007fb\\\"c\",\"method\":\"ping\"}\n"],
1427            AllowBatching,
1428        )
1429        .await?;
1430        served?;
1431        let frames = written(&stdout)?;
1432        let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
1433
1434        assert_eq!(id_of(reply)?, Value::from("a\u{7f}b\"c"));
1435        Ok(())
1436    }
1437
1438    /// A reply that a reader cannot match to its request is worse than no
1439    /// reply, so the number stays a number.
1440    #[tokio::test]
1441    async fn a_numeric_id_stays_numeric() -> Result<(), Failure> {
1442        let (served, stdout, _stderr) = run(
1443            &["{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"ping\"}\n"],
1444            AllowBatching,
1445        )
1446        .await?;
1447        served?;
1448        let frames = written(&stdout)?;
1449        let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
1450
1451        assert_eq!(id_of(reply)?, Value::from(7));
1452        Ok(())
1453    }
1454
1455    /// `Debug` is public API on a type a caller will log at startup. It names
1456    /// the session and the routing table and nothing about the streams, so it
1457    /// neither needs `W: Debug` nor prints a peer's buffered bytes.
1458    #[test]
1459    fn the_debug_output_names_the_session_and_not_the_streams() -> Result<(), Failure> {
1460        let (server, _stdout, _stderr) = harness(&[], AllowBatching, ROOMY)?;
1461
1462        let printed = format!("{server:?}");
1463
1464        assert!(printed.contains("ServeLoop"), "{printed}");
1465        assert!(printed.contains("peer"), "{printed}");
1466        assert!(!printed.contains("Trickle"), "{printed}");
1467        Ok(())
1468    }
1469}