Skip to main content

rmcp_soddygo/transport/streamable_http_server/
tower.rs

1use std::{
2    borrow::Cow,
3    collections::HashMap,
4    convert::Infallible,
5    fmt::Display,
6    pin::Pin,
7    sync::Arc,
8    task::{Context, Poll},
9    time::Duration,
10};
11
12use bytes::Bytes;
13use futures::{Stream, StreamExt, future::BoxFuture};
14use http::{HeaderMap, Method, Request, Response, header::ALLOW};
15use http_body::Body;
16use http_body_util::{BodyExt, Full, combinators::BoxBody};
17use pin_project_lite::pin_project;
18use tokio_stream::wrappers::ReceiverStream;
19use tokio_util::sync::CancellationToken;
20
21use super::session::{
22    RestoreOutcome, SessionId, SessionManager, SessionRestoreMarker, SessionState, SessionStore,
23};
24use crate::{
25    RoleServer,
26    model::{
27        ClientCapabilities, ClientJsonRpcMessage, ClientNotification, ClientRequest, ErrorCode,
28        ErrorData, GetExtensions, GetMeta, Implementation, InitializeRequest,
29        InitializeRequestParams, InitializedNotification, JsonObject, JsonRpcError,
30        ProtocolVersion, RequestId, ServerJsonRpcMessage,
31    },
32    serve_server,
33    service::serve_directly_with_ct,
34    transport::{
35        OneshotTransport, TransportAdapterIdentity,
36        common::{
37            http_header::{
38                EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_MCP_PROTOCOL_VERSION,
39                HEADER_SESSION_ID, JSON_MIME_TYPE,
40            },
41            mcp_headers,
42            server_side_http::{
43                BoxResponse, ServerSseMessage, accepted_response, expect_json,
44                internal_error_response, sse_stream_response, unexpected_message_response,
45            },
46        },
47    },
48};
49
50/// Default maximum POST request body size (4 MiB).
51pub(crate) const DEFAULT_MAX_REQUEST_BODY_BYTES: usize = 4 * 1024 * 1024;
52
53#[non_exhaustive]
54#[derive(Debug, Clone)]
55pub struct StreamableHttpServerConfig {
56    /// The ping message duration for SSE connections.
57    pub sse_keep_alive: Option<Duration>,
58    /// The retry interval for SSE priming events.
59    pub sse_retry: Option<Duration>,
60    /// If true, the server will create a session for each request and keep it alive.
61    /// When enabled, SSE priming events are sent to enable client reconnection.
62    pub stateful_mode: bool,
63    /// When true and `stateful_mode` is false, the server prefers
64    /// `Content-Type: application/json` for simple request-response tools.
65    /// If the handler emits a notification or request before the final response,
66    /// the server falls back to `text/event-stream` so no message is lost.
67    pub json_response: bool,
68    /// Cancellation token for the Streamable HTTP server.
69    ///
70    /// When this token is cancelled, all active sessions are terminated and
71    /// the server stops accepting new requests.
72    pub cancellation_token: CancellationToken,
73    /// Allowed hostnames or `host:port` authorities for inbound `Host` validation.
74    ///
75    /// By default, Streamable HTTP servers only accept loopback hosts to
76    /// prevent DNS rebinding attacks against locally running servers. Public
77    /// deployments should override this list with their own hostnames.
78    /// examples:
79    ///     allowed_hosts = ["localhost", "127.0.0.1", "0.0.0.0"]
80    /// or with ports:
81    ///     allowed_hosts = ["example.com", "example.com:8080"]
82    pub allowed_hosts: Vec<String>,
83    /// Allowed browser origins for inbound `Origin` validation.
84    ///
85    /// Defaults to an empty list, which disables Origin validation. When
86    /// non-empty, requests carrying an `Origin` header must match per RFC 6454
87    /// `(scheme, host, port)`; missing-`Origin` requests still pass. Entries
88    /// must include a scheme; `"null"` matches the browser's `Origin: null`.
89    /// examples:
90    ///     allowed_origins = ["https://app.example.com", "http://localhost:8080"]
91    pub allowed_origins: Vec<String>,
92    /// Optional external session store for cross-instance recovery.
93    ///
94    /// When set, [`SessionState`] (the client's `initialize` parameters) is
95    /// persisted after a successful handshake and deleted when the session
96    /// closes. On any subsequent request that arrives at an instance with no
97    /// in-memory session, the store is consulted: if an entry is found the
98    /// session is transparently restored so the client does not need to
99    /// re-initialize.
100    ///
101    /// # Example
102    /// ```rust,ignore
103    /// use std::sync::Arc;
104    /// use rmcp::transport::streamable_http_server::{
105    ///     StreamableHttpServerConfig, session::SessionStore,
106    /// };
107    ///
108    /// let config = StreamableHttpServerConfig {
109    ///     session_store: Some(Arc::new(MyRedisStore::new())),
110    ///     ..Default::default()
111    /// };
112    /// ```
113    pub session_store: Option<Arc<dyn SessionStore>>,
114    /// Maximum POST request body size in bytes.
115    ///
116    /// Enforced while streaming the body, independent of `Content-Length`,
117    /// chunked transfer encoding, or HTTP version. Oversized payloads receive
118    /// a `413 Payload Too Large` response.
119    pub max_request_body_bytes: usize,
120}
121
122impl std::fmt::Debug for dyn SessionStore {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.write_str("<SessionStore>")
125    }
126}
127
128impl Default for StreamableHttpServerConfig {
129    fn default() -> Self {
130        Self {
131            sse_keep_alive: Some(Duration::from_secs(15)),
132            sse_retry: Some(Duration::from_secs(3)),
133            stateful_mode: true,
134            json_response: false,
135            cancellation_token: CancellationToken::new(),
136            allowed_hosts: vec!["localhost".into(), "127.0.0.1".into(), "::1".into()],
137            allowed_origins: vec![],
138            session_store: None,
139            max_request_body_bytes: DEFAULT_MAX_REQUEST_BODY_BYTES,
140        }
141    }
142}
143
144impl StreamableHttpServerConfig {
145    pub fn with_allowed_hosts(
146        mut self,
147        allowed_hosts: impl IntoIterator<Item = impl Into<String>>,
148    ) -> Self {
149        self.allowed_hosts = allowed_hosts.into_iter().map(Into::into).collect();
150        self
151    }
152    /// Disable allowed hosts. This will allow requests with any `Host` header, which is NOT recommended for public deployments.
153    pub fn disable_allowed_hosts(mut self) -> Self {
154        self.allowed_hosts.clear();
155        self
156    }
157    pub fn with_allowed_origins(
158        mut self,
159        allowed_origins: impl IntoIterator<Item = impl Into<String>>,
160    ) -> Self {
161        self.allowed_origins = allowed_origins.into_iter().map(Into::into).collect();
162        self
163    }
164    /// Disable Origin validation, reverting to the default ignore-Origin behavior.
165    pub fn disable_allowed_origins(mut self) -> Self {
166        self.allowed_origins.clear();
167        self
168    }
169    pub fn with_sse_keep_alive(mut self, duration: Option<Duration>) -> Self {
170        self.sse_keep_alive = duration;
171        self
172    }
173
174    pub fn with_sse_retry(mut self, duration: Option<Duration>) -> Self {
175        self.sse_retry = duration;
176        self
177    }
178
179    pub fn with_stateful_mode(mut self, stateful: bool) -> Self {
180        self.stateful_mode = stateful;
181        self
182    }
183
184    pub fn with_json_response(mut self, json_response: bool) -> Self {
185        self.json_response = json_response;
186        self
187    }
188
189    pub fn with_cancellation_token(mut self, token: CancellationToken) -> Self {
190        self.cancellation_token = token;
191        self
192    }
193
194    /// Set the maximum POST request body size in bytes.
195    pub fn with_max_request_body_bytes(mut self, bytes: usize) -> Self {
196        self.max_request_body_bytes = bytes;
197        self
198    }
199}
200
201#[expect(
202    clippy::result_large_err,
203    reason = "BoxResponse is intentionally large; matches other handlers in this file"
204)]
205/// Validates the `MCP-Protocol-Version` header on incoming HTTP requests.
206///
207/// Per the MCP 2025-06-18 spec:
208/// - If the header is present but contains an unsupported version, return 400 Bad Request.
209/// - If the header is absent, assume `2025-03-26` for backwards compatibility (no error).
210fn validate_protocol_version_header(
211    headers: &http::HeaderMap,
212    allow_unknown: bool,
213) -> Result<(), BoxResponse> {
214    if let Some(value) = headers.get(HEADER_MCP_PROTOCOL_VERSION) {
215        let version_str = value.to_str().map_err(|_| {
216            Response::builder()
217                .status(http::StatusCode::BAD_REQUEST)
218                .body(
219                    Full::new(Bytes::from(
220                        "Bad Request: Invalid MCP-Protocol-Version header encoding",
221                    ))
222                    .boxed(),
223                )
224                .expect("valid response")
225        })?;
226        let is_known = ProtocolVersion::KNOWN_VERSIONS
227            .iter()
228            .any(|v| v.as_str() == version_str);
229        if !allow_unknown && !is_known {
230            return Err(Response::builder()
231                .status(http::StatusCode::BAD_REQUEST)
232                .body(
233                    Full::new(Bytes::from(format!(
234                        "Bad Request: Unsupported MCP-Protocol-Version: {version_str}"
235                    )))
236                    .boxed(),
237                )
238                .expect("valid response"));
239        }
240    }
241    Ok(())
242}
243
244fn message_has_per_request_protocol_version(message: &ClientJsonRpcMessage) -> bool {
245    match message {
246        ClientJsonRpcMessage::Request(request) => {
247            request.request.get_meta().protocol_version().is_some()
248        }
249        _ => false,
250    }
251}
252
253fn invalid_request_jsonrpc_response(
254    id: Option<RequestId>,
255    message: impl Into<Cow<'static, str>>,
256) -> BoxResponse {
257    let err = JsonRpcError::new(id, ErrorData::invalid_request(message, None));
258    let body = serde_json::to_vec(&err).expect("serialize JsonRpcError");
259    Response::builder()
260        .status(http::StatusCode::BAD_REQUEST)
261        .header(http::header::CONTENT_TYPE, JSON_MIME_TYPE)
262        .body(Full::new(Bytes::from(body)).boxed())
263        .expect("valid response")
264}
265
266fn invalid_params_jsonrpc_response(
267    id: Option<RequestId>,
268    message: impl Into<Cow<'static, str>>,
269) -> BoxResponse {
270    let err = JsonRpcError::new(id, ErrorData::invalid_params(message, None));
271    let body = serde_json::to_vec(&err).expect("serialize JsonRpcError");
272    Response::builder()
273        .status(http::StatusCode::BAD_REQUEST)
274        .header(http::header::CONTENT_TYPE, JSON_MIME_TYPE)
275        .body(Full::new(Bytes::from(body)).boxed())
276        .expect("valid response")
277}
278
279#[expect(
280    clippy::result_large_err,
281    reason = "BoxResponse is intentionally large; matches other handlers in this file"
282)]
283/// Absent header is allowed; the first initialize round-trip may legitimately omit it.
284fn validate_header_matches_init_body(
285    headers: &http::HeaderMap,
286    body_version: &str,
287    request_id: Option<RequestId>,
288) -> Result<(), BoxResponse> {
289    let Some(header_value) = headers.get(HEADER_MCP_PROTOCOL_VERSION) else {
290        return Ok(());
291    };
292    let header_str = header_value.to_str().map_err(|_| {
293        invalid_request_jsonrpc_response(
294            request_id.clone(),
295            "Invalid Request: MCP-Protocol-Version header is not valid UTF-8",
296        )
297    })?;
298    if header_str != body_version {
299        tracing::warn!(
300            header = header_str,
301            body = body_version,
302            "rejecting initialize: MCP-Protocol-Version header does not match params.protocolVersion"
303        );
304        return Err(invalid_request_jsonrpc_response(
305            request_id,
306            format!(
307                "Invalid Request: MCP-Protocol-Version header ({header_str}) does not match initialize params.protocolVersion ({body_version})"
308            ),
309        ));
310    }
311    Ok(())
312}
313
314#[expect(
315    clippy::result_large_err,
316    reason = "BoxResponse is intentionally large; matches other handlers in this file"
317)]
318fn validate_request_protocol_version_meta(
319    headers: &HeaderMap,
320    message: &ClientJsonRpcMessage,
321) -> Result<(), BoxResponse> {
322    let ClientJsonRpcMessage::Request(request) = message else {
323        return Ok(());
324    };
325    if matches!(&request.request, ClientRequest::InitializeRequest(_)) {
326        return Ok(());
327    }
328    let is_discover = matches!(&request.request, ClientRequest::DiscoverRequest(_));
329    let Some(meta_version) = request.request.get_meta().protocol_version() else {
330        if is_discover {
331            return Err(invalid_params_jsonrpc_response(
332                Some(request.id.clone()),
333                "Invalid params: server/discover requires protocolVersion in request _meta",
334            ));
335        }
336        return Ok(());
337    };
338    let Some(header_version) = headers
339        .get(HEADER_MCP_PROTOCOL_VERSION)
340        .and_then(|value| value.to_str().ok())
341    else {
342        return Err(invalid_request_jsonrpc_response(
343            Some(request.id.clone()),
344            "Invalid Request: request _meta protocolVersion requires MCP-Protocol-Version header",
345        ));
346    };
347    if header_version != meta_version.as_str() {
348        return Err(header_mismatch_jsonrpc_response(
349            Some(request.id.clone()),
350            format!(
351                "MCP-Protocol-Version header ({header_version}) does not match request _meta protocolVersion ({meta_version})"
352            ),
353        ));
354    }
355    Ok(())
356}
357
358fn jsonrpc_http_status(message: &ServerJsonRpcMessage) -> http::StatusCode {
359    let ServerJsonRpcMessage::Error(error) = message else {
360        return http::StatusCode::OK;
361    };
362    // Modern per-request HTTP treats invalid params as a malformed request.
363    // Legacy requests bypass this mapper and retain HTTP 200 JSON-RPC errors.
364    match error.error.code {
365        ErrorCode::UNSUPPORTED_PROTOCOL_VERSION
366        | ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY
367        | ErrorCode::INVALID_PARAMS => http::StatusCode::BAD_REQUEST,
368        ErrorCode::METHOD_NOT_FOUND => http::StatusCode::NOT_FOUND,
369        _ => http::StatusCode::OK,
370    }
371}
372
373fn jsonrpc_message_response(
374    message: ServerJsonRpcMessage,
375    map_protocol_status: bool,
376) -> Result<BoxResponse, BoxResponse> {
377    let status = if map_protocol_status {
378        jsonrpc_http_status(&message)
379    } else {
380        http::StatusCode::OK
381    };
382    let body =
383        serde_json::to_vec(&message).map_err(internal_error_response("serialize json response"))?;
384    Ok(Response::builder()
385        .status(status)
386        .header(http::header::CONTENT_TYPE, JSON_MIME_TYPE)
387        .body(Full::new(Bytes::from(body)).boxed())
388        .expect("valid response"))
389}
390
391fn header_mismatch_jsonrpc_response(
392    id: Option<RequestId>,
393    message: impl Into<Cow<'static, str>>,
394) -> BoxResponse {
395    let err = JsonRpcError::new(id, ErrorData::header_mismatch(message, None));
396    let body = serde_json::to_vec(&err).expect("serialize JsonRpcError");
397    Response::builder()
398        .status(http::StatusCode::BAD_REQUEST)
399        .header(http::header::CONTENT_TYPE, JSON_MIME_TYPE)
400        .body(Full::new(Bytes::from(body)).boxed())
401        .expect("valid response")
402}
403
404/// Validates SEP-2243 `Mcp-Method` / `Mcp-Name` / `Mcp-Param-*` headers against the body.
405///
406/// Only enforced when the request declares a protocol version `>= STANDARD_HEADERS`.
407/// The `initialize` handshake is exempt: clients emit these headers only after the
408/// version has been negotiated. `tool_schema` supplies the called tool's input schema
409/// so annotated `Mcp-Param-*` headers can be checked (no schema => those are skipped).
410#[expect(
411    clippy::result_large_err,
412    reason = "BoxResponse is intentionally large; matches other handlers in this file"
413)]
414fn validate_standard_headers(
415    headers: &HeaderMap,
416    message: &ClientJsonRpcMessage,
417    tool_schema: impl Fn(&str) -> Option<Arc<JsonObject>>,
418) -> Result<(), BoxResponse> {
419    let version_requires_headers = headers
420        .get(HEADER_MCP_PROTOCOL_VERSION)
421        .and_then(|value| value.to_str().ok())
422        .is_some_and(|version| version >= ProtocolVersion::STANDARD_HEADERS.as_str());
423    if !version_requires_headers {
424        return Ok(());
425    }
426
427    let request_id = match message {
428        ClientJsonRpcMessage::Request(req) => {
429            if matches!(&req.request, ClientRequest::InitializeRequest(_)) {
430                return Ok(());
431            }
432            Some(req.id.clone())
433        }
434        ClientJsonRpcMessage::Notification(_) => None,
435        _ => return Ok(()),
436    };
437
438    let Ok(value) = serde_json::to_value(message) else {
439        return Ok(());
440    };
441    // For tools/call, look up the tool schema so Mcp-Param-* headers are validated.
442    let schema = value
443        .get("method")
444        .and_then(|method| method.as_str())
445        .filter(|method| *method == "tools/call")
446        .and_then(|_| value.get("params"))
447        .and_then(|params| params.get("name"))
448        .and_then(|name| name.as_str())
449        .and_then(tool_schema);
450    if let Err(reason) = mcp_headers::validate_request_headers(headers, &value, schema.as_deref()) {
451        return Err(header_mismatch_jsonrpc_response(request_id, reason));
452    }
453    Ok(())
454}
455
456fn forbidden_response(message: impl Into<String>) -> BoxResponse {
457    Response::builder()
458        .status(http::StatusCode::FORBIDDEN)
459        .body(Full::new(Bytes::from(message.into())).boxed())
460        .expect("valid response")
461}
462
463fn normalize_host(host: &str) -> String {
464    host.trim_matches('[')
465        .trim_matches(']')
466        .to_ascii_lowercase()
467}
468
469#[derive(Debug, Clone, PartialEq, Eq)]
470struct NormalizedAuthority {
471    host: String,
472    port: Option<u16>,
473}
474
475fn normalize_authority(host: &str, port: Option<u16>) -> NormalizedAuthority {
476    NormalizedAuthority {
477        host: normalize_host(host),
478        port,
479    }
480}
481
482fn parse_allowed_authority(allowed: &str) -> Option<NormalizedAuthority> {
483    let allowed = allowed.trim();
484    if allowed.is_empty() {
485        return None;
486    }
487
488    if let Ok(authority) = http::uri::Authority::try_from(allowed) {
489        return Some(normalize_authority(authority.host(), authority.port_u16()));
490    }
491
492    Some(normalize_authority(allowed, None))
493}
494
495fn host_is_allowed(host: &NormalizedAuthority, allowed_hosts: &[String]) -> bool {
496    if allowed_hosts.is_empty() {
497        // If the allowed hosts list is empty, allow all hosts (not recommended).
498        return true;
499    }
500    allowed_hosts
501        .iter()
502        .filter_map(|allowed| parse_allowed_authority(allowed))
503        .any(|allowed| {
504            allowed.host == host.host
505                && match allowed.port {
506                    Some(port) => host.port == Some(port),
507                    None => true,
508                }
509        })
510}
511
512#[derive(Debug, Clone, PartialEq, Eq)]
513enum NormalizedOrigin {
514    Null,
515    Tuple {
516        scheme: String,
517        host: String,
518        port: Option<u16>,
519    },
520}
521
522fn parse_origin_value(value: &str) -> Option<NormalizedOrigin> {
523    let value = value.trim();
524    if value.is_empty() {
525        return None;
526    }
527    if value.eq_ignore_ascii_case("null") {
528        return Some(NormalizedOrigin::Null);
529    }
530    let uri = http::Uri::try_from(value).ok()?;
531    let scheme = uri.scheme_str()?.to_ascii_lowercase();
532    let authority = uri.authority()?;
533    Some(NormalizedOrigin::Tuple {
534        scheme,
535        host: normalize_host(authority.host()),
536        port: authority.port_u16(),
537    })
538}
539
540fn origin_is_allowed(origin: &NormalizedOrigin, allowed_origins: &[String]) -> bool {
541    if allowed_origins.is_empty() {
542        return true;
543    }
544    allowed_origins
545        .iter()
546        .filter_map(|raw| parse_origin_value(raw))
547        .any(|allowed| match (&allowed, origin) {
548            (NormalizedOrigin::Null, NormalizedOrigin::Null) => true,
549            (
550                NormalizedOrigin::Tuple {
551                    scheme: a_scheme,
552                    host: a_host,
553                    port: a_port,
554                },
555                NormalizedOrigin::Tuple {
556                    scheme: o_scheme,
557                    host: o_host,
558                    port: o_port,
559                },
560            ) => a_scheme == o_scheme && a_host == o_host && (a_port.is_none() || a_port == o_port),
561            _ => false,
562        })
563}
564
565fn bad_request_response(message: &str) -> BoxResponse {
566    let body = Full::from(message.to_string()).boxed();
567
568    http::Response::builder()
569        .status(http::StatusCode::BAD_REQUEST)
570        .header(http::header::CONTENT_TYPE, "text/plain; charset=utf-8")
571        .body(body)
572        .expect("failed to build bad request response")
573}
574
575fn parse_host_header(
576    uri: &http::Uri,
577    headers: &HeaderMap,
578) -> Result<NormalizedAuthority, BoxResponse> {
579    if let Some(host) = headers.get(http::header::HOST) {
580        let host_str = host
581            .to_str()
582            .inspect_err(|_| {
583                tracing::warn!(host = ?host, "rejected request with non-UTF-8 Host header");
584            })
585            .map_err(|_| bad_request_response("Bad Request: Invalid Host header encoding"))?;
586        let authority = http::uri::Authority::try_from(host_str)
587            .inspect_err(|_| {
588                tracing::warn!(
589                    host = host_str,
590                    "rejected request with malformed Host header"
591                );
592            })
593            .map_err(|_| bad_request_response("Bad Request: Invalid Host header"))?;
594        return Ok(normalize_authority(authority.host(), authority.port_u16()));
595    }
596    // HTTP/2 carries the host in `:authority`; middleware such as
597    // `axum::Router::nest` can drop the `Host` header hyper synthesizes from it.
598    let authority = uri.authority().ok_or_else(|| {
599        tracing::warn!("rejected request with missing Host header and no :authority");
600        bad_request_response("Bad Request: missing Host header")
601    })?;
602    Ok(normalize_authority(authority.host(), authority.port_u16()))
603}
604
605fn validate_dns_rebinding_headers(
606    uri: &http::Uri,
607    headers: &HeaderMap,
608    config: &StreamableHttpServerConfig,
609) -> Result<(), BoxResponse> {
610    let host = parse_host_header(uri, headers)?;
611    if !host_is_allowed(&host, &config.allowed_hosts) {
612        tracing::warn!(
613            host = ?host,
614            "rejected request with disallowed Host header (possible DNS rebinding attempt)",
615        );
616        return Err(forbidden_response("Forbidden: Host header is not allowed"));
617    }
618    validate_origin_header(headers, &config.allowed_origins)?;
619    Ok(())
620}
621
622fn validate_origin_header(
623    headers: &HeaderMap,
624    allowed_origins: &[String],
625) -> Result<(), BoxResponse> {
626    if allowed_origins.is_empty() {
627        return Ok(());
628    }
629    let Some(origin_header) = headers.get(http::header::ORIGIN) else {
630        return Ok(());
631    };
632    let origin_str = origin_header
633        .to_str()
634        .inspect_err(|_| {
635            tracing::warn!(origin = ?origin_header, "rejected request with non-UTF-8 Origin header");
636        })
637        .map_err(|_| bad_request_response("Bad Request: Invalid Origin header encoding"))?;
638    let origin = parse_origin_value(origin_str).ok_or_else(|| {
639        tracing::warn!(
640            origin = origin_str,
641            "rejected request with malformed Origin header",
642        );
643        bad_request_response("Bad Request: Invalid Origin header")
644    })?;
645    if !origin_is_allowed(&origin, allowed_origins) {
646        tracing::warn!(
647            origin = ?origin,
648            "rejected request with disallowed Origin header (possible cross-origin attack)",
649        );
650        return Err(forbidden_response(
651            "Forbidden: Origin header is not allowed",
652        ));
653    }
654    Ok(())
655}
656
657/// # Streamable HTTP server
658///
659/// An HTTP service that implements the
660/// [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http)
661/// for MCP servers.
662///
663/// ## Session management
664///
665/// When [`StreamableHttpServerConfig::stateful_mode`] is `true` (the default),
666/// the server creates a session for each client that sends an `initialize`
667/// request. The session ID is returned in the `Mcp-Session-Id` response header
668/// and the client must include it on all subsequent requests.
669///
670/// Two tool calls carrying the same `Mcp-Session-Id` come from the same logical
671/// session (typically one conversation in an LLM client). Different session IDs
672/// mean different sessions.
673///
674/// The [`SessionManager`] trait controls how sessions are stored and routed:
675///
676/// * [`LocalSessionManager`](super::session::local::LocalSessionManager) —
677///   in-memory session store (default).
678/// * [`NeverSessionManager`](super::session::never::NeverSessionManager) —
679///   disables sessions entirely (stateless mode).
680///
681/// ## Accessing HTTP request data from tool handlers
682///
683/// The service consumes the request body but injects the remaining
684/// [`http::request::Parts`] into [`crate::model::Extensions`], which is
685/// accessible through [`crate::service::RequestContext`].
686///
687/// ### Reading the raw HTTP parts
688///
689/// ```rust
690/// use rmcp::handler::server::tool::Extension;
691/// use http::request::Parts;
692/// async fn my_tool(Extension(parts): Extension<Parts>) {
693///     tracing::info!("http parts:{parts:?}")
694/// }
695/// ```
696///
697/// ### Reading the session ID inside a tool handler
698///
699/// ```rust,ignore
700/// use rmcp::handler::server::tool::Extension;
701/// use rmcp::service::RequestContext;
702/// use rmcp::model::RoleServer;
703///
704/// #[tool(description = "session-aware tool")]
705/// async fn my_tool(
706///     &self,
707///     Extension(parts): Extension<http::request::Parts>,
708/// ) -> Result<CallToolResult, rmcp::ErrorData> {
709///     if let Some(session_id) = parts.headers.get("mcp-session-id") {
710///         tracing::info!(?session_id, "called from session");
711///     }
712///     // ...
713///     # todo!()
714/// }
715/// ```
716///
717/// ### Accessing custom axum/tower extension state
718///
719/// State added via axum's `Extension` layer is available inside
720/// `Parts.extensions`:
721///
722/// ```rust,ignore
723/// use rmcp::service::RequestContext;
724/// use rmcp::model::RoleServer;
725///
726/// #[derive(Clone)]
727/// struct AppState { /* ... */ }
728///
729/// #[tool(description = "example")]
730/// async fn my_tool(
731///     &self,
732///     ctx: RequestContext<RoleServer>,
733/// ) -> Result<CallToolResult, rmcp::ErrorData> {
734///     let parts = ctx.extensions.get::<http::request::Parts>().unwrap();
735///     let state = parts.extensions.get::<AppState>().unwrap();
736///     // use state...
737///     # todo!()
738/// }
739/// ```
740pub struct StreamableHttpService<S, M> {
741    pub config: StreamableHttpServerConfig,
742    session_manager: Arc<M>,
743    service_factory: Arc<dyn Fn() -> Result<S, std::io::Error> + Send + Sync>,
744    /// Tracks in-progress session restores so that concurrent requests for the
745    /// same unknown session ID wait for the first restore to complete rather
746    /// than racing to replay the initialize handshake. `None` when no external
747    /// session store is configured (avoids allocating the map).
748    pending_restores: Option<
749        Arc<tokio::sync::RwLock<HashMap<SessionId, tokio::sync::watch::Sender<Option<bool>>>>>,
750    >,
751    /// Caches tool input schemas by name for SEP-2243 `Mcp-Param-*` validation.
752    /// Populated lazily via `get_tool` so the service factory runs at most once
753    /// per tool name. `None` value means the tool exposes no schema.
754    tool_schemas: Arc<std::sync::RwLock<HashMap<String, Option<Arc<JsonObject>>>>>,
755}
756
757impl<S, M> Clone for StreamableHttpService<S, M> {
758    fn clone(&self) -> Self {
759        Self {
760            config: self.config.clone(),
761            session_manager: self.session_manager.clone(),
762            service_factory: self.service_factory.clone(),
763            pending_restores: self.pending_restores.clone(),
764            tool_schemas: self.tool_schemas.clone(),
765        }
766    }
767}
768
769impl<RequestBody, S, M> tower_service::Service<Request<RequestBody>> for StreamableHttpService<S, M>
770where
771    RequestBody: Body + Send + 'static,
772    S: crate::ServerHandler + Send + 'static,
773    M: SessionManager,
774    RequestBody::Error: Display,
775    RequestBody::Data: Send + 'static,
776{
777    type Response = BoxResponse;
778    type Error = Infallible;
779    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
780    fn call(&mut self, req: http::Request<RequestBody>) -> Self::Future {
781        let service = self.clone();
782        Box::pin(async move {
783            let response = service.handle(req).await;
784            Ok(response)
785        })
786    }
787    fn poll_ready(
788        &mut self,
789        _cx: &mut std::task::Context<'_>,
790    ) -> std::task::Poll<Result<(), Self::Error>> {
791        std::task::Poll::Ready(Ok(()))
792    }
793}
794
795/// Guard used inside [`StreamableHttpService::try_restore_from_store`].
796///
797/// Ensures the `pending_restores` map entry is always cleaned up — even when
798/// the future is cancelled mid-await.
799///
800/// `result` defaults to `false` (failure / cancellation). Only the success path
801/// needs to set it to `true` before returning.
802struct PendingRestoreGuard {
803    pending_restores:
804        Arc<tokio::sync::RwLock<HashMap<SessionId, tokio::sync::watch::Sender<Option<bool>>>>>,
805    session_id: SessionId,
806    watch_tx: tokio::sync::watch::Sender<Option<bool>>,
807    /// The value that will be broadcast to waiting tasks on drop.
808    result: bool,
809}
810
811impl Drop for PendingRestoreGuard {
812    fn drop(&mut self) {
813        // `send` is synchronous — unblocks waiters immediately, no lock needed.
814        let _ = self.watch_tx.send(Some(self.result));
815        // Remove the map entry asynchronously (requires the async write lock).
816        let pending_restores = self.pending_restores.clone();
817        let session_id = self.session_id.clone();
818        tokio::spawn(async move {
819            pending_restores.write().await.remove(&session_id);
820        });
821    }
822}
823
824impl<S, M> StreamableHttpService<S, M>
825where
826    S: crate::ServerHandler + Send + 'static,
827    M: SessionManager,
828{
829    pub fn new(
830        service_factory: impl Fn() -> Result<S, std::io::Error> + Send + Sync + 'static,
831        session_manager: Arc<M>,
832        config: StreamableHttpServerConfig,
833    ) -> Self {
834        let pending_restores = config.session_store.is_some().then(|| {
835            Arc::new(tokio::sync::RwLock::new(HashMap::<
836                SessionId,
837                tokio::sync::watch::Sender<Option<bool>>,
838            >::new()))
839        });
840        Self {
841            config,
842            session_manager,
843            service_factory: Arc::new(service_factory),
844            pending_restores,
845            tool_schemas: Arc::new(std::sync::RwLock::new(HashMap::new())),
846        }
847    }
848    fn get_service(&self) -> Result<S, std::io::Error> {
849        (self.service_factory)()
850    }
851
852    // The HTTP status must be known before opening an SSE stream.
853    async fn serve_negotiated_request_directly(
854        &self,
855        service: S,
856        mut request: crate::model::JsonRpcRequest<ClientRequest>,
857        parts: http::request::Parts,
858    ) -> Result<BoxResponse, BoxResponse> {
859        let peer_info = Self::peer_info_for_stateless_request(&request, &parts.headers);
860        request.request.extensions_mut().insert(parts);
861        let (transport, mut receiver) =
862            OneshotTransport::<RoleServer>::new(ClientJsonRpcMessage::Request(request));
863        // Give this stateless request its own cancellation token so a client
864        // disconnect can cancel the in-flight handler (#857), as in the
865        // non-negotiated stateless path below.
866        let request_ct = CancellationToken::new();
867        let service = serve_directly_with_ct(service, transport, peer_info, request_ct.clone());
868        tokio::spawn(async move {
869            let _ = service.waiting().await;
870        });
871
872        let cancel = self.config.cancellation_token.child_token();
873        // Cancel the handler if the client disconnects while it is still
874        // producing its first message (this future is dropped before
875        // `receiver.recv()` completes). Disarmed once the handler emits
876        // anything, so a normal response is never cancelled.
877        let mut disconnect_guard = Some(request_ct.clone().drop_guard());
878        let first = tokio::select! {
879            message = receiver.recv() => {
880                if let Some(guard) = disconnect_guard.take() {
881                    guard.disarm();
882                }
883                message
884            }
885            _ = cancel.cancelled() => None,
886        }
887        .ok_or_else(|| {
888            internal_error_response("empty response")(std::io::Error::new(
889                std::io::ErrorKind::UnexpectedEof,
890                "no response message received from handler",
891            ))
892        })?;
893
894        if self.config.json_response || jsonrpc_http_status(&first) != http::StatusCode::OK {
895            // This message is the whole reply, so `receiver` is dropped here and
896            // anything the handler emits afterwards is undeliverable. Cancel it so
897            // a still-running handler stops instead of running on unobserved: its
898            // terminal `send` would otherwise fail before adding the termination
899            // permit, leaving the serve loop parked forever. A no-op when the
900            // handler already completed.
901            request_ct.cancel();
902            return jsonrpc_message_response(first, true);
903        }
904
905        // The handler may still be streaming, so guard the response: dropping it
906        // (client disconnect) must cancel the handler.
907        let stream = futures::stream::once(async move { first })
908            .chain(ReceiverStream::new(receiver))
909            .map(|message| {
910                tracing::trace!(?message);
911                ServerSseMessage::from_message(message)
912            });
913        Ok(sse_stream_response(
914            CancelOnDisconnect::new(stream, request_ct),
915            self.config.sse_keep_alive,
916            self.config.cancellation_token.child_token(),
917        ))
918    }
919
920    /// Returns the cached input schema for `name`, constructing a service once
921    /// per name to read its `ServerHandler::get_tool` definition. Used to
922    /// validate SEP-2243 `Mcp-Param-*` headers against the request body.
923    fn tool_schema(&self, name: &str) -> Option<Arc<JsonObject>> {
924        if let Ok(cache) = self.tool_schemas.read() {
925            if let Some(schema) = cache.get(name) {
926                return schema.clone();
927            }
928        }
929        let schema = self
930            .get_service()
931            .ok()
932            .and_then(|service| service.get_tool(name))
933            .map(|tool| tool.input_schema);
934        if let Ok(mut cache) = self.tool_schemas.write() {
935            cache.insert(name.to_owned(), schema.clone());
936        }
937        schema
938    }
939
940    /// Spawn a task that runs `serve_server` for the given session, waits for
941    /// it to finish, and then calls `close_session`.
942    ///
943    /// `init_done_tx`: when `Some`, the sender is fired after `serve_server`
944    /// returns successfully, signalling to the caller that the MCP handshake
945    /// is complete. Used by `try_restore_from_store` to synchronise with the
946    /// restore `initialize` replay; `handle_post` passes `None`.
947    fn spawn_session_worker(
948        session_manager: Arc<M>,
949        session_id: SessionId,
950        service: S,
951        transport: M::Transport,
952        init_done_tx: Option<tokio::sync::oneshot::Sender<()>>,
953    ) where
954        S: crate::ServerHandler + Send + 'static,
955        M: SessionManager,
956    {
957        tokio::spawn(async move {
958            let svc =
959                serve_server::<S, M::Transport, _, TransportAdapterIdentity>(service, transport)
960                    .await;
961            match svc {
962                Ok(svc) => {
963                    if let Some(tx) = init_done_tx {
964                        let _ = tx.send(());
965                    }
966                    let _ = svc.waiting().await;
967                }
968                Err(e) => {
969                    tracing::error!("Failed to serve session: {e}");
970                    // Dropping init_done_tx (if Some) signals failure to the caller.
971                }
972            }
973            let _ = session_manager
974                .close_session(&session_id)
975                .await
976                .inspect_err(|e| {
977                    tracing::error!("Failed to close session {session_id}: {e}");
978                });
979        });
980    }
981
982    /// Attempt to restore a session from the external store.
983    ///
984    /// Returns `true` when the session is available and ready to serve the
985    /// current request (either just restored or already in memory). Returns
986    /// `false` when no store is configured or the session ID is unknown.
987    ///
988    /// Concurrent requests for the same unknown session ID are serialized: the
989    /// first caller performs the full restore and handshake replay while others
990    /// subscribe to a `watch` channel and wait, avoiding duplicate handshakes.
991    async fn try_restore_from_store(
992        &self,
993        session_id: &SessionId,
994        parts: &http::request::Parts,
995    ) -> Result<bool, std::io::Error>
996    where
997        S: crate::ServerHandler + Send + 'static,
998        M: SessionManager,
999    {
1000        // Both fields are Some iff a session store is configured.
1001        let (Some(pending_restores), Some(store)) =
1002            (&self.pending_restores, &self.config.session_store)
1003        else {
1004            return Ok(false);
1005        };
1006
1007        // Serialize concurrent restores for the same session ID.
1008        // Write-lock once: if another task is already restoring, subscribe and wait;
1009        // otherwise, register ourselves as the restoring task.
1010        // Channel value: None = in progress, Some(true) = restored, Some(false) = not found/failed.
1011        let (watch_tx, _watch_rx) = tokio::sync::watch::channel(None::<bool>);
1012        {
1013            let mut pending = pending_restores.write().await;
1014            if let Some(tx) = pending.get(session_id) {
1015                let mut rx = tx.subscribe();
1016                drop(pending);
1017                // Wait for the restore to finish, then propagate the outcome.
1018                let result = rx
1019                    .wait_for(|r| r.is_some())
1020                    .await
1021                    .map(|r| r.unwrap_or(false))
1022                    .unwrap_or(false);
1023                return Ok(result);
1024            }
1025            pending.insert(session_id.clone(), watch_tx.clone());
1026        }
1027
1028        // Guard: signals waiters and cleans up the map entry on drop
1029        let mut guard = PendingRestoreGuard {
1030            pending_restores: pending_restores.clone(),
1031            session_id: session_id.clone(),
1032            watch_tx: watch_tx.clone(),
1033            result: false,
1034        };
1035
1036        // --- Step 3: load from external store ---
1037        let state = match store.load(session_id.as_ref()).await {
1038            Ok(Some(s)) => s,
1039            Ok(None) => {
1040                return Ok(false);
1041            }
1042            Err(e) => {
1043                tracing::error!(
1044                    session_id = session_id.as_ref(),
1045                    error = %e,
1046                    "session store load failed during restore"
1047                );
1048                return Err(std::io::Error::other(e));
1049            }
1050        };
1051
1052        // --- Step 4: ask the session manager to allocate an in-memory worker ---
1053        let transport = match self
1054            .session_manager
1055            .restore_session(session_id.clone())
1056            .await
1057            .map_err(|e| std::io::Error::other(e.to_string()))
1058        {
1059            Ok(RestoreOutcome::Restored(t)) => t,
1060            Ok(RestoreOutcome::AlreadyPresent) => {
1061                // Invariant violation: pending_restores ensures only one task can call
1062                // restore_session per session ID, so AlreadyPresent is impossible here.
1063                return Err(std::io::Error::other(
1064                    "restore_session returned AlreadyPresent unexpectedly; session manager might have modified the session store outside of the restore_session API",
1065                ));
1066            }
1067            Ok(RestoreOutcome::NotSupported) => {
1068                return Ok(false);
1069            }
1070            Err(e) => {
1071                return Err(e);
1072            }
1073        };
1074
1075        // --- Step 5: replay the MCP initialize handshake ---
1076        let service = match self.get_service() {
1077            Ok(s) => s,
1078            Err(e) => {
1079                return Err(e);
1080            }
1081        };
1082
1083        // `serve_server` requires both the `initialize` request and the
1084        // `notifications/initialized` notification before transitioning to
1085        // the running state — we must send both before returning.
1086        let mut restore_init = ClientJsonRpcMessage::request(
1087            ClientRequest::InitializeRequest(InitializeRequest {
1088                params: state.initialize_params,
1089                ..Default::default()
1090            }),
1091            crate::model::NumberOrString::Number(0),
1092        );
1093        restore_init.insert_extension(parts.clone());
1094        restore_init.insert_extension(SessionRestoreMarker {
1095            id: session_id.clone(),
1096        });
1097        let mut restore_initialized = ClientJsonRpcMessage::notification(
1098            ClientNotification::InitializedNotification(InitializedNotification {
1099                ..Default::default()
1100            }),
1101        );
1102        restore_initialized.insert_extension(parts.clone());
1103        restore_initialized.insert_extension(SessionRestoreMarker {
1104            id: session_id.clone(),
1105        });
1106        // Signal from the spawned task once serve_server finishes initialising.
1107        let (init_done_tx, init_done_rx) = tokio::sync::oneshot::channel::<()>();
1108
1109        Self::spawn_session_worker(
1110            self.session_manager.clone(),
1111            session_id.clone(),
1112            service,
1113            transport,
1114            Some(init_done_tx),
1115        );
1116
1117        if let Err(e) = self
1118            .session_manager
1119            .initialize_session(session_id, restore_init)
1120            .await
1121            .map_err(|e| std::io::Error::other(e.to_string()))
1122        {
1123            return Err(e);
1124        }
1125
1126        if let Err(e) = self
1127            .session_manager
1128            .accept_message(session_id, restore_initialized)
1129            .await
1130            .map_err(|e| std::io::Error::other(e.to_string()))
1131        {
1132            return Err(e);
1133        }
1134
1135        if init_done_rx.await.is_err() {
1136            return Err(std::io::Error::other(
1137                "serve_server initialization failed during restore",
1138            ));
1139        }
1140
1141        // Restore complete — wake any waiting concurrent requests.
1142        guard.result = true;
1143
1144        tracing::debug!(
1145            session_id = session_id.as_ref(),
1146            "session restored from external store"
1147        );
1148        Ok(true)
1149    }
1150    pub async fn handle<B>(&self, request: Request<B>) -> Response<BoxBody<Bytes, Infallible>>
1151    where
1152        B: Body + Send + 'static,
1153        B::Error: Display,
1154    {
1155        if let Err(response) =
1156            validate_dns_rebinding_headers(request.uri(), request.headers(), &self.config)
1157        {
1158            return response;
1159        }
1160        let method = request.method().clone();
1161        let allowed_methods = match self.config.stateful_mode {
1162            true => "GET, POST, DELETE",
1163            false => "POST",
1164        };
1165        let result = match (method, self.config.stateful_mode) {
1166            (Method::POST, _) => self.handle_post(request).await,
1167            // if we're not in stateful mode, we don't support GET or DELETE because there is no session
1168            (Method::GET, true) => self.handle_get(request).await,
1169            (Method::DELETE, true) => self.handle_delete(request).await,
1170            _ => {
1171                // Handle other methods or return an error
1172                let response = Response::builder()
1173                    .status(http::StatusCode::METHOD_NOT_ALLOWED)
1174                    .header(ALLOW, allowed_methods)
1175                    .body(Full::new(Bytes::from("Method Not Allowed")).boxed())
1176                    .expect("valid response");
1177                return response;
1178            }
1179        };
1180        match result {
1181            Ok(response) => response,
1182            Err(response) => response,
1183        }
1184    }
1185    async fn handle_get<B>(&self, request: Request<B>) -> Result<BoxResponse, BoxResponse>
1186    where
1187        B: Body + Send + 'static,
1188        B::Error: Display,
1189    {
1190        // check accept header
1191        if !request
1192            .headers()
1193            .get(http::header::ACCEPT)
1194            .and_then(|header| header.to_str().ok())
1195            .is_some_and(|header| header.contains(EVENT_STREAM_MIME_TYPE))
1196        {
1197            return Ok(Response::builder()
1198                .status(http::StatusCode::NOT_ACCEPTABLE)
1199                .body(
1200                    Full::new(Bytes::from(
1201                        "Not Acceptable: Client must accept text/event-stream",
1202                    ))
1203                    .boxed(),
1204                )
1205                .expect("valid response"));
1206        }
1207        // check session id
1208        let session_id = request
1209            .headers()
1210            .get(HEADER_SESSION_ID)
1211            .and_then(|v| v.to_str().ok())
1212            .map(|s| s.to_owned().into());
1213        let Some(session_id) = session_id else {
1214            // MCP spec: servers that require a session ID SHOULD respond with 400 Bad Request
1215            return Ok(Response::builder()
1216                .status(http::StatusCode::BAD_REQUEST)
1217                .body(Full::new(Bytes::from("Bad Request: Session ID is required")).boxed())
1218                .expect("valid response"));
1219        };
1220        // check if session exists
1221        let has_session = self
1222            .session_manager
1223            .has_session(&session_id)
1224            .await
1225            .map_err(internal_error_response("check session"))?;
1226        let (parts, _) = request.into_parts();
1227        if !has_session {
1228            // Attempt transparent cross-instance restore from external store.
1229            let restored = self
1230                .try_restore_from_store(&session_id, &parts)
1231                .await
1232                .map_err(internal_error_response("restore session"))?;
1233            if !restored {
1234                // MCP spec: server MUST respond with 404 Not Found for terminated/unknown sessions
1235                return Ok(Response::builder()
1236                    .status(http::StatusCode::NOT_FOUND)
1237                    .body(Full::new(Bytes::from("Not Found: Session not found")).boxed())
1238                    .expect("valid response"));
1239            }
1240        }
1241        // Validate MCP-Protocol-Version header (per 2025-06-18 spec)
1242        validate_protocol_version_header(&parts.headers, false)?;
1243        // check if last event id is provided
1244        let last_event_id = parts
1245            .headers
1246            .get(HEADER_LAST_EVENT_ID)
1247            .and_then(|v| v.to_str().ok())
1248            .map(|s| s.to_owned());
1249        if let Some(last_event_id) = last_event_id {
1250            match self
1251                .session_manager
1252                .resume(&session_id, last_event_id)
1253                .await
1254            {
1255                Ok(stream) => {
1256                    return Ok(sse_stream_response(
1257                        stream,
1258                        self.config.sse_keep_alive,
1259                        self.config.cancellation_token.child_token(),
1260                    ));
1261                }
1262                Err(e) => {
1263                    // Return 200 with an immediately-closed empty stream.
1264                    // Returning an HTTP error would cause EventSource to retry
1265                    // with the same Last-Event-ID in an infinite loop. An empty
1266                    // 200 cleanly terminates the EventSource without delivering
1267                    // events from a different stream.
1268                    tracing::warn!("Resume failed ({e}), returning empty stream");
1269                    return Ok(sse_stream_response(
1270                        futures::stream::empty(),
1271                        None,
1272                        self.config.cancellation_token.child_token(),
1273                    ));
1274                }
1275            }
1276        }
1277        // No Last-Event-ID — create standalone stream
1278        let stream = self
1279            .session_manager
1280            .create_standalone_stream(&session_id)
1281            .await
1282            .map_err(internal_error_response("create standalone stream"))?;
1283        let stream = if let Some(retry) = self.config.sse_retry {
1284            let priming = ServerSseMessage::priming("0", retry);
1285            futures::stream::once(async move { priming })
1286                .chain(stream)
1287                .left_stream()
1288        } else {
1289            stream.right_stream()
1290        };
1291        Ok(sse_stream_response(
1292            stream,
1293            self.config.sse_keep_alive,
1294            self.config.cancellation_token.child_token(),
1295        ))
1296    }
1297
1298    async fn handle_post<B>(&self, request: Request<B>) -> Result<BoxResponse, BoxResponse>
1299    where
1300        B: Body + Send + 'static,
1301        B::Error: Display,
1302    {
1303        // check accept header
1304        if !request
1305            .headers()
1306            .get(http::header::ACCEPT)
1307            .and_then(|header| header.to_str().ok())
1308            .is_some_and(|header| {
1309                header.contains(JSON_MIME_TYPE) && header.contains(EVENT_STREAM_MIME_TYPE)
1310            })
1311        {
1312            return Ok(Response::builder()
1313                .status(http::StatusCode::NOT_ACCEPTABLE)
1314                .body(Full::new(Bytes::from("Not Acceptable: Client must accept both application/json and text/event-stream")).boxed())
1315                .expect("valid response"));
1316        }
1317
1318        // check content type
1319        if !request
1320            .headers()
1321            .get(http::header::CONTENT_TYPE)
1322            .and_then(|header| header.to_str().ok())
1323            .is_some_and(|header| header.starts_with(JSON_MIME_TYPE))
1324        {
1325            return Ok(Response::builder()
1326                .status(http::StatusCode::UNSUPPORTED_MEDIA_TYPE)
1327                .body(
1328                    Full::new(Bytes::from(
1329                        "Unsupported Media Type: Content-Type must be application/json",
1330                    ))
1331                    .boxed(),
1332                )
1333                .expect("valid response"));
1334        }
1335
1336        // json deserialize request body
1337        let (part, body) = request.into_parts();
1338        let mut message = match expect_json(body, self.config.max_request_body_bytes).await {
1339            Ok(message) => message,
1340            Err(response) => return Ok(response),
1341        };
1342
1343        if self.config.stateful_mode {
1344            // do we have a session id?
1345            let session_id = part
1346                .headers
1347                .get(HEADER_SESSION_ID)
1348                .and_then(|v| v.to_str().ok());
1349            if let Some(session_id) = session_id {
1350                let session_id = session_id.to_owned().into();
1351                let has_session = self
1352                    .session_manager
1353                    .has_session(&session_id)
1354                    .await
1355                    .map_err(internal_error_response("check session"))?;
1356                if !has_session {
1357                    // Attempt transparent cross-instance restore from external store.
1358                    let restored = self
1359                        .try_restore_from_store(&session_id, &part)
1360                        .await
1361                        .map_err(internal_error_response("restore session"))?;
1362                    if !restored {
1363                        // MCP spec: server MUST respond with 404 Not Found for terminated/unknown sessions
1364                        return Ok(Response::builder()
1365                            .status(http::StatusCode::NOT_FOUND)
1366                            .body(Full::new(Bytes::from("Not Found: Session not found")).boxed())
1367                            .expect("valid response"));
1368                    }
1369                }
1370
1371                // Validate MCP-Protocol-Version header (per 2025-06-18 spec)
1372                let has_per_request_version = message_has_per_request_protocol_version(&message);
1373                validate_protocol_version_header(&part.headers, has_per_request_version)?;
1374                validate_request_protocol_version_meta(&part.headers, &message)?;
1375                // Validate SEP-2243 standard headers against the body
1376                validate_standard_headers(&part.headers, &message, |name| self.tool_schema(name))?;
1377
1378                // inject request part to extensions
1379                match &mut message {
1380                    ClientJsonRpcMessage::Request(req) => {
1381                        req.request.extensions_mut().insert(part);
1382                    }
1383                    ClientJsonRpcMessage::Notification(not) => {
1384                        not.notification.extensions_mut().insert(part);
1385                    }
1386                    _ => {
1387                        // skip
1388                    }
1389                }
1390
1391                match message {
1392                    ClientJsonRpcMessage::Request(_) => {
1393                        // Priming for request-wise streams is handled by the
1394                        // session layer (SessionManager::create_stream) which
1395                        // has access to the http_request_id for correct event IDs.
1396                        let stream = self
1397                            .session_manager
1398                            .create_stream(&session_id, message)
1399                            .await
1400                            .map_err(internal_error_response("get session"))?;
1401                        Ok(sse_stream_response(
1402                            stream,
1403                            self.config.sse_keep_alive,
1404                            self.config.cancellation_token.child_token(),
1405                        ))
1406                    }
1407                    ClientJsonRpcMessage::Notification(_)
1408                    | ClientJsonRpcMessage::Response(_)
1409                    | ClientJsonRpcMessage::Error(_) => {
1410                        // handle notification
1411                        self.session_manager
1412                            .accept_message(&session_id, message)
1413                            .await
1414                            .map_err(internal_error_response("accept message"))?;
1415                        Ok(accepted_response())
1416                    }
1417                }
1418            } else {
1419                if matches!(
1420                    &message,
1421                    ClientJsonRpcMessage::Request(request)
1422                        if matches!(&request.request, ClientRequest::DiscoverRequest(_))
1423                ) {
1424                    validate_protocol_version_header(
1425                        &part.headers,
1426                        message_has_per_request_protocol_version(&message),
1427                    )?;
1428                    validate_standard_headers(&part.headers, &message, |name| {
1429                        self.tool_schema(name)
1430                    })?;
1431                    validate_request_protocol_version_meta(&part.headers, &message)?;
1432                    let ClientJsonRpcMessage::Request(request) = message else {
1433                        unreachable!("guarded as a request above");
1434                    };
1435                    let service = self
1436                        .get_service()
1437                        .map_err(internal_error_response("get service"))?;
1438                    return self
1439                        .serve_negotiated_request_directly(service, request, part)
1440                        .await;
1441                }
1442                // Capture init params for external store persistence before
1443                // extensions are injected (which would require Clone).
1444                let stored_init_params = match &mut message {
1445                    ClientJsonRpcMessage::Request(req) => {
1446                        let ClientRequest::InitializeRequest(init_req) = &req.request else {
1447                            return Err(unexpected_message_response("initialize request"));
1448                        };
1449                        // Reject mismatched MCP-Protocol-Version header before binding the session to anything.
1450                        validate_header_matches_init_body(
1451                            &part.headers,
1452                            init_req.params.protocol_version.as_str(),
1453                            Some(req.id.clone()),
1454                        )?;
1455                        let stored_init_params = self
1456                            .config
1457                            .session_store
1458                            .as_ref()
1459                            .map(|_| init_req.params.clone());
1460                        // inject request part to extensions
1461                        req.request.extensions_mut().insert(part);
1462                        stored_init_params
1463                    }
1464                    _ => {
1465                        return Err(unexpected_message_response("initialize request"));
1466                    }
1467                };
1468                let service = self
1469                    .get_service()
1470                    .map_err(internal_error_response("get service"))?;
1471                let (session_id, transport) = self
1472                    .session_manager
1473                    .create_session()
1474                    .await
1475                    .map_err(internal_error_response("create session"))?;
1476                // spawn a task to serve the session
1477                Self::spawn_session_worker(
1478                    self.session_manager.clone(),
1479                    session_id.clone(),
1480                    service,
1481                    transport,
1482                    None,
1483                );
1484                // get initialize response
1485                let response = self
1486                    .session_manager
1487                    .initialize_session(&session_id, message)
1488                    .await
1489                    .map_err(internal_error_response("create stream"))?;
1490                // Persist session state to external store after a successful handshake.
1491                if let (Some(store), Some(params)) =
1492                    (&self.config.session_store, stored_init_params)
1493                {
1494                    let state = SessionState {
1495                        initialize_params: params,
1496                    };
1497                    let _ = store
1498                        .store(session_id.as_ref(), &state)
1499                        .await
1500                        .inspect_err(|e| {
1501                            tracing::warn!(
1502                                "Failed to persist session {} to store: {e}",
1503                                session_id
1504                            );
1505                        });
1506                }
1507                let stream =
1508                    futures::stream::once(async move { ServerSseMessage::from_message(response) });
1509                // Prepend priming event if sse_retry configured
1510                let stream = if let Some(retry) = self.config.sse_retry {
1511                    let priming = ServerSseMessage::priming("0", retry);
1512                    futures::stream::once(async move { priming })
1513                        .chain(stream)
1514                        .left_stream()
1515                } else {
1516                    stream.right_stream()
1517                };
1518                let mut response = sse_stream_response(
1519                    stream,
1520                    self.config.sse_keep_alive,
1521                    self.config.cancellation_token.child_token(),
1522                );
1523
1524                response.headers_mut().insert(
1525                    HEADER_SESSION_ID,
1526                    session_id
1527                        .parse()
1528                        .map_err(internal_error_response("create session id header"))?,
1529                );
1530                Ok(response)
1531            }
1532        } else {
1533            // Stateless mode:
1534            // - on initialize: the header (if present) must match `params.protocolVersion`
1535            // - on every other request: the header must name a known version.
1536            let has_per_request_version = message_has_per_request_protocol_version(&message);
1537            match &message {
1538                ClientJsonRpcMessage::Request(req) => {
1539                    if let ClientRequest::InitializeRequest(init_req) = &req.request {
1540                        validate_header_matches_init_body(
1541                            &part.headers,
1542                            init_req.params.protocol_version.as_str(),
1543                            Some(req.id.clone()),
1544                        )?;
1545                    } else {
1546                        validate_protocol_version_header(&part.headers, has_per_request_version)?;
1547                    }
1548                }
1549                _ => {
1550                    validate_protocol_version_header(&part.headers, has_per_request_version)?;
1551                }
1552            }
1553            // Validate SEP-2243 standard headers against the body
1554            validate_standard_headers(&part.headers, &message, |name| self.tool_schema(name))?;
1555            validate_request_protocol_version_meta(&part.headers, &message)?;
1556            let service = self
1557                .get_service()
1558                .map_err(internal_error_response("get service"))?;
1559            match message {
1560                ClientJsonRpcMessage::Request(mut request) => {
1561                    let negotiates_per_request = has_per_request_version
1562                        || matches!(&request.request, ClientRequest::DiscoverRequest(_));
1563                    if negotiates_per_request {
1564                        return self
1565                            .serve_negotiated_request_directly(service, request, part)
1566                            .await;
1567                    }
1568                    // Build a peer_info so context.protocol_version() works inside handlers.
1569                    // serve_directly skips the handshake and receives None by default, making
1570                    // protocol_version() always return None in stateless mode. We reconstruct it:
1571                    // - initialize requests: version comes from the request body params
1572                    // - all other requests: version comes from the MCP-Protocol-Version header
1573                    //   (already validated above; absent header defaults to 2025-03-26)
1574                    let peer_info = Self::peer_info_for_stateless_request(&request, &part.headers);
1575                    request.request.extensions_mut().insert(part);
1576                    let (transport, mut receiver) =
1577                        OneshotTransport::<RoleServer>::new(ClientJsonRpcMessage::Request(request));
1578                    // Give this stateless request its own cancellation token so a
1579                    // client disconnect can cancel the in-flight handler (#857). A
1580                    // stateless request is one-shot (no session, no resumption), so a
1581                    // dropped response is terminal and safe to cancel.
1582                    let request_ct = CancellationToken::new();
1583                    let service =
1584                        serve_directly_with_ct(service, transport, peer_info, request_ct.clone());
1585                    tokio::spawn(async move {
1586                        // on service created
1587                        let _ = service.waiting().await;
1588                    });
1589                    if self.config.json_response {
1590                        // Prefer JSON for a terminal first message. If the handler
1591                        // emits an intermediate notification or request, preserve
1592                        // the complete message sequence by falling back to SSE.
1593                        let cancel = self.config.cancellation_token.child_token();
1594                        // Cancel the handler if the client disconnects while it is
1595                        // still producing its first message (this future is dropped
1596                        // before `receiver.recv()` completes). Disarmed once the
1597                        // handler emits anything, so a normal response is never
1598                        // cancelled.
1599                        let mut disconnect_guard = Some(request_ct.clone().drop_guard());
1600                        let Some(message) = (tokio::select! {
1601                            res = receiver.recv() => {
1602                                if let Some(guard) = disconnect_guard.take() {
1603                                    guard.disarm();
1604                                }
1605                                res
1606                            }
1607                            _ = cancel.cancelled() => None,
1608                        }) else {
1609                            return Err(internal_error_response("empty response")(
1610                                std::io::Error::new(
1611                                    std::io::ErrorKind::UnexpectedEof,
1612                                    "no response message received from handler",
1613                                ),
1614                            ));
1615                        };
1616                        tracing::trace!(?message);
1617                        if matches!(
1618                            message,
1619                            ServerJsonRpcMessage::Response(_) | ServerJsonRpcMessage::Error(_)
1620                        ) {
1621                            let body = serde_json::to_vec(&message).map_err(|e| {
1622                                internal_error_response("serialize json response")(e)
1623                            })?;
1624                            Ok(Response::builder()
1625                                .status(http::StatusCode::OK)
1626                                .header(http::header::CONTENT_TYPE, JSON_MIME_TYPE)
1627                                .body(Full::new(Bytes::from(body)).boxed())
1628                                .expect("valid response"))
1629                        } else {
1630                            // The handler emitted an intermediate message and is still
1631                            // running, so guard the streamed sequence too: dropping it
1632                            // (client disconnect) must cancel the handler.
1633                            let first = futures::stream::once(async move {
1634                                ServerSseMessage::from_message(message)
1635                            });
1636                            let remaining = ReceiverStream::new(receiver).map(|message| {
1637                                tracing::trace!(?message);
1638                                ServerSseMessage::from_message(message)
1639                            });
1640                            Ok(sse_stream_response(
1641                                CancelOnDisconnect::new(first.chain(remaining), request_ct),
1642                                self.config.sse_keep_alive,
1643                                self.config.cancellation_token.child_token(),
1644                            ))
1645                        }
1646                    } else {
1647                        // SSE mode (default): cancel the handler if the client
1648                        // disconnects (drops the response stream) before it completes.
1649                        let stream = ReceiverStream::new(receiver).map(|message| {
1650                            tracing::trace!(?message);
1651                            ServerSseMessage::from_message(message)
1652                        });
1653                        let stream = CancelOnDisconnect::new(stream, request_ct);
1654                        Ok(sse_stream_response(
1655                            stream,
1656                            self.config.sse_keep_alive,
1657                            self.config.cancellation_token.child_token(),
1658                        ))
1659                    }
1660                }
1661                ClientJsonRpcMessage::Notification(_notification) => {
1662                    // ignore
1663                    Ok(accepted_response())
1664                }
1665                ClientJsonRpcMessage::Response(_json_rpc_response) => Ok(accepted_response()),
1666                ClientJsonRpcMessage::Error(_json_rpc_error) => Ok(accepted_response()),
1667            }
1668        }
1669    }
1670
1671    async fn handle_delete<B>(&self, request: Request<B>) -> Result<BoxResponse, BoxResponse>
1672    where
1673        B: Body + Send + 'static,
1674        B::Error: Display,
1675    {
1676        // check session id
1677        let session_id = request
1678            .headers()
1679            .get(HEADER_SESSION_ID)
1680            .and_then(|v| v.to_str().ok())
1681            .map(|s| s.to_owned().into());
1682        let Some(session_id) = session_id else {
1683            // MCP spec: servers that require a session ID SHOULD respond with 400 Bad Request
1684            return Ok(Response::builder()
1685                .status(http::StatusCode::BAD_REQUEST)
1686                .body(Full::new(Bytes::from("Bad Request: Session ID is required")).boxed())
1687                .expect("valid response"));
1688        };
1689        // Validate MCP-Protocol-Version header (per 2025-06-18 spec)
1690        validate_protocol_version_header(request.headers(), false)?;
1691        // close session
1692        self.session_manager
1693            .close_session(&session_id)
1694            .await
1695            .map_err(internal_error_response("close session"))?;
1696        // Remove from external store: a DELETE means the client intentionally
1697        // ends the session, so the store entry is no longer needed.
1698        if let Some(store) = &self.config.session_store {
1699            let _ = store.delete(session_id.as_ref()).await.inspect_err(|e| {
1700                tracing::warn!("Failed to delete session {} from store: {e}", session_id);
1701            });
1702        }
1703        Ok(accepted_response())
1704    }
1705
1706    /// Build a `ClientInfo` (peer_info) for a stateless request so that
1707    /// `context.protocol_version()` returns the correct value inside handlers.
1708    ///
1709    /// `serve_directly` skips the MCP handshake and accepts `peer_info = None`,
1710    /// which means `context.protocol_version()` is always `None` in stateless mode.
1711    /// We reconstruct the protocol version from the available signal per request type:
1712    /// - initialize: version is in the request body params (authoritative)
1713    /// - all other requests: version is in the MCP-Protocol-Version header
1714    ///   (validated before this point; absent header defaults to 2025-03-26)
1715    fn peer_info_for_stateless_request(
1716        request: &crate::model::JsonRpcRequest<ClientRequest>,
1717        headers: &HeaderMap,
1718    ) -> Option<InitializeRequestParams> {
1719        let version = if let ClientRequest::InitializeRequest(ref init) = request.request {
1720            init.params.protocol_version.clone()
1721        } else {
1722            headers
1723                .get(HEADER_MCP_PROTOCOL_VERSION)
1724                .and_then(|v| v.to_str().ok())
1725                .and_then(|s| serde_json::from_value(serde_json::Value::String(s.to_owned())).ok())
1726                .unwrap_or(ProtocolVersion::V_2025_03_26)
1727        };
1728        Some(InitializeRequestParams {
1729            meta: None,
1730            protocol_version: version,
1731            capabilities: ClientCapabilities::default(),
1732            client_info: Implementation::default(),
1733        })
1734    }
1735}
1736
1737pin_project! {
1738    /// Wraps a stateless SSE response stream so a client disconnect cancels the
1739    /// in-flight request.
1740    ///
1741    /// A stateless streamable-HTTP request is one-shot: it has no session and no
1742    /// resumption, so a dropped response stream means the client is gone for
1743    /// good. When the stream is dropped *before* it ends naturally, the request's
1744    /// cancellation token is fired, which stops the dedicated `serve_directly`
1745    /// loop and cancels the handler's `RequestContext::ct` (see #857). If the
1746    /// stream ends naturally (the request completed), the guard is disarmed so
1747    /// normal completion cancels nothing.
1748    struct CancelOnDisconnect<S> {
1749        #[pin]
1750        inner: S,
1751        ct: Option<CancellationToken>,
1752    }
1753    impl<S> PinnedDrop for CancelOnDisconnect<S> {
1754        fn drop(this: Pin<&mut Self>) {
1755            let this = this.project();
1756            if let Some(ct) = this.ct.take() {
1757                ct.cancel();
1758            }
1759        }
1760    }
1761}
1762
1763impl<S> CancelOnDisconnect<S> {
1764    fn new(inner: S, ct: CancellationToken) -> Self {
1765        Self {
1766            inner,
1767            ct: Some(ct),
1768        }
1769    }
1770}
1771
1772impl<S: Stream> Stream for CancelOnDisconnect<S> {
1773    type Item = S::Item;
1774
1775    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1776        let this = self.project();
1777        let polled = this.inner.poll_next(cx);
1778        if let Poll::Ready(None) = &polled {
1779            // Ended naturally: the request completed, so don't cancel on drop.
1780            *this.ct = None;
1781        }
1782        polled
1783    }
1784}