Skip to main content

protocol_droid/adapters/primary/transport/http/serve/
mod.rs

1//! Serving MCP over Streamable HTTP, on a socket this crate bound.
2//!
3//! [`McpRouter`] builds an axum `Router`, and a `Router` is not a server. The
4//! gap between the two is where a consumer's HTTP support actually lives:
5//! somebody has to own a [`SessionStore`], mint ids for it, implement
6//! [`RequestHandler`] against the real [`Dispatcher`], bind a listener, and
7//! decide the bind address. Five decisions, of which one — the bind address —
8//! is genuinely the caller's.
9//!
10//! This module makes the other four. It is the HTTP counterpart of
11//! [`StdioBuilder`](crate::application::server::serve::StdioBuilder), and it
12//! mirrors it deliberately: both resolve a **ceiling** before the peer's first
13//! frame, both leave the ceiling overridable where the choice is real, and
14//! neither asks the caller to name a protocol version.
15//!
16//! The dispatch sequence is the stdio loop's, not a second one. Decoding,
17//! refusing a malformed frame, distinguishing `initialize` from everything
18//! else, and choosing a JSON-RPC code for a refusal are all decisions the
19//! crate makes elsewhere; a second copy here would be a second opinion on each
20//! of them, and a second opinion is wrong in exactly the cases nobody tests.
21//!
22//! # What it deliberately does not do
23//!
24//! It does not terminate TLS. The bind address defaults to loopback, and a
25//! loopback listener serving plain HTTP is a development shape rather than a
26//! deployment one. Binding `0.0.0.0` is permitted — the right answer depends on
27//! what terminates TLS in front of it, and that is not a fact this crate can
28//! see — but a non-loopback bind is this server's responsibility to get right,
29//! not something [`HttpBuilder::addr`] pretends to make safe.
30
31use core::num::NonZeroUsize;
32use std::sync::Arc;
33
34use serde_json::Value;
35
36use crate::adapters::primary::transport::http::config::Streaming;
37use crate::adapters::primary::transport::http::documents::{
38    answer_of, id_as_i64, method_of, params_of, refusal_document, refusal_document_from_refusal,
39    result_document,
40};
41use crate::adapters::primary::transport::http::exchange::{ExchangeTable, HttpExchange};
42use crate::adapters::primary::transport::http::refusal::Refusal;
43use crate::adapters::primary::transport::http::router::{
44    EventSource, Inbound, RequestHandler, SessionHandle, Unsessioned,
45};
46use crate::adapters::primary::transport::http::session_store::SessionStore;
47use crate::adapters::primary::transport::http::stateless::{StatelessHeaders, StatelessRequest};
48use crate::adapters::primary::transport::http::stream::{EventTail, Finishing};
49use crate::application::server::context::{PeerSession, RequestContext};
50use crate::application::server::dispatch::Dispatcher;
51use crate::application::server::{ListKind, ListSubscription};
52use crate::application::session::IdSource;
53use crate::domain::feature::resolve;
54use crate::domain::ir::JsonObject;
55use crate::domain::jsonrpc::{Frame, WireId, decode};
56use crate::domain::mcp::McpSessionId;
57use crate::domain::session::{CancellationToken, ProgressToken, SessionId};
58use crate::domain::{
59    CapabilitySet, EventId, Negotiation, ProtocolVersion, RequestId, TransportKind,
60};
61
62mod builder;
63mod check;
64mod sink;
65
66#[cfg(test)]
67mod tests;
68
69use check::{check_header, declares_own_version};
70
71// `TailSink` is how the `listen` and `stateless` adapters hand a session's
72// outgoing frames somewhere. `pub(super)` is `http`, which is where those two
73// live — `serve` is private to `http`, so nothing wider is possible or needed.
74pub(super) use sink::TailSink;
75
76// `HttpBuilder` is the public face of this module: the four decisions a
77// socket server needs, in a builder. It is defined in `builder` because the
78// axum wiring is a different concern from the request handling, and it is
79// re-exported here because this is where a consumer looks for it.
80// `HttpBuilder`, `ServeError` and `ServeReport` are this module's public
81// face: the four decisions a socket server needs, what fails them, and what a
82// successful bind reports. They live in `builder` because the axum wiring and
83// the socket are the same concern, and they are re-exported here because this
84// is where a consumer looks for them.
85pub use builder::{HttpBuilder, ServeError, ServeReport};
86
87// The test-only entry point into the axum wiring, reached from the `listen`
88// and `stateless` test modules as well as this module's own. `pub(super)` is
89// `http`, which is where those siblings live — `pub(crate)` would be refused
90// here, because `serve` is itself private to `http`. It is `#[cfg(test)]` on
91// both sides, so a lean build never carries it.
92#[cfg(test)]
93pub(super) use builder::test_routes;
94
95/// The tail this session's server-initiated messages are published on.
96pub(super) type Tail = EventTail<Value>;
97
98/// Where an [`IdSource`] mints session ids from.
99///
100/// A named alias rather than a bare trait object repeated in two signatures, so
101/// that a reader sees the choice being made once.
102type Ids = Arc<dyn IdSource + Send + Sync>;
103
104/// How many undelivered frames one session's tail retains.
105///
106/// A `GET` that follows a `POST` resumes from this ring, so a client that
107/// disconnects and reconnects within the window gets the frames it missed.
108/// Large enough to cover a reconnect, small enough that an idle session is not
109/// holding a megabyte of `Value`s nobody will read.
110/// The keep-alive interval a draining stream uses.
111///
112/// Zero, because a `POST`'s stream ends as soon as the tail is drained and a
113/// keep-alive only matters to a stream that is staying open. A non-zero value
114/// here would emit `:ping` frames into the middle of a response that is about
115/// to close.
116pub(super) const KEEP_ALIVE: core::time::Duration = core::time::Duration::ZERO;
117
118pub(super) const TAIL_CAPACITY: NonZeroUsize = match NonZeroUsize::new(64) {
119    Some(capacity) => capacity,
120    // A const context cannot call `ok_or`, and `None` here would mean a zero,
121    // which is not a capacity. Unreachable for any value above zero.
122    None => NonZeroUsize::MIN,
123};
124
125/// One session's state on an HTTP connection.
126///
127/// Holds the negotiated [`PeerSession`] beside the tail its server-initiated
128/// messages go on, because the router is generic over the stored type and a
129/// [`PeerSession`] alone cannot answer [`EventSource::tail`].
130///
131/// The peer starts at the ceiling profile and is replaced by
132/// [`Dispatcher::handshake`]'s return once `initialize` arrives. That ordering
133/// is not incidental: a [`RequestContext`] needs a `PeerSession` to exist before
134/// dispatch, and the handshake is the call that produces one.
135#[derive(Debug)]
136pub struct HttpSession {
137    /// The session's protocol state — the ceiling profile until `initialize`
138    /// replaces it, and the negotiated one forever after.
139    peer: PeerSession,
140    /// Where this session's server-initiated messages are published.
141    tail: Tail,
142    /// The server-initiated requests this session is waiting on.
143    ///
144    /// Held per session rather than globally because the id a server-initiated
145    /// request carries is minted per session: two sessions both asking the peer
146    /// to sample both mint id `1`, and a shared table would hand the second
147    /// answer to the first session's tool.
148    exchange: ExchangeTable,
149    /// This session's standing interest in list changes.
150    ///
151    /// Held here so the session and the subscription end together: the store
152    /// dropping an expired or deleted session drops this guard, and the
153    /// fan-out stops writing to a tail nobody reads. `None` on a session that
154    /// has not finished its handshake, because nothing has been negotiated yet
155    /// that could gate a notification.
156    _lists: Option<ListSubscription>,
157}
158
159impl EventSource for HttpSession {
160    fn tail(&self) -> &Tail {
161        &self.tail
162    }
163}
164
165impl HttpSession {
166    /// This session's peer, for a handler that reads it off a stored session.
167    fn peer_of(session: &Arc<Self>) -> PeerSession {
168        session.peer.clone()
169    }
170
171    /// This session's peer.
172    ///
173    /// The same state as `Self::peer_of` and for the same reason: a task
174    /// that outlives the request it was spawned for has to keep reading the
175    /// *session's* profile rather than a borrow of a body that is gone.
176    pub fn peer(&self) -> PeerSession {
177        self.peer.clone()
178    }
179}
180
181/// The handler that bridges [`McpRouter`](crate::adapters::primary::transport::http::router::McpRouter)
182/// to the real [`Dispatcher`].
183///
184/// The port trait belongs to the router, so this type implements it and
185/// declares nothing. There is no second "how to answer a request" here — only
186/// [`Dispatcher`] with an HTTP envelope around it.
187pub struct HttpHandler {
188    /// The protocol layer. Shared, because the router is cheap to clone and the
189    /// dispatcher is not.
190    pub(super) dispatcher: Arc<Dispatcher>,
191    /// This server's sessions. The handler owns the store rather than being
192    /// handed one per request because *minting* is a protocol-layer decision:
193    /// the router can look a session up but cannot decide what a session is.
194    pub(super) sessions: Arc<SessionStore<HttpSession>>,
195    /// The revision to gate a session with before it has been negotiated.
196    ceiling: ProtocolVersion,
197}
198
199impl core::fmt::Debug for HttpHandler {
200    /// Prints the ceiling and nothing else.
201    ///
202    /// Handwritten because the store it holds is a table of session ids, and
203    /// this project does not put a full session id in a log line. A derived
204    /// `Debug` would make a `{:?}` in a log statement a way around that.
205    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
206        formatter
207            .debug_struct("HttpHandler")
208            .field("ceiling", &self.ceiling)
209            .finish_non_exhaustive()
210    }
211}
212
213impl HttpHandler {
214    /// A handler over `dispatcher` and `sessions`, gated at `ceiling`.
215    #[must_use]
216    pub const fn new(
217        dispatcher: Arc<Dispatcher>,
218        sessions: Arc<SessionStore<HttpSession>>,
219        ceiling: ProtocolVersion,
220    ) -> Self {
221        Self {
222            dispatcher,
223            sessions,
224            ceiling,
225        }
226    }
227
228    /// The revision a session is gated with before it is negotiated.
229    #[must_use]
230    pub const fn ceiling(&self) -> ProtocolVersion {
231        self.ceiling
232    }
233
234    /// The session a request arrives on, or a fresh one to handshake on.
235    ///
236    /// A fresh session is seeded at the ceiling profile rather than at the
237    /// peer's answer, because `initialize` is the request that *produces* the
238    /// negotiated peer: there is nothing to read until it has run. The same
239    /// ordering the stdio loop uses, and for the same reason.
240    ///
241    /// The principal is deliberately *not* applied here. It is the request's
242    /// rather than the session's, because the router resolved it from the
243    /// credentials this request presented and a bearer token can be refreshed,
244    /// replaced, or withdrawn between two requests on one session. Every
245    /// visibility gate reads `ctx.peer().auth()` — `Dispatcher` for the
246    /// listings, `RegistryServer::call` for the call path — so a peer that
247    /// never received the request's context is anonymous to all of them, and a
248    /// scope-gated primitive is invisible to the very caller who earned it.
249    fn peer_for(&self, stored: Option<&Arc<HttpSession>>) -> PeerSession {
250        stored.map_or_else(|| self.seed().peer, HttpSession::peer_of)
251    }
252
253    /// A session at the ceiling, for a connection that has not handshaken.
254    fn seed(&self) -> HttpSession {
255        Self::session_for(self.ceiling, CapabilitySet::empty())
256    }
257
258    /// A peer resolved for `version` and what the client said it supports.
259    ///
260    /// Shared by the two callers that need a session with no handshake behind
261    /// it: the ceiling seed for a request that has not negotiated, and the
262    /// stateless peer, which differs only in that it knows its revision and its
263    /// capabilities. Building both from one function is what keeps the
264    /// stateless path from quietly acquiring a difference of its own.
265    pub(super) fn session_for(version: ProtocolVersion, client_caps: CapabilitySet) -> HttpSession {
266        let profile = resolve(Negotiation {
267            version,
268            client_caps,
269            server_caps: CapabilitySet::all_for(version),
270            transport: TransportKind::StreamableHttp,
271        });
272        let tail = Tail::new(TAIL_CAPACITY);
273        let exchange = ExchangeTable::new();
274        let sink = Arc::new(TailSink { tail: tail.clone() });
275        HttpSession {
276            peer: PeerSession::new(
277                version,
278                profile,
279                SessionId::new(1),
280                TransportKind::StreamableHttp,
281                Arc::new(HttpExchange::new(exchange.clone(), tail.clone())),
282                sink,
283            ),
284            tail,
285            exchange,
286            _lists: None,
287        }
288    }
289    /// Answers a request beside a stream that is already open.
290    ///
291    /// A handler that publishes *and waits* — a tool that asks the peer to
292    /// sample or to elicit — cannot be answered the way a handler that only
293    /// publishes is. Awaiting it inline means the response is built after the
294    /// tool returns, and the tool returns after the peer answers, and the peer
295    /// answers after it reads a request sitting in a stream this method has
296    /// not built yet. A deadlock whose symptom is a client that waits forever
297    /// for a request it will never be sent.
298    ///
299    /// So the stream is built *first* and the handler runs beside it. The
300    /// tool's request is on the wire within milliseconds, the peer's answer
301    /// arrives on its own `POST`, and the handler finishes and signals, at
302    /// which point the stream ends.
303    ///
304    /// Separate from the caller's own dispatch because the two shapes do not
305    /// share a tail: this one needs the stream before the handler runs, and
306    /// the other needs the handler before the stream.
307    ///
308    /// # Errors
309    ///
310    /// [`Refusal::Handler`] if the stream cannot be assembled.
311    fn respond_beside_stream(
312        &self,
313        request_peer: &PeerSession,
314        session: &Arc<HttpSession>,
315        id: Option<&WireId<'static>>,
316        progress: Option<ProgressToken>,
317        method: &str,
318        params: &JsonObject,
319    ) -> Result<axum::response::Response, Refusal> {
320        let tail = session.tail.clone();
321        // Where the stream stood before this request ran, so it carries this
322        // request's frames and not the exchange that opened the session.
323        let since = session.tail.last().to_string();
324        let finished = Finishing::new();
325        let response = tail
326            .plan(Some(since.as_str()))
327            .into_finishable_response(finished.clone(), KEEP_ALIVE)
328            .map_err(|error| Refusal::Handler(error.to_string()))?;
329
330        let dispatcher = Arc::clone(&self.dispatcher);
331        // The whole peer is carried, not just the session's. The caller's
332        // principal lives on the *request's* peer — the router resolved it from
333        // the credentials this request presented — and rebuilding from the
334        // session would answer as though nobody had authenticated. Every
335        // visibility gate reads `ctx.peer().auth()`, so the cost of that is a
336        // scope-gated primitive invisible to the caller who earned it.
337        let owned_peer = request_peer.clone();
338        let owned_id = id.map(WireId::to_owned);
339        let owned_method = method.to_owned();
340        let owned_params = params.clone();
341        let tail_for_result = tail;
342        let finished_for_task = finished;
343        let task = tokio::spawn(async move {
344            let peer = owned_peer;
345            let context = RequestContext::for_request_with_state(
346                &peer,
347                RequestId::from_wire(owned_id.as_ref()),
348                CancellationToken::new(),
349                progress,
350                dispatcher.state().clone(),
351            );
352            let document = match dispatcher
353                .dispatch(&context, &owned_method, &owned_params)
354                .await
355            {
356                Ok(result) => match result_document(owned_id.as_ref(), &result) {
357                    Ok(document) => document,
358                    // A refusal here is this server's own data failing to
359                    // serialise, reported as a JSON-RPC error rather than
360                    // dropped: a `POST` whose handler has already run cannot
361                    // take an HTTP status for it any more.
362                    Err(refusal) => refusal_document_from_refusal(owned_id.as_ref(), &refusal),
363                },
364                Err(error) => refusal_document(owned_id.as_ref(), &error),
365            };
366            tail_for_result.push(document);
367            finished_for_task.fire();
368        });
369
370        // The stream owns the handler's lifetime from here: dropping the
371        // response drops the body, which drops the handle the task holds. The
372        // task is already running, so returning does not wait for it.
373        let _task = task;
374        Ok(response)
375    }
376
377    /// Routes a peer's answer to the tool waiting for it, and acknowledges.
378    ///
379    /// An answer is a JSON-RPC message with an id and no method. It is
380    /// recognised rather than dispatched because there is nothing to dispatch
381    /// it to: the tool that asked the question is parked on the answer, and
382    /// routing it down the request path fails to find a method, answers the
383    /// peer with an error, and leaves the tool waiting for an answer the server
384    /// has already decided does not exist.
385    ///
386    /// The acknowledgement is `202 Accepted` with no body, which is what the
387    /// specification asks of a response to a response — and the same answer is
388    /// given whether the answer found its waiter or not, because a reply to an
389    /// unknown request is discarded and inventing a request to attach the
390    /// discard to would be worse.
391    fn acknowledge_answer(
392        body: &[u8],
393        session: Option<&Arc<HttpSession>>,
394        id: Option<&WireId<'static>>,
395    ) -> Result<axum::response::Response, Refusal> {
396        if let (Some(session), Some(id)) = (session, id)
397            && let Some(numeric) = id_as_i64(id)
398        {
399            session.exchange.resolve(numeric, answer_of(body));
400        }
401        axum::response::Response::builder()
402            .status(axum::http::StatusCode::ACCEPTED)
403            .body(axum::body::Body::empty())
404            .map_err(|_| Refusal::Handler("the acknowledgement did not build".to_owned()))
405    }
406}
407
408impl RequestHandler<HttpSession> for HttpHandler {
409    /// What a body means when the request named no session.
410    ///
411    /// Three arms, and which one a body falls in is a wire-format question
412    /// this method can answer on its own:
413    ///
414    /// - `initialize` opens a session. It is the only method that does.
415    /// - A body whose `params._meta` names
416    ///   [`PROTOCOL_VERSION_KEY`](crate::application::mcp::request_meta::PROTOCOL_VERSION_KEY)
417    ///   is a SEP-2575 stateless request. It carries its own revision and its
418    ///   own capabilities, so it needs no session and is served against its
419    ///   own profile.
420    /// - Anything else is a stateful call missing its header, which the router
421    ///   answers with a `400` and never hands here — so returning
422    ///   [`Unsessioned::NeedsSession`] for it is not a guess, it is the arm
423    ///   that produces that `400`.
424    ///
425    /// The version key is what makes a body self-describing, and it is the
426    /// only member that can: capabilities without a revision would not say
427    /// which gates the answer is read against.
428    ///
429    /// This runs on every sessionless POST, so the check is a lookup and not a
430    /// parse — a full [`RequestMeta::read`] would decode the whole body only
431    /// for a caller with a session header to throw it away.
432    ///
433    /// [`RequestMeta::read`]: crate::application::mcp::request_meta::RequestMeta::read
434    fn unsessioned(&self, body: &[u8], headers: &axum::http::HeaderMap) -> Unsessioned {
435        // Stateless first. A body naming its own revision, or a request whose
436        // `MCP-Protocol-Version` is a revision with no sessions, is served by
437        // the stateless lifecycle — including an `initialize`, which that
438        // lifecycle answers with `-32601` rather than by opening a session the
439        // client's own revision says do not exist.
440        if declares_own_version(body) || StatelessHeaders::names_stateless_revision(headers) {
441            return Unsessioned::Stateless;
442        }
443        match method_of(body) {
444            Some(method) if method == "initialize" => Unsessioned::Establishing,
445            _ => Unsessioned::NeedsSession,
446        }
447    }
448
449    /// Runs one request, and answers it.
450    ///
451    /// The refusal channel is a [`Refusal`]: there is no other, and an adapter
452    /// that cannot answer a request has exactly one honest way to say so. Note
453    /// that a *protocol* refusal — an unknown method, a tool that does not
454    /// exist — is not one. Those are answered in the body with a JSON-RPC error
455    /// object, because the peer asked a well-formed question and the answer is
456    /// that no such thing exists.
457    fn dispatch(
458        &self,
459        call: Inbound<'_, HttpSession>,
460    ) -> impl core::future::Future<Output = Result<axum::response::Response, Refusal>> + Send {
461        // Everything the handler needs is copied out of `call` before the
462        // future, because `Inbound` borrows the body and the router's `Arc`,
463        // neither of which outlives this function.
464        let body = call.body.to_vec();
465        let protocol = call.protocol;
466        // Captured here because the framing decision below happens after the
467        // body is owned, and this is the only record of how the server was
468        // configured to answer. See [`Streaming`].
469        let streaming = call.streaming;
470        let opening = matches!(call.session, SessionHandle::Establishing);
471        let stateless = matches!(call.session, SessionHandle::Stateless);
472        let stored = match &call.session {
473            SessionHandle::Establishing | SessionHandle::Stateless => None,
474            SessionHandle::Live { session, .. } => Some(Arc::clone(session)),
475        };
476        // The principal is this request's, not the session's — see
477        // `peer_for`, where the reasoning lives.
478        //
479        // A stateless request's peer is *not* built here, because there is
480        // nothing yet to build it from: the revision and the capabilities both
481        // live in this request's `_meta`, and the body has not been decoded
482        // until the future below runs. The seed peer stands in for that case
483        // and is replaced before anything reads it.
484        let auth = call.context.clone();
485        let peer = self.peer_for(stored.as_ref()).with_auth(auth.clone());
486        // Copied now for the same reason as the body: the header map is the
487        // router's, and a stateless request is checked against it after the
488        // body has been decoded.
489        let stateless_headers = stateless.then(|| StatelessHeaders::read(call.headers));
490
491        async move {
492            // Decoded once. A body that is a batch, or a reply shape, or
493            // unreadable, is the router's business and is refused here rather
494            // than answered with a second opinion about what it meant.
495            let decoded = decode(&body).map_err(|error| Refusal::MalformedBody {
496                reason: error.to_string(),
497            })?;
498            let Frame::Single(envelope) = decoded else {
499                return Err(Refusal::MalformedBody {
500                    reason: "an HTTP request body carries one message".to_owned(),
501                });
502            };
503            // Where the session's event stream stands *before* this request
504            // runs. Everything the handler publishes from here on is this
505            // request's answer; everything before it is a previous exchange's,
506            // and replaying that here is how a `tools/list` answers with the
507            // `initialize` frame that opened the session.
508            let cursor: Option<EventId> = stored.as_ref().map(|session| session.tail.last());
509            // Owned before anything else, because a handler may outlive this
510            // frame: the request body is dropped when this function returns,
511            // and a task still running needs the id it was answering.
512            let id: Option<WireId<'static>> = envelope.id().map(WireId::to_owned);
513
514            // An answer to a request *this* server made: a JSON-RPC message
515            // with an id and no method. It is recognised here rather than
516            // dispatched, because there is nothing to dispatch it to.
517            if method_of(&body).is_none() {
518                return Self::acknowledge_answer(&body, stored.as_ref(), id.as_ref());
519            }
520
521            let params = params_of(&envelope)?;
522            let method = envelope.method_str().unwrap_or_default();
523
524            // SEP-2575: a stateless request is served from its own `_meta`, by
525            // a path that shares nothing with a session — see `stateless`.
526            if let Some(headers) = stateless_headers {
527                return self
528                    .serve_stateless(StatelessRequest {
529                        id,
530                        method: method.to_owned(),
531                        params,
532                        headers,
533                        auth,
534                        streaming,
535                    })
536                    .await;
537            }
538
539            // The progress token lives in `params._meta.progressToken`, which
540            // is where the specification puts it and not beside the request id.
541            // Not reading it is a quiet failure: `report_progress` answers
542            // `NoProgressToken` and the client that asked to be told waits for
543            // updates from a server that believed nobody had asked.
544            let progress: Option<ProgressToken> =
545                crate::application::mcp::request_meta::progress_token(&params);
546            let peer_for_now = peer;
547            // Held only as long as this body. The task below builds its own
548            // from the stored session, because it outlives both of them.
549            let context = RequestContext::for_request_with_state(
550                &peer_for_now,
551                RequestId::from_wire(id.as_ref()),
552                CancellationToken::new(),
553                progress.clone(),
554                Arc::clone(self.dispatcher.state()),
555            );
556
557            // `initialize` is the one method that changes the session rather
558            // than being answered from it: the handshake resolves a profile for
559            // the revision the client offered, and every gate after this point
560            // has to read *that* one. A peer that negotiated `2024-11-05` must
561            // not be served on a profile resolved for whatever the process was
562            // built with.
563            let (document, negotiated) = if method == "initialize" {
564                match self.dispatcher.handshake(&context, &params) {
565                    Ok((result, peer)) => (result_document(id.as_ref(), &result)?, Some(peer)),
566                    Err(error) => (refusal_document(id.as_ref(), &error), None),
567                }
568            } else if let Some(session) = stored.as_ref() {
569                // A sessioned request is answered beside an already-open
570                // stream, which is the only shape that survives a handler that
571                // publishes *and waits* — see [`Self::respond_beside_stream`].
572                //
573                // Only when this server was configured to frame a stream at
574                // all. Under `Streaming::Json` the documented default, the
575                // reply is one JSON document: the stream is built and drained
576                // into it before the response exists, which is why a tool that
577                // elicits cannot work here and says so rather than hanging —
578                // see [`ExchangeTable::is_readable`].
579                if streaming == Streaming::EventStream {
580                    return self.respond_beside_stream(
581                        &peer_for_now,
582                        session,
583                        id.as_ref(),
584                        progress,
585                        method,
586                        &params,
587                    );
588                }
589                match self.dispatcher.dispatch(&context, method, &params).await {
590                    Ok(result) => (result_document(id.as_ref(), &result)?, None),
591                    Err(error) => (refusal_document(id.as_ref(), &error), None),
592                }
593            } else {
594                match self.dispatcher.dispatch(&context, method, &params).await {
595                    Ok(result) => (result_document(id.as_ref(), &result)?, None),
596                    Err(error) => (refusal_document(id.as_ref(), &error), None),
597                }
598            };
599
600            // A notification wants no answer, and a notification cannot open a
601            // session either — there is no id to put in the response header.
602            if id.is_none() {
603                return Self::response(&Value::Null, None, None);
604            }
605
606            // The header is checked against the session's own revision, which
607            // is why this is asked of the peer rather than of the raw header.
608            if let Some(agreed) = negotiated.as_ref().map(PeerSession::negotiated) {
609                check_header(protocol, agreed)?;
610            }
611
612            if opening {
613                return self.open_session(negotiated, stored.as_deref(), streaming, &document);
614            }
615
616            // The tail is handed over only when this server was configured to
617            // frame a stream. Under `Streaming::Json` the reply is one
618            // document with nothing to drain into, and passing the tail would
619            // answer with an event stream regardless — which is exactly how the
620            // default came to mean a bare document on `initialize` and a stream
621            // on everything after it.
622            Self::response(
623                &document,
624                None,
625                if streaming == Streaming::EventStream {
626                    stored.map(|session| (session.tail.clone(), cursor))
627                } else {
628                    None
629                },
630            )
631        }
632    }
633}
634
635impl HttpHandler {
636    /// Mints the session an `initialize` establishes, and answers with its id.
637    ///
638    /// Split out of [`RequestHandler::dispatch`] because opening a session is
639    /// one concern with one rule in it — what this session may be asked, and
640    /// what may be published to it — and inlining that into the request loop
641    /// left the rule sitting behind forty lines of session bookkeeping.
642    ///
643    /// # Errors
644    ///
645    /// [`Refusal::Handler`] if `initialize` succeeded without producing a peer
646    /// to store, if the store refuses the session, or if the answer will not
647    /// serialise.
648    fn open_session(
649        &self,
650        negotiated: Option<PeerSession>,
651        stored: Option<&HttpSession>,
652        streaming: Streaming,
653        document: &Value,
654    ) -> Result<axum::response::Response, Refusal> {
655        // `handshake` narrows the context it was handed, so the peer it
656        // returns carries the *establishing request's* principal. Every
657        // request resolves its own principal above, so nothing downstream
658        // reads this one — which is exactly why it is worth dropping rather
659        // than storing: session state outlives the credential that produced
660        // it, and a principal recorded there has the wrong lifetime for a
661        // token that can be refreshed, replaced, or withdrawn mid-session.
662        let peer = negotiated
663            .ok_or_else(|| Refusal::Handler("a session was opened without a handshake".to_owned()))?
664            .without_auth();
665        // The tail the router streams to the client, and the sink the handler
666        // writes through, are the same ring. Binding the peer to a *different*
667        // one is the quiet failure this shape invites: the session's own tail
668        // stays empty forever while the peer dutifully publishes into a ring
669        // no reader ever drains, so a tool that logs looks like a tool that
670        // logs nothing.
671        //
672        // Whether anything drains that ring is the framing decision, so it is
673        // recorded on the table here rather than left for a tool to discover by
674        // hanging. A session opened under `Streaming::Json` is never drained,
675        // because every reply it gives is one document.
676        let readable = streaming == Streaming::EventStream;
677        let (tail, exchange) = stored.map_or_else(
678            || {
679                let exchange = if readable {
680                    ExchangeTable::readable()
681                } else {
682                    ExchangeTable::new()
683                };
684                (Tail::new(TAIL_CAPACITY), exchange)
685            },
686            |session| (session.tail.clone(), session.exchange.clone()),
687        );
688        // The store allocates the id below, so it is read back from the minted
689        // handle rather than allocated twice — and the peer is bound to it
690        // before anything can key on it.
691        let sink: Arc<dyn crate::application::session::DynFrameSink> =
692            Arc::new(TailSink { tail: tail.clone() });
693        // Every list notification the negotiated profile allows. A session
694        // that did not negotiate `listChanged` for a kind is filtered by the
695        // fan-out's own gate, so subscribing to all three is not a promise.
696        let lists = self.dispatcher.server().handle().subscribe(
697            &sink,
698            peer.profile(),
699            &ListKind::ALL,
700            None,
701        );
702        let peer = peer
703            .with_sink(sink)
704            // Bound to *this* session's table, for the same reason the sink is
705            // bound to this session's tail: the peer outlives the seed it was
706            // built from, and an exchange on the wrong table publishes a
707            // request nobody on this session is waiting to answer.
708            .with_exchange(Arc::new(HttpExchange::new(exchange.clone(), tail.clone())));
709        let id_source = self.sessions.next_id();
710        let peer = peer.with_session(id_source);
711        let minted = self
712            .sessions
713            .open_with_id(
714                Arc::new(HttpSession {
715                    peer,
716                    tail,
717                    exchange,
718                    _lists: Some(lists),
719                }),
720                id_source,
721            )
722            .map_err(|error| Refusal::Handler(format!("no session: {error}")))?;
723        Self::response(document, Some(&minted), None)
724    }
725
726    /// The answer document, with the session header when one was minted.
727    ///
728    /// # Errors
729    ///
730    /// [`Refusal::Handler`] if the document will not serialise or the minted id
731    /// will not become a header value. Both are this server's own data, which
732    /// is why they are not reported as the peer's fault.
733    fn response(
734        document: &Value,
735        id: Option<&McpSessionId>,
736        tail: Option<(Tail, Option<EventId>)>,
737    ) -> Result<axum::response::Response, Refusal> {
738        // A session that may have published is answered with its event stream,
739        // and the reply goes *into* that stream rather than beside it.
740        //
741        // The alternative — streaming the handler's notifications and putting
742        // the JSON-RPC result in the body — is a response with two answers in
743        // two places, and which one a client reads as the result depends on the
744        // content type. The same tool call would resolve or hang purely on how
745        // the peer asked. One stream, frames in the order they happened: the
746        // notifications the call emitted, then its result.
747        if let Some((tail, cursor)) = tail {
748            tail.push(document.clone());
749            // The cursor, as the `Last-Event-ID` spelling `plan` reads. Held in
750            // a binding rather than built inline because the reference has to
751            // outlive the call.
752            let since = cursor.map(|id| id.to_string());
753            return match tail
754                .plan(since.as_deref())
755                .into_draining_response(KEEP_ALIVE)
756            {
757                Ok(response) => {
758                    let mut response = response;
759                    if let Some(id) = id {
760                        let value = axum::http::HeaderValue::from_str(&id.to_string()).map_err(
761                            |error| Refusal::Handler(format!("the id was not a header: {error}")),
762                        )?;
763                        response.headers_mut().insert("mcp-session-id", value);
764                    }
765                    Ok(response)
766                }
767                Err(error) => Err(Refusal::Handler(error.to_string())),
768            };
769        }
770
771        let encoded = serde_json::to_vec(document)
772            .map_err(|error| Refusal::Handler(format!("the reply did not serialise: {error}")))?;
773        let mut response = axum::response::Response::new(axum::body::Body::from(encoded));
774        if let Some(id) = id {
775            let value = axum::http::HeaderValue::from_str(&id.to_string())
776                .map_err(|error| Refusal::Handler(format!("the id was not a header: {error}")))?;
777            response.headers_mut().insert("mcp-session-id", value);
778        }
779        Ok(response)
780    }
781}