Skip to main content

flymcprust_core/transport/
http.rs

1//! Streamable HTTP transport, stateless (MCP 2026-07-28).
2//!
3//! [`StreamableHttpService`] is a `tower::Service<http::Request<B>>`, so it
4//! can be mounted in any tower-based framework, e.g. with axum:
5//! `Router::new().nest_service("/mcp", StreamableHttpService::new(server))`.
6//! [`Server::serve_http`] and [`serve`] run it on `hyper` directly.
7//!
8//! Request handling, in order:
9//! 1. `Origin` must be allowed (403); `Host` must be allowed (403).
10//! 2. Only `POST` (405); `OPTIONS` answers CORS preflights from allowed origins.
11//! 3. `Content-Type: application/json` (415); `Accept` must allow JSON (406).
12//! 4. At most [`HttpConfig::max_in_flight`] requests are processed at once
13//!    (503 beyond that); the body is read up to the message limit (413)
14//!    within [`HttpConfig::body_timeout`] (408).
15//! 5. `MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name` and recognized
16//!    `Mcp-Param-*` headers must match the body (400, `-32020`).
17//! 6. The message is dispatched; notifications get `202`, unknown methods
18//!    `404`, malformed requests `400`, everything else `200`.
19//!
20//! `Last-Event-ID` is ignored: streams are not resumable. Dropping the
21//! response future (client disconnect) cancels a 2026-07-28 request.
22//!
23//! Legacy clients (2025-11-25, 2025-06-18) open with `initialize` and get a
24//! signed `MCP-Session-Id` that carries what they declared, so any instance
25//! sharing the `request_state_key` serves them without server-side session
26//! state. Their requests skip the 2026-07-28 header checks. When a handler
27//! asks for input, the response becomes an SSE stream carrying the
28//! server-to-client request; the client answers with a POST that is routed
29//! back to the waiting handler. `DELETE` gets `405`: sessions end when their
30//! token expires.
31
32use std::borrow::Cow;
33use std::convert::Infallible;
34use std::future::Future;
35use std::io;
36use std::pin::Pin;
37use std::sync::Arc;
38use std::task::{Context, Poll};
39use std::time::Duration;
40
41use base64::Engine as _;
42use bytes::Bytes;
43use http::header::{self, HeaderMap, HeaderValue};
44use http::{Method, Request, Response, StatusCode};
45use http_body::Body;
46use http_body_util::{BodyExt as _, Full, LengthLimitError, Limited};
47use serde::Deserialize;
48use serde_json::Value;
49use serde_json::value::RawValue;
50use tokio::net::TcpListener;
51use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
52use tokio_util::sync::{CancellationToken, DropGuard};
53
54use super::oauth::Challenge;
55pub use super::oauth::{OAuth, OAuthBuilder, OAuthError, ProtectedResourceMetadata};
56use crate::auth::AuthContext;
57use crate::context::TransportContext;
58use crate::error::{ErrorKind, McpError};
59use crate::hints::{self, Once};
60use crate::jsonrpc::{self, Message, Rejection, Response as RpcResponse};
61use crate::legacy::{self, ChannelPeer, LegacySession, PendingRequests};
62use crate::mirror::ParamKind;
63use crate::notify::NotifyChannel;
64use crate::server::{McpRequest, Server};
65use crate::spec::{RequestId, methods};
66
67/// The HTTP response body: a complete JSON body, or an SSE stream (legacy
68/// clients asked for input).
69pub struct HttpBody(BodyKind);
70
71enum BodyKind {
72    Full(Full<Bytes>),
73    Stream(Pin<Box<dyn Body<Data = Bytes, Error = Infallible> + Send>>),
74}
75
76impl HttpBody {
77    pub(crate) fn full(bytes: impl Into<Bytes>) -> Self {
78        Self(BodyKind::Full(Full::new(bytes.into())))
79    }
80}
81
82impl std::fmt::Debug for HttpBody {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match &self.0 {
85            BodyKind::Full(_) => f.write_str("HttpBody::Full"),
86            BodyKind::Stream(_) => f.write_str("HttpBody::Stream"),
87        }
88    }
89}
90
91impl Body for HttpBody {
92    type Data = Bytes;
93    type Error = Infallible;
94
95    fn poll_frame(
96        self: Pin<&mut Self>,
97        cx: &mut Context<'_>,
98    ) -> Poll<Option<Result<http_body::Frame<Bytes>, Infallible>>> {
99        match &mut self.get_mut().0 {
100            BodyKind::Full(body) => Pin::new(body).poll_frame(cx),
101            BodyKind::Stream(body) => body.as_mut().poll_frame(cx),
102        }
103    }
104
105    fn is_end_stream(&self) -> bool {
106        match &self.0 {
107            BodyKind::Full(body) => body.is_end_stream(),
108            BodyKind::Stream(body) => body.is_end_stream(),
109        }
110    }
111
112    fn size_hint(&self) -> http_body::SizeHint {
113        match &self.0 {
114            BodyKind::Full(body) => body.size_hint(),
115            BodyKind::Stream(body) => body.size_hint(),
116        }
117    }
118}
119
120const MCP_PROTOCOL_VERSION: &str = "mcp-protocol-version";
121const MCP_METHOD: &str = "mcp-method";
122const MCP_NAME: &str = "mcp-name";
123const MCP_PARAM_PREFIX: &str = "mcp-param-";
124const MCP_SESSION_ID: &str = "mcp-session-id";
125const BASE64_PREFIX: &str = "=?base64?";
126const BASE64_SUFFIX: &str = "?=";
127const ALLOWED_REQUEST_HEADERS: &str =
128    "content-type, accept, authorization, mcp-protocol-version, mcp-method, mcp-name";
129
130/// `Origin` values accepted by the transport.
131#[derive(Debug, Clone, PartialEq, Eq)]
132#[non_exhaustive]
133pub enum AllowedOrigins {
134    /// Origins whose host is `localhost`, `127.0.0.1` or `[::1]` (any
135    /// scheme and port). The default.
136    Localhost,
137    /// Exactly these origins (e.g. `https://app.example.com`).
138    List(Vec<String>),
139    /// Any origin. Only safe behind other protections such as
140    /// authentication; it disables DNS-rebinding protection.
141    Any,
142}
143
144/// `Host` values accepted by the transport.
145#[derive(Debug, Clone, PartialEq, Eq)]
146#[non_exhaustive]
147pub enum AllowedHosts {
148    /// `localhost`, `127.0.0.1` or `[::1]`, on any port. The default.
149    Localhost,
150    /// Exactly these host names (ports are ignored).
151    List(Vec<String>),
152    /// Any host.
153    Any,
154}
155
156/// Security settings of the Streamable HTTP transport.
157#[derive(Debug, Clone)]
158pub struct HttpConfig {
159    oauth: Option<OAuth>,
160    origins: AllowedOrigins,
161    hosts: AllowedHosts,
162    header_timeout: Duration,
163    body_timeout: Duration,
164    max_in_flight: usize,
165    max_connections: usize,
166}
167
168impl Default for HttpConfig {
169    fn default() -> Self {
170        Self {
171            oauth: None,
172            origins: AllowedOrigins::Localhost,
173            hosts: AllowedHosts::Localhost,
174            header_timeout: Duration::from_secs(10),
175            body_timeout: Duration::from_secs(30),
176            max_in_flight: 256,
177            max_connections: 1024,
178        }
179    }
180}
181
182impl HttpConfig {
183    /// Secure defaults: localhost origins and hosts only.
184    #[must_use]
185    pub fn new() -> Self {
186        Self::default()
187    }
188
189    /// Requires OAuth 2.1 access tokens on every request (the server acts
190    /// as a resource server). See [`OAuth`].
191    #[must_use]
192    pub fn oauth(mut self, oauth: OAuth) -> Self {
193        self.oauth = Some(oauth);
194        self
195    }
196
197    /// Sets the accepted `Origin` values.
198    #[must_use]
199    pub fn allowed_origins(mut self, origins: AllowedOrigins) -> Self {
200        self.origins = origins;
201        self
202    }
203
204    /// Sets the accepted `Host` values. Public deployments must list their
205    /// own host names (or use [`AllowedHosts::Any`]).
206    #[must_use]
207    pub fn allowed_hosts(mut self, hosts: AllowedHosts) -> Self {
208        self.hosts = hosts;
209        self
210    }
211
212    /// Time allowed, in [`serve`], to receive a request's headers, counted
213    /// from when the connection opens or becomes idle. Slower or idle
214    /// connections are closed (slow-loris protection). Default: 10 s.
215    #[must_use]
216    pub fn header_timeout(mut self, timeout: Duration) -> Self {
217        self.header_timeout = timeout;
218        self
219    }
220
221    /// Time allowed to receive a request body; slower clients get `408`.
222    /// Default: 30 s.
223    #[must_use]
224    pub fn body_timeout(mut self, timeout: Duration) -> Self {
225        self.body_timeout = timeout;
226        self
227    }
228
229    /// Maximum number of requests being read or processed at once, which
230    /// bounds the memory used for request bodies (each up to the message
231    /// limit). Further requests get `503`. Default: 256.
232    #[must_use]
233    pub fn max_in_flight(mut self, requests: usize) -> Self {
234        self.max_in_flight = requests.max(1);
235        self
236    }
237
238    /// Maximum number of open connections in [`serve`]; further connections
239    /// wait in the operating system's accept queue. Default: 1024.
240    #[must_use]
241    pub fn max_connections(mut self, connections: usize) -> Self {
242        self.max_connections = connections.max(1);
243        self
244    }
245}
246
247/// Streamable HTTP as a `tower::Service`.
248#[derive(Clone, Debug)]
249pub struct StreamableHttpService {
250    server: Server,
251    config: Arc<HttpConfig>,
252    in_flight: Arc<Semaphore>,
253    hints: Arc<Hints>,
254    /// Server-to-client requests of legacy sessions awaiting an answer.
255    pending: Arc<PendingRequests>,
256}
257
258/// Which developer hints were already logged.
259#[derive(Debug, Default)]
260struct Hints {
261    origin: Once,
262    host: Once,
263    busy: Once,
264    slow_body: Once,
265    too_large: Once,
266    session: Once,
267}
268
269impl StreamableHttpService {
270    /// Wraps a server with the default (localhost-only) security settings.
271    #[must_use]
272    pub fn new(server: Server) -> Self {
273        Self::with_config(server, HttpConfig::default())
274    }
275
276    /// Wraps a server with explicit security settings.
277    #[must_use]
278    pub fn with_config(server: Server, config: HttpConfig) -> Self {
279        match &config.oauth {
280            None if server.has_scopes() => tracing::warn!(
281                "handlers declare OAuth scopes, but this HTTP service does not require OAuth: \
282                 the scopes are not enforced\n  = help: add HttpConfig::new().oauth(OAuth::\
283                 builder(..)..build()?), or remove the scopes"
284            ),
285            Some(oauth) => {
286                let host = oauth
287                    .resource()
288                    .split_once("://")
289                    .map(|(_, rest)| rest.split(['/', '?']).next().unwrap_or_default())
290                    .unwrap_or_default();
291                if !host_name_allowed(&config.hosts, strip_port(host)) {
292                    tracing::warn!(
293                        "the OAuth resource {} is not on an allowed host, so its requests \
294                         will get HTTP 403\n  = help: list the host with \
295                         HttpConfig::allowed_hosts(AllowedHosts::List(..))",
296                        oauth.resource()
297                    );
298                }
299            }
300            None => {}
301        }
302        Self {
303            server,
304            in_flight: Arc::new(Semaphore::new(config.max_in_flight)),
305            config: Arc::new(config),
306            hints: Arc::default(),
307            pending: Arc::default(),
308        }
309    }
310
311    async fn handle<B>(self, request: Request<B>) -> Response<HttpBody>
312    where
313        B: Body,
314        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
315    {
316        let (mut parts, body) = request.into_parts();
317
318        let cors = match parts.headers.get(header::ORIGIN) {
319            None => None,
320            Some(origin) if origin_allowed(&self.config.origins, origin) => Some(origin.clone()),
321            Some(origin) => {
322                hints::warn_once(
323                    &self.hints.origin,
324                    format_args!(
325                        "rejected a request from Origin {} (HTTP 403)",
326                        hints::client_text(origin.to_str().unwrap_or("<not ASCII>"))
327                    ),
328                    match self.config.origins {
329                        AllowedOrigins::Localhost => {
330                            "only localhost origins are accepted by default (DNS-rebinding \
331                             protection); to allow a web app, list it: \
332                             HttpConfig::new().allowed_origins(AllowedOrigins::List(vec![\
333                             \"https://app.example.com\".into()]))"
334                        }
335                        _ => "add the origin to HttpConfig::allowed_origins if it is legitimate",
336                    },
337                );
338                return rpc_error(
339                    StatusCode::FORBIDDEN,
340                    None,
341                    &McpError::invalid_request("Origin not allowed"),
342                    None,
343                );
344            }
345        };
346        if !host_allowed(&self.config.hosts, &parts) {
347            let host = parts
348                .headers
349                .get(header::HOST)
350                .and_then(|h| h.to_str().ok())
351                .or_else(|| parts.uri.authority().map(http::uri::Authority::as_str))
352                .unwrap_or("<none>");
353            hints::warn_once(
354                &self.hints.host,
355                format_args!(
356                    "rejected a request for Host {} (HTTP 403)",
357                    hints::client_text(host)
358                ),
359                match self.config.hosts {
360                    AllowedHosts::Localhost => {
361                        "only localhost is accepted by default (DNS-rebinding protection); to \
362                         serve other machines, list your host names: \
363                         HttpConfig::new().allowed_hosts(AllowedHosts::List(vec![\
364                         \"mcp.example.com\".into()]))"
365                    }
366                    _ => "add the host name to HttpConfig::allowed_hosts if it is legitimate",
367                },
368            );
369            return rpc_error(
370                StatusCode::FORBIDDEN,
371                None,
372                &McpError::invalid_request("Host not allowed"),
373                None,
374            );
375        }
376
377        match parts.method {
378            Method::POST => {}
379            Method::OPTIONS if cors.is_some() => return preflight(cors, &parts.headers),
380            _ => {
381                let mut response = plain(StatusCode::METHOD_NOT_ALLOWED, cors.as_ref());
382                response
383                    .headers_mut()
384                    .insert(header::ALLOW, HeaderValue::from_static("POST"));
385                return response;
386            }
387        }
388
389        if !is_json_content_type(parts.headers.get(header::CONTENT_TYPE)) {
390            return rpc_error(
391                StatusCode::UNSUPPORTED_MEDIA_TYPE,
392                None,
393                &McpError::invalid_request("Content-Type must be application/json"),
394                cors.as_ref(),
395            );
396        }
397        if !accepts_json(&parts.headers) {
398            return rpc_error(
399                StatusCode::NOT_ACCEPTABLE,
400                None,
401                &McpError::invalid_request("Accept must allow application/json"),
402                cors.as_ref(),
403            );
404        }
405
406        // Bounds the memory held by bodies being read or processed.
407        let Ok(permit) = Arc::clone(&self.in_flight).try_acquire_owned() else {
408            hints::warn_once(
409                &self.hints.busy,
410                format_args!(
411                    "rejected a request: {} requests already in flight (HTTP 503)",
412                    self.config.max_in_flight
413                ),
414                "raise HttpConfig::max_in_flight(..) if the machine has room, or run more \
415                 instances",
416            );
417            let mut response = rpc_error(
418                StatusCode::SERVICE_UNAVAILABLE,
419                None,
420                &McpError::internal_with_message("The server is busy; retry later."),
421                cors.as_ref(),
422            );
423            response
424                .headers_mut()
425                .insert(header::RETRY_AFTER, HeaderValue::from_static("1"));
426            return response;
427        };
428
429        // Authorization comes before the body is read: unauthenticated
430        // clients cannot make the server buffer their messages.
431        let auth = match &self.config.oauth {
432            None => None,
433            Some(oauth) => match oauth.authenticate(&parts.headers).await {
434                Ok(auth) => {
435                    // Handlers see the principal, never the token: they
436                    // cannot pass it through to other services by mistake.
437                    parts.headers.remove(header::AUTHORIZATION);
438                    Some(auth)
439                }
440                Err(challenge) => return challenge_response(oauth, &challenge, cors.as_ref()),
441            },
442        };
443
444        let limits = self.server.limits();
445        let read = Limited::new(body, limits.max_message_bytes).collect();
446        let Ok(read) = tokio::time::timeout(self.config.body_timeout, read).await else {
447            hints::warn_once(
448                &self.hints.slow_body,
449                format_args!(
450                    "a client did not send its request body within {:?} (HTTP 408)",
451                    self.config.body_timeout
452                ),
453                "raise HttpConfig::body_timeout(..) if clients on slow networks are expected",
454            );
455            return rpc_error(
456                StatusCode::REQUEST_TIMEOUT,
457                None,
458                &McpError::invalid_request("Request body not received in time"),
459                cors.as_ref(),
460            );
461        };
462        let bytes = match read {
463            Ok(collected) => collected.to_bytes(),
464            Err(error) => {
465                let (status, message) = if error.downcast_ref::<LengthLimitError>().is_some() {
466                    hints::warn_once(
467                        &self.hints.too_large,
468                        format_args!(
469                            "rejected a request body over the {}-byte message limit (HTTP 413)",
470                            limits.max_message_bytes
471                        ),
472                        "raise ServerBuilder::max_message_bytes(..) if clients legitimately \
473                         send larger arguments",
474                    );
475                    (StatusCode::PAYLOAD_TOO_LARGE, "Message too large")
476                } else {
477                    (StatusCode::BAD_REQUEST, "Could not read the request body")
478                };
479                return rpc_error(
480                    status,
481                    None,
482                    &McpError::invalid_request(message),
483                    cors.as_ref(),
484                );
485            }
486        };
487
488        // Legacy clients: a session token, or a legacy version header.
489        let legacy = match self.legacy_session(&parts.headers, auth.as_ref()) {
490            Ok(legacy) => legacy,
491            Err(response) => return *response,
492        };
493
494        let message = match jsonrpc::parse(&bytes, limits) {
495            Ok(message) => message,
496            Err(Rejection::Error { id, error }) => {
497                return rpc_error(status_for(&error), id, &error, cors.as_ref());
498            }
499            Err(Rejection::UnexpectedResponse) if legacy.is_some() => {
500                // A legacy client answering one of our requests.
501                let owner = session_owner(&parts.headers);
502                let routed = jsonrpc::parse_client_response(&bytes)
503                    .is_some_and(|response| self.pending.complete(response, owner));
504                return if routed {
505                    plain(StatusCode::ACCEPTED, cors.as_ref())
506                } else {
507                    rpc_error(
508                        StatusCode::BAD_REQUEST,
509                        None,
510                        &McpError::invalid_request("The response answers no pending request"),
511                        cors.as_ref(),
512                    )
513                };
514            }
515            Err(Rejection::UnexpectedResponse) => {
516                return rpc_error(
517                    StatusCode::BAD_REQUEST,
518                    None,
519                    &McpError::invalid_request("Clients must not send JSON-RPC responses"),
520                    cors.as_ref(),
521                );
522            }
523        };
524
525        // A body that declares 2026-07-28 in `_meta` follows 2026-07-28 rules
526        // (including header/body agreement), whatever its version header.
527        let legacy = legacy.filter(|_| {
528            parts.headers.contains_key(MCP_SESSION_ID) || !declares_modern_meta(&message)
529        });
530
531        // `initialize` opens a legacy session, unless the request declares
532        // 2026-07-28 (legacy clients never send the version header with
533        // `initialize`): for that protocol the method does not exist.
534        let modern = parts
535            .headers
536            .get(MCP_PROTOCOL_VERSION)
537            .and_then(|v| v.to_str().ok())
538            .is_some_and(|v| self.server.supported_versions().iter().any(|s| s == v));
539        if let Message::Request(request) = &message
540            && request.method == "initialize"
541        {
542            if self.server.legacy_clients_enabled() && !modern {
543                return self.legacy_initialize(request, cors.as_ref(), auth.as_ref());
544            }
545            if modern {
546                return rpc_error(
547                    StatusCode::NOT_FOUND,
548                    Some(request.id.clone()),
549                    &McpError::method_not_found_explained(
550                        "Method not found: `initialize` is not part of MCP 2026-07-28",
551                    ),
552                    cors.as_ref(),
553                );
554            }
555        }
556        if let Some(session) = legacy {
557            let mut transport = TransportContext::new();
558            let owner = session_owner(&parts.headers);
559            transport.insert(parts);
560            if let Some(auth) = auth {
561                transport.insert(auth);
562            }
563            return self
564                .handle_legacy(message, session, owner, transport, permit, cors)
565                .await;
566        }
567
568        // A legacy client's `initialize` carries none of the modern headers;
569        // let the server answer it with an error naming the supported
570        // versions (the spec recommends that diagnostic for legacy clients).
571        let mut wants_notifications = false;
572        if let Message::Request(request) = &message
573            && request.method != "initialize"
574        {
575            match validate_headers(&self.server, &parts.headers, request) {
576                Ok(wants) => wants_notifications = wants,
577                Err(error) => {
578                    return rpc_error(
579                        StatusCode::BAD_REQUEST,
580                        Some(request.id.clone()),
581                        &error,
582                        cors.as_ref(),
583                    );
584                }
585            }
586        }
587
588        let mut transport = TransportContext::new();
589        transport.insert(parts);
590        if let Some(auth) = auth {
591            transport.insert(auth);
592        }
593        if wants_notifications {
594            // The client asked for progress or log messages: they can only
595            // travel on an SSE response, which closing cancels the request.
596            let cancellation = CancellationToken::new();
597            let guard = cancellation.clone().drop_guard();
598            let server = self.server.clone();
599            return self
600                .respond_streaming(permit, cors, Some(guard), move |out| {
601                    let mut transport = transport;
602                    transport.insert(NotifyChannel(out));
603                    Box::pin(async move {
604                        server
605                            .handle(McpRequest {
606                                message,
607                                transport,
608                                cancellation,
609                            })
610                            .await
611                    })
612                })
613                .await;
614        }
615        let response = self
616            .server
617            .handle(McpRequest {
618                message,
619                transport,
620                cancellation: CancellationToken::new(),
621            })
622            .await;
623        self.reply(response, cors.as_ref())
624    }
625
626    /// The HTTP response for a JSON-RPC response (`None`: a notification).
627    /// Scope errors carry an `insufficient_scope` challenge.
628    fn reply(
629        &self,
630        response: Option<RpcResponse>,
631        cors: Option<&HeaderValue>,
632    ) -> Response<HttpBody> {
633        let Some(response) = response else {
634            return plain(StatusCode::ACCEPTED, cors);
635        };
636        let status = response.as_error().map_or(StatusCode::OK, status_for);
637        let challenge = match (&self.config.oauth, response.as_error()) {
638            (Some(oauth), Some(error)) if error.kind() == ErrorKind::Forbidden => oauth
639                .www_authenticate(&Challenge::InsufficientScope(
640                    error.required_scopes().join(" "),
641                )),
642            _ => None,
643        };
644        let mut http = json(status, response.to_vec(), cors);
645        if let Some(challenge) = challenge {
646            http.headers_mut()
647                .insert(header::WWW_AUTHENTICATE, challenge);
648        }
649        http
650    }
651}
652
653impl StreamableHttpService {
654    /// The legacy session of a request, if it is a legacy request. A token
655    /// that fails verification (or expired) gets `404`, which tells the
656    /// client to initialize again. A 2026-07-28 version header always means
657    /// a 2026-07-28 request.
658    #[allow(clippy::result_large_err)]
659    fn legacy_session(
660        &self,
661        headers: &HeaderMap,
662        auth: Option<&AuthContext>,
663    ) -> Result<Option<LegacySession>, Box<Response<HttpBody>>> {
664        if !self.server.legacy_clients_enabled() {
665            return Ok(None);
666        }
667        let version = headers
668            .get(MCP_PROTOCOL_VERSION)
669            .and_then(|v| v.to_str().ok());
670        if version.is_some_and(|v| self.server.supported_versions().iter().any(|s| s == v)) {
671            return Ok(None);
672        }
673        match headers.get(MCP_SESSION_ID).and_then(|v| v.to_str().ok()) {
674            Some(token) => match self
675                .server
676                .open_session(token)
677                // A session opened with one user's token is not another's.
678                .filter(|s| s.principal.as_deref() == auth.map(|a| a.digest.as_str()))
679            {
680                Some(session) => Ok(Some(session)),
681                None => {
682                    hints::warn_once(
683                        &self.hints.session,
684                        format_args!("rejected an unknown or expired MCP-Session-Id (HTTP 404)"),
685                        "the client will initialize again; if this server runs as several \
686                         instances, give all of them the same ServerBuilder::request_state_key(..)",
687                    );
688                    Err(Box::new(rpc_error(
689                        StatusCode::NOT_FOUND,
690                        None,
691                        &McpError::invalid_request("Session not found or expired"),
692                        None,
693                    )))
694                }
695            },
696            None => Ok(version
697                .filter(|v| legacy::is_legacy_version(v))
698                .map(LegacySession::sessionless)),
699        }
700    }
701
702    fn legacy_initialize(
703        &self,
704        request: &jsonrpc::Request,
705        cors: Option<&HeaderValue>,
706        auth: Option<&AuthContext>,
707    ) -> Response<HttpBody> {
708        match self.server.legacy_initialize(request.params.as_deref()) {
709            Ok((result, mut session)) => {
710                session.principal = auth.map(|a| a.digest.clone());
711                let body = RpcResponse::Result {
712                    id: request.id.clone(),
713                    result,
714                }
715                .to_vec();
716                let mut response = json(StatusCode::OK, body, cors);
717                if let Some(token) = self.server.seal_session(&session)
718                    && let Ok(value) = HeaderValue::from_str(&token)
719                {
720                    response.headers_mut().insert(MCP_SESSION_ID, value);
721                }
722                response
723            }
724            Err(error) => rpc_error(status_for(&error), Some(request.id.clone()), &error, cors),
725        }
726    }
727
728    /// Handles a legacy message. A request answers with JSON, or with an SSE
729    /// stream as soon as the handler asks the client for input.
730    async fn handle_legacy(
731        &self,
732        message: Message,
733        session: LegacySession,
734        owner: u64,
735        transport: TransportContext,
736        permit: OwnedSemaphorePermit,
737        cors: Option<HeaderValue>,
738    ) -> Response<HttpBody> {
739        let Message::Request(request) = message else {
740            // Notifications (`initialized`, `cancelled`) need no answer.
741            return plain(StatusCode::ACCEPTED, cors.as_ref());
742        };
743        let pending = Arc::clone(&self.pending);
744        let timeout = self.server.limits().tool_timeout;
745        let server = self.server.clone();
746        // 2025-11-25: a disconnection is not a cancellation.
747        self.respond_streaming(permit, cors, None, move |out| {
748            let mut transport = transport;
749            transport.insert(NotifyChannel(out.clone()));
750            let peer = ChannelPeer {
751                pending,
752                owner,
753                out,
754                timeout,
755            };
756            Box::pin(async move {
757                server
758                    .handle_legacy(
759                        request,
760                        &session,
761                        transport,
762                        CancellationToken::new(),
763                        &peer,
764                    )
765                    .await
766            })
767        })
768        .await
769    }
770
771    /// Runs a request whose handler may emit messages before its response
772    /// (notifications, or requests to a legacy client). The response is
773    /// plain JSON if the handler finishes first, and an SSE stream from the
774    /// first message on. `cancel` is cancelled when the response is dropped
775    /// (the client closed the stream).
776    async fn respond_streaming<F>(
777        &self,
778        permit: OwnedSemaphorePermit,
779        cors: Option<HeaderValue>,
780        cancel: Option<DropGuard>,
781        run: F,
782    ) -> Response<HttpBody>
783    where
784        F: FnOnce(
785            mpsc::Sender<Vec<u8>>,
786        ) -> Pin<Box<dyn Future<Output = Option<RpcResponse>> + Send>>,
787    {
788        let (out, mut outgoing) = mpsc::channel::<Vec<u8>>(STREAM_BUFFER);
789        let (done, mut finished) = oneshot::channel::<Option<RpcResponse>>();
790        let task = run(out);
791        // The request counts against the in-flight limit until both the
792        // handler and the response stream are done: a legacy handler keeps
793        // running after its client disconnects.
794        let permit = Arc::new(permit);
795        let task_permit = Arc::clone(&permit);
796        tokio::spawn(async move {
797            let _ = done.send(task.await);
798            drop(task_permit);
799        });
800
801        let reply = |response: Option<RpcResponse>| self.reply(response, cors.as_ref());
802        let first = tokio::select! {
803            biased;
804            first = outgoing.recv() => first,
805            response = &mut finished => return reply(response.ok().flatten()),
806        };
807        let Some(first) = first else {
808            return reply(finished.await.ok().flatten());
809        };
810        let state = SseState {
811            queue: std::collections::VecDeque::from([first]),
812            outgoing,
813            finished: Some(finished),
814            _permit: permit,
815            _cancel: cancel,
816        };
817        let stream = futures_util::stream::unfold(state, |mut state| async move {
818            loop {
819                if let Some(message) = state.queue.pop_front() {
820                    return Some((Ok(sse_event(&message)), state));
821                }
822                let finished = state.finished.as_mut()?;
823                tokio::select! {
824                    biased;
825                    message = state.outgoing.recv() => match message {
826                        Some(message) => state.queue.push_back(message),
827                        None => {
828                            // Every sender is gone: the response follows.
829                            let response = state.finished.take()?.await.ok().flatten()?;
830                            state.queue.push_back(response.to_vec());
831                        }
832                    },
833                    response = finished => {
834                        // Messages sent before the response come first.
835                        state.finished = None;
836                        while let Ok(message) = state.outgoing.try_recv() {
837                            state.queue.push_back(message);
838                        }
839                        if let Ok(Some(response)) = response {
840                            state.queue.push_back(response.to_vec());
841                        }
842                    }
843                }
844            }
845        });
846        let body = http_body_util::StreamBody::new(stream);
847        let mut response = Response::new(HttpBody(BodyKind::Stream(Box::pin(body))));
848        let headers = response.headers_mut();
849        headers.insert(
850            header::CONTENT_TYPE,
851            HeaderValue::from_static("text/event-stream"),
852        );
853        headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
854        // Reverse proxies must not buffer the stream (spec recommendation).
855        headers.insert("x-accel-buffering", HeaderValue::from_static("no"));
856        headers.insert(
857            header::X_CONTENT_TYPE_OPTIONS,
858            HeaderValue::from_static("nosniff"),
859        );
860        with_cors(&mut response, cors.as_ref());
861        response
862    }
863}
864
865/// Whether a request's `_meta` carries the 2026-07-28 protocol version.
866fn declares_modern_meta(message: &Message) -> bool {
867    let Message::Request(request) = message else {
868        return false;
869    };
870    request.params.as_deref().is_some_and(|params| {
871        serde_json::from_str::<PeekParams<'_>>(params.get())
872            .ok()
873            .and_then(|p| p.meta)
874            .is_some_and(|meta| meta.protocol_version.is_some())
875    })
876}
877
878/// Messages buffered between a handler and its SSE response.
879const STREAM_BUFFER: usize = 32;
880
881/// State of an SSE response.
882struct SseState {
883    /// Messages ready to send, in order.
884    queue: std::collections::VecDeque<Vec<u8>>,
885    outgoing: mpsc::Receiver<Vec<u8>>,
886    /// The handler's response; `None` once received.
887    finished: Option<oneshot::Receiver<Option<RpcResponse>>>,
888    /// Counts against the in-flight limit until the stream ends.
889    _permit: Arc<OwnedSemaphorePermit>,
890    /// Cancels the request if the stream is dropped (client disconnect).
891    _cancel: Option<DropGuard>,
892}
893
894fn sse_event(message: &[u8]) -> http_body::Frame<Bytes> {
895    let mut event = Vec::with_capacity(message.len() + 24);
896    event.extend_from_slice(b"event: message\ndata: ");
897    event.extend_from_slice(message);
898    event.extend_from_slice(b"\n\n");
899    http_body::Frame::data(Bytes::from(event))
900}
901
902/// Binds server-to-client requests to the session that caused them, so one
903/// client cannot answer another's.
904fn session_owner(headers: &HeaderMap) -> u64 {
905    use std::hash::{BuildHasher as _, BuildHasherDefault, DefaultHasher};
906    headers.get(MCP_SESSION_ID).map_or(0, |token| {
907        BuildHasherDefault::<DefaultHasher>::default().hash_one(token.as_bytes())
908    })
909}
910
911impl<B> tower_service::Service<Request<B>> for StreamableHttpService
912where
913    B: Body + Send + 'static,
914    B::Data: Send,
915    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
916{
917    type Response = Response<HttpBody>;
918    type Error = Infallible;
919    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Infallible>> + Send>>;
920
921    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
922        Poll::Ready(Ok(()))
923    }
924
925    fn call(&mut self, request: Request<B>) -> Self::Future {
926        let service = self.clone();
927        Box::pin(async move { Ok(service.handle(request).await) })
928    }
929}
930
931impl Server {
932    /// Serves Streamable HTTP at path `/mcp` with the default security
933    /// settings (localhost origins and hosts only) until the process stops.
934    ///
935    /// Bind to a loopback address (e.g. `127.0.0.1:8000`) unless the server
936    /// is meant to be reachable from other machines.
937    ///
938    /// # Errors
939    ///
940    /// Returns an error when the address cannot be bound.
941    pub async fn serve_http(
942        self,
943        addr: impl tokio::net::ToSocketAddrs + Clone + std::fmt::Debug,
944    ) -> io::Result<()> {
945        let listener = TcpListener::bind(addr.clone()).await.map_err(|error| {
946            let help = match error.kind() {
947                io::ErrorKind::AddrInUse => {
948                    "another program (or another copy of this server) is \
949                     already using this port; stop it or choose another port"
950                }
951                io::ErrorKind::PermissionDenied => {
952                    "ports below 1024 usually need elevated \
953                     privileges; choose a port such as 8000"
954                }
955                _ => "check the address, for example `127.0.0.1:8000`",
956            };
957            io::Error::new(
958                error.kind(),
959                format!("could not listen on {addr:?}: {error}\n  = help: {help}"),
960            )
961        })?;
962        serve(
963            listener,
964            StreamableHttpService::new(self),
965            "/mcp",
966            std::future::pending(),
967        )
968        .await
969    }
970}
971
972/// Serves `service` at `path` on an already-bound listener until `shutdown`
973/// completes, then waits up to the tool timeout for open connections.
974///
975/// # Errors
976///
977/// Returns an error when accepting connections fails.
978pub async fn serve(
979    listener: TcpListener,
980    service: StreamableHttpService,
981    path: &str,
982    shutdown: impl Future<Output = ()>,
983) -> io::Result<()> {
984    use hyper::server::conn::http1;
985    use hyper_util::rt::{TokioIo, TokioTimer};
986    use hyper_util::server::graceful::GracefulShutdown;
987
988    if let Ok(addr) = listener.local_addr()
989        && !addr.ip().is_loopback()
990        && service.config.hosts == AllowedHosts::Localhost
991    {
992        tracing::warn!(
993            "listening on {addr}, but only requests addressed to localhost are accepted\n  \
994             = help: other machines will get HTTP 403; list the host names they use with \
995             HttpConfig::allowed_hosts and serve with StreamableHttpService::with_config, or \
996             bind 127.0.0.1"
997        );
998    }
999
1000    let path: Arc<str> = Arc::from(path);
1001    let metadata = service.config.oauth.as_ref().map(|oauth| {
1002        (
1003            Arc::<[String]>::from(oauth.metadata_paths()),
1004            oauth.metadata_service(),
1005        )
1006    });
1007    let grace = service.server.limits().tool_timeout;
1008    let connections = Arc::new(Semaphore::new(service.config.max_connections));
1009    // HTTP/1.1 only: its header timeout runs from the moment a connection
1010    // opens, so idle connections cannot hold slots. (Automatic HTTP/2
1011    // detection waits for the first bytes without any deadline.) Mount
1012    // `StreamableHttpService` in another server for HTTP/2.
1013    let mut builder = http1::Builder::new();
1014    builder
1015        .timer(TokioTimer::new())
1016        .header_read_timeout(service.config.header_timeout);
1017    let graceful = GracefulShutdown::new();
1018    tokio::pin!(shutdown);
1019
1020    loop {
1021        // At capacity, stop accepting; new connections wait in the backlog.
1022        let permit = tokio::select! {
1023            permit = Arc::clone(&connections).acquire_owned() => match permit {
1024                Ok(permit) => permit,
1025                Err(_closed) => break,
1026            },
1027            () = &mut shutdown => break,
1028        };
1029        let (stream, peer) = tokio::select! {
1030            accepted = listener.accept() => match accepted {
1031                Ok(accepted) => accepted,
1032                Err(error) => {
1033                    // Transient errors (e.g. too many open files) must not
1034                    // stop the server.
1035                    tracing::warn!(%error, "failed to accept a connection");
1036                    tokio::time::sleep(Duration::from_millis(50)).await;
1037                    continue;
1038                }
1039            },
1040            () = &mut shutdown => break,
1041        };
1042        let service = service.clone();
1043        let path = Arc::clone(&path);
1044        let metadata = metadata.clone();
1045        let hyper_service =
1046            hyper::service::service_fn(move |request: Request<hyper::body::Incoming>| {
1047                let mut service = service.clone();
1048                let path = Arc::clone(&path);
1049                let metadata = metadata.clone();
1050                async move {
1051                    let requested = request.uri().path();
1052                    if requested == &*path {
1053                        return tower_service::Service::call(&mut service, request).await;
1054                    }
1055                    match &metadata {
1056                        Some((paths, metadata)) if paths.iter().any(|p| p == requested) => {
1057                            Ok(metadata.respond(request.method()))
1058                        }
1059                        _ => Ok(plain(StatusCode::NOT_FOUND, None)),
1060                    }
1061                }
1062            });
1063        let connection = builder.serve_connection(TokioIo::new(stream), hyper_service);
1064        let connection = graceful.watch(connection);
1065        tokio::spawn(async move {
1066            if let Err(error) = connection.await {
1067                tracing::debug!(%peer, %error, "connection closed with an error");
1068            }
1069            drop(permit);
1070        });
1071    }
1072
1073    if tokio::time::timeout(grace, graceful.shutdown())
1074        .await
1075        .is_err()
1076    {
1077        tracing::warn!("open connections did not close before the shutdown deadline");
1078    }
1079    Ok(())
1080}
1081
1082#[derive(Deserialize)]
1083struct PeekParams<'a> {
1084    #[serde(rename = "_meta", borrow, default)]
1085    meta: Option<PeekMeta<'a>>,
1086    #[serde(borrow, default)]
1087    name: Option<Cow<'a, str>>,
1088    #[serde(borrow, default)]
1089    uri: Option<Cow<'a, str>>,
1090    #[serde(borrow, default)]
1091    arguments: Option<&'a RawValue>,
1092}
1093
1094#[derive(Deserialize)]
1095struct PeekMeta<'a> {
1096    #[serde(rename = "io.modelcontextprotocol/protocolVersion", borrow, default)]
1097    protocol_version: Option<Cow<'a, str>>,
1098    #[serde(rename = "progressToken", borrow, default)]
1099    progress_token: Option<&'a RawValue>,
1100    #[serde(rename = "io.modelcontextprotocol/logLevel", borrow, default)]
1101    log_level: Option<&'a RawValue>,
1102}
1103
1104/// Checks the mirrored request headers against the body (SEP-2243). Returns
1105/// whether the request asks for notifications (a progress token or a log
1106/// level), which need an SSE response.
1107fn validate_headers(
1108    server: &Server,
1109    headers: &HeaderMap,
1110    request: &jsonrpc::Request,
1111) -> Result<bool, McpError> {
1112    let version = single_header(headers, MCP_PROTOCOL_VERSION)?
1113        .ok_or_else(|| McpError::header_mismatch("Missing MCP-Protocol-Version header"))?;
1114    let method = single_header(headers, MCP_METHOD)?
1115        .ok_or_else(|| McpError::header_mismatch("Missing Mcp-Method header"))?;
1116    if method != request.method {
1117        return Err(McpError::header_mismatch(
1118            "Mcp-Method header does not match the request method",
1119        ));
1120    }
1121
1122    // When the params cannot be read, the server reports that itself.
1123    let Ok(params) = serde_json::from_str::<PeekParams<'_>>(
1124        request.params.as_deref().map_or("{}", RawValue::get),
1125    ) else {
1126        return Ok(false);
1127    };
1128    let wants_notifications = params
1129        .meta
1130        .as_ref()
1131        .is_some_and(|m| m.progress_token.is_some() || m.log_level.is_some());
1132
1133    if let Some(body_version) = params
1134        .meta
1135        .as_ref()
1136        .and_then(|m| m.protocol_version.as_deref())
1137        && body_version != version
1138    {
1139        return Err(McpError::header_mismatch(
1140            "MCP-Protocol-Version header does not match `_meta`",
1141        ));
1142    }
1143
1144    let name_source = match request.method.as_str() {
1145        methods::TOOLS_CALL | methods::PROMPTS_GET => Some(params.name.as_deref()),
1146        methods::RESOURCES_READ => Some(params.uri.as_deref()),
1147        _ => None,
1148    };
1149    if let Some(body_name) = name_source {
1150        let header_name = single_header(headers, MCP_NAME)?
1151            .ok_or_else(|| McpError::header_mismatch("Missing Mcp-Name header"))?;
1152        if Some(decode_header_value(header_name)?.as_ref()) != body_name {
1153            return Err(McpError::header_mismatch(
1154                "Mcp-Name header does not match the request body",
1155            ));
1156        }
1157    }
1158
1159    if request.method == methods::TOOLS_CALL
1160        && let Some(name) = params.name.as_deref()
1161    {
1162        validate_param_headers(server, headers, name, params.arguments)?;
1163    }
1164    Ok(wants_notifications)
1165}
1166
1167fn validate_param_headers(
1168    server: &Server,
1169    headers: &HeaderMap,
1170    tool: &str,
1171    arguments: Option<&RawValue>,
1172) -> Result<(), McpError> {
1173    let Some(mirrored) = server.mirrored_params(tool).filter(|m| !m.is_empty()) else {
1174        return Ok(());
1175    };
1176    let arguments: Value = match arguments {
1177        Some(raw) => serde_json::from_str(raw.get()).unwrap_or(Value::Null),
1178        None => Value::Null,
1179    };
1180    for param in mirrored {
1181        let header_name = format!("{MCP_PARAM_PREFIX}{}", param.header().to_ascii_lowercase());
1182        let header = single_header(headers, &header_name)?;
1183        let value = param.value_in(&arguments).filter(|v| !v.is_null());
1184        let matches = match (value, header) {
1185            (None, None) => true,
1186            (None, Some(_)) | (Some(_), None) => false,
1187            (Some(value), Some(header)) => {
1188                let header = decode_header_value(header)?;
1189                match param.kind() {
1190                    ParamKind::String => value.as_str() == Some(header.as_ref()),
1191                    ParamKind::Boolean => {
1192                        value.as_bool().map(|b| if b { "true" } else { "false" })
1193                            == Some(header.as_ref())
1194                    }
1195                    ParamKind::Integer => integers_equal(value, &header),
1196                }
1197            }
1198        };
1199        if !matches {
1200            return Err(McpError::header_mismatch(format!(
1201                "Mcp-Param-{} header does not match the request body",
1202                param.header()
1203            )));
1204        }
1205    }
1206    Ok(())
1207}
1208
1209/// Compares a JSON integer with a header value exactly (no floating-point
1210/// rounding). `42.0` is accepted for `42` within the exactly representable
1211/// range of `f64`.
1212fn integers_equal(value: &Value, header: &str) -> bool {
1213    const EXACT_F64: f64 = 9_007_199_254_740_992.0; // 2^53
1214    let as_integer = |text: &str, float: Option<f64>| -> Option<i128> {
1215        text.parse::<i128>().ok().or_else(|| {
1216            float
1217                .filter(|f| f.fract() == 0.0 && f.abs() <= EXACT_F64)
1218                // The guard above keeps the value within i128 exactly.
1219                .map(|f| f as i128)
1220        })
1221    };
1222    let body = match value {
1223        Value::Number(n) => as_integer(&n.to_string(), n.as_f64()),
1224        _ => None,
1225    };
1226    let header = as_integer(header, header.parse::<f64>().ok());
1227    matches!((body, header), (Some(a), Some(b)) if a == b)
1228}
1229
1230/// Returns the single value of a header; duplicates and non-visible-ASCII
1231/// values are rejected.
1232fn single_header<'h>(headers: &'h HeaderMap, name: &str) -> Result<Option<&'h str>, McpError> {
1233    let mut values = headers.get_all(name).iter();
1234    let Some(first) = values.next() else {
1235        return Ok(None);
1236    };
1237    if values.next().is_some() {
1238        return Err(McpError::header_mismatch("Duplicate MCP header"));
1239    }
1240    first
1241        .to_str()
1242        .map(Some)
1243        .map_err(|_| McpError::header_mismatch("MCP header contains invalid characters"))
1244}
1245
1246/// Decodes the `=?base64?…?=` sentinel encoding.
1247fn decode_header_value(value: &str) -> Result<Cow<'_, str>, McpError> {
1248    match value
1249        .strip_prefix(BASE64_PREFIX)
1250        .and_then(|v| v.strip_suffix(BASE64_SUFFIX))
1251    {
1252        None => Ok(Cow::Borrowed(value)),
1253        Some(encoded) => base64::engine::general_purpose::STANDARD
1254            .decode(encoded)
1255            .ok()
1256            .and_then(|bytes| String::from_utf8(bytes).ok())
1257            .map(Cow::Owned)
1258            .ok_or_else(|| McpError::header_mismatch("Invalid base64-encoded MCP header")),
1259    }
1260}
1261
1262fn status_for(error: &McpError) -> StatusCode {
1263    match error.kind() {
1264        ErrorKind::BadRequest => StatusCode::BAD_REQUEST,
1265        ErrorKind::MethodNotFound => StatusCode::NOT_FOUND,
1266        ErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
1267        ErrorKind::Forbidden => StatusCode::FORBIDDEN,
1268        _ => StatusCode::OK,
1269    }
1270}
1271
1272fn origin_allowed(allowed: &AllowedOrigins, origin: &HeaderValue) -> bool {
1273    let Ok(origin) = origin.to_str() else {
1274        return false;
1275    };
1276    match allowed {
1277        AllowedOrigins::Any => true,
1278        AllowedOrigins::List(list) => list.iter().any(|o| o.eq_ignore_ascii_case(origin)),
1279        AllowedOrigins::Localhost => {
1280            let Some((scheme, rest)) = origin.split_once("://") else {
1281                return false;
1282            };
1283            matches!(scheme, "http" | "https") && is_localhost(strip_port(rest))
1284        }
1285    }
1286}
1287
1288fn host_allowed(allowed: &AllowedHosts, parts: &http::request::Parts) -> bool {
1289    // RFC 9112 §3.2: a request with several `Host` headers is invalid. Only
1290    // the first would be checked, while an intermediary might use another.
1291    if parts.headers.get_all(header::HOST).iter().nth(1).is_some() {
1292        return false;
1293    }
1294    if *allowed == AllowedHosts::Any {
1295        return true;
1296    }
1297    // HTTP/1 sends `Host`; HTTP/2 carries the host in the URI authority.
1298    let host = parts
1299        .headers
1300        .get(header::HOST)
1301        .and_then(|h| h.to_str().ok())
1302        .or_else(|| parts.uri.authority().map(http::uri::Authority::as_str));
1303    let Some(host) = host else {
1304        return false;
1305    };
1306    host_name_allowed(allowed, strip_port(host))
1307}
1308
1309/// Whether a host name (without port) is allowed.
1310fn host_name_allowed(allowed: &AllowedHosts, host: &str) -> bool {
1311    match allowed {
1312        AllowedHosts::Any => true,
1313        AllowedHosts::List(list) => list.iter().any(|h| h.eq_ignore_ascii_case(host)),
1314        AllowedHosts::Localhost => is_localhost(host),
1315    }
1316}
1317
1318pub(crate) fn strip_port(authority: &str) -> &str {
1319    if authority.starts_with('[') {
1320        // `[::1]:8080` → `[::1]`
1321        return authority
1322            .find(']')
1323            .and_then(|end| authority.get(..=end))
1324            .unwrap_or(authority);
1325    }
1326    authority.split(':').next().unwrap_or(authority)
1327}
1328
1329pub(crate) fn is_localhost(host: &str) -> bool {
1330    host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1" || host == "[::1]"
1331}
1332
1333fn is_json_content_type(value: Option<&HeaderValue>) -> bool {
1334    value
1335        .and_then(|v| v.to_str().ok())
1336        .and_then(|v| v.split(';').next())
1337        .is_some_and(|mime| mime.trim().eq_ignore_ascii_case("application/json"))
1338}
1339
1340/// The response is JSON, so `Accept` (when present) must allow it.
1341fn accepts_json(headers: &HeaderMap) -> bool {
1342    let mut values = headers.get_all(header::ACCEPT).iter().peekable();
1343    if values.peek().is_none() {
1344        return true;
1345    }
1346    values
1347        .filter_map(|v| v.to_str().ok())
1348        .flat_map(|v| v.split(','))
1349        .filter_map(|item| item.split(';').next())
1350        .map(str::trim)
1351        .any(|mime| {
1352            mime.eq_ignore_ascii_case("application/json")
1353                || mime.eq_ignore_ascii_case("application/*")
1354                || mime == "*/*"
1355        })
1356}
1357
1358fn preflight(origin: Option<HeaderValue>, request_headers: &HeaderMap) -> Response<HttpBody> {
1359    let mut response = plain(StatusCode::NO_CONTENT, origin.as_ref());
1360    let headers = response.headers_mut();
1361    headers.insert(
1362        header::ACCESS_CONTROL_ALLOW_METHODS,
1363        HeaderValue::from_static("POST"),
1364    );
1365    let allowed = request_headers
1366        .get(header::ACCESS_CONTROL_REQUEST_HEADERS)
1367        .cloned()
1368        .unwrap_or_else(|| HeaderValue::from_static(ALLOWED_REQUEST_HEADERS));
1369    headers.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, allowed);
1370    headers.insert(
1371        header::ACCESS_CONTROL_MAX_AGE,
1372        HeaderValue::from_static("600"),
1373    );
1374    response
1375}
1376
1377fn with_cors(response: &mut Response<HttpBody>, origin: Option<&HeaderValue>) {
1378    if let Some(origin) = origin {
1379        let headers = response.headers_mut();
1380        headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone());
1381        headers.insert(header::VARY, HeaderValue::from_static("Origin"));
1382        // Browser clients must read OAuth challenges and legacy sessions.
1383        headers.insert(
1384            header::ACCESS_CONTROL_EXPOSE_HEADERS,
1385            HeaderValue::from_static("www-authenticate, mcp-session-id"),
1386        );
1387    }
1388}
1389
1390/// A `401`/`403`/`400`/`503` answer to a request that failed authorization.
1391fn challenge_response(
1392    oauth: &OAuth,
1393    challenge: &Challenge,
1394    cors: Option<&HeaderValue>,
1395) -> Response<HttpBody> {
1396    let error = match challenge {
1397        Challenge::Unavailable => McpError::internal_with_message(challenge.message()),
1398        _ => McpError::invalid_request(challenge.message()),
1399    };
1400    let mut response = rpc_error(challenge.status(), None, &error, cors);
1401    if let Some(value) = oauth.www_authenticate(challenge) {
1402        response
1403            .headers_mut()
1404            .insert(header::WWW_AUTHENTICATE, value);
1405    }
1406    if *challenge == Challenge::Unavailable {
1407        response
1408            .headers_mut()
1409            .insert(header::RETRY_AFTER, HeaderValue::from_static("5"));
1410    }
1411    response
1412}
1413
1414fn plain(status: StatusCode, cors: Option<&HeaderValue>) -> Response<HttpBody> {
1415    let mut response = Response::new(HttpBody::full(Bytes::new()));
1416    *response.status_mut() = status;
1417    with_cors(&mut response, cors);
1418    response
1419}
1420
1421fn json(status: StatusCode, body: Vec<u8>, cors: Option<&HeaderValue>) -> Response<HttpBody> {
1422    let mut response = Response::new(HttpBody::full(body));
1423    *response.status_mut() = status;
1424    response.headers_mut().insert(
1425        header::CONTENT_TYPE,
1426        HeaderValue::from_static("application/json"),
1427    );
1428    response.headers_mut().insert(
1429        header::X_CONTENT_TYPE_OPTIONS,
1430        HeaderValue::from_static("nosniff"),
1431    );
1432    with_cors(&mut response, cors);
1433    response
1434}
1435
1436fn rpc_error(
1437    status: StatusCode,
1438    id: Option<RequestId>,
1439    error: &McpError,
1440    cors: Option<&HeaderValue>,
1441) -> Response<HttpBody> {
1442    json(status, RpcResponse::error(id, error.clone()).to_vec(), cors)
1443}
1444
1445#[cfg(test)]
1446mod tests {
1447    use super::*;
1448
1449    #[test]
1450    fn decodes_base64_sentinel() {
1451        assert_eq!(decode_header_value("us-west1").unwrap(), "us-west1");
1452        assert_eq!(
1453            decode_header_value("=?base64?SGVsbG8sIOS4lueVjA==?=").unwrap(),
1454            "Hello, 世界"
1455        );
1456        assert_eq!(
1457            decode_header_value("=?base64?PT9iYXNlNjQ/bGl0ZXJhbD89?=").unwrap(),
1458            "=?base64?literal?="
1459        );
1460        assert!(decode_header_value("=?base64?***?=").is_err());
1461    }
1462
1463    #[test]
1464    fn origin_rules() {
1465        let localhost = AllowedOrigins::Localhost;
1466        let ok = |o: &'static str| origin_allowed(&localhost, &HeaderValue::from_static(o));
1467        assert!(ok("http://localhost:6274"));
1468        assert!(ok("http://127.0.0.1"));
1469        assert!(ok("https://[::1]:8443"));
1470        assert!(!ok("http://localhost.evil.com"));
1471        assert!(!ok("http://evil.com"));
1472        assert!(!ok("null"));
1473    }
1474
1475    fn parts(host: Option<&'static str>, uri: &str) -> http::request::Parts {
1476        let mut builder = Request::builder().uri(uri);
1477        if let Some(host) = host {
1478            builder = builder.header(header::HOST, host);
1479        }
1480        builder.body(()).unwrap().into_parts().0
1481    }
1482
1483    #[test]
1484    fn host_rules() {
1485        let local = parts(Some("127.0.0.1:8000"), "/mcp");
1486        assert!(host_allowed(&AllowedHosts::Localhost, &local));
1487        let evil = parts(Some("attacker.example"), "/mcp");
1488        assert!(!host_allowed(&AllowedHosts::Localhost, &evil));
1489        assert!(host_allowed(
1490            &AllowedHosts::List(vec!["attacker.example".into()]),
1491            &evil
1492        ));
1493        // HTTP/2: no Host header, authority in the URI.
1494        let h2 = parts(None, "http://localhost:8000/mcp");
1495        assert!(host_allowed(&AllowedHosts::Localhost, &h2));
1496        assert!(!host_allowed(
1497            &AllowedHosts::Localhost,
1498            &parts(None, "/mcp")
1499        ));
1500    }
1501
1502    #[test]
1503    fn accept_rules() {
1504        let mut headers = HeaderMap::new();
1505        assert!(accepts_json(&headers));
1506        headers.insert(
1507            header::ACCEPT,
1508            HeaderValue::from_static("application/json, text/event-stream"),
1509        );
1510        assert!(accepts_json(&headers));
1511        headers.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
1512        assert!(!accepts_json(&headers));
1513    }
1514}