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(¶ms);
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, ¶ms) {
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 ¶ms,
587 );
588 }
589 match self.dispatcher.dispatch(&context, method, ¶ms).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, ¶ms).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}