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(¶ms);
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(¶ms) {
483 return self.answer_stateless(envelope.id(), method, ¶ms).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(¶ms),
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, ¶ms);
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, ¶ms).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}