1use 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
67pub 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#[derive(Debug, Clone, PartialEq, Eq)]
132#[non_exhaustive]
133pub enum AllowedOrigins {
134 Localhost,
137 List(Vec<String>),
139 Any,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146#[non_exhaustive]
147pub enum AllowedHosts {
148 Localhost,
150 List(Vec<String>),
152 Any,
154}
155
156#[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 #[must_use]
185 pub fn new() -> Self {
186 Self::default()
187 }
188
189 #[must_use]
192 pub fn oauth(mut self, oauth: OAuth) -> Self {
193 self.oauth = Some(oauth);
194 self
195 }
196
197 #[must_use]
199 pub fn allowed_origins(mut self, origins: AllowedOrigins) -> Self {
200 self.origins = origins;
201 self
202 }
203
204 #[must_use]
207 pub fn allowed_hosts(mut self, hosts: AllowedHosts) -> Self {
208 self.hosts = hosts;
209 self
210 }
211
212 #[must_use]
216 pub fn header_timeout(mut self, timeout: Duration) -> Self {
217 self.header_timeout = timeout;
218 self
219 }
220
221 #[must_use]
224 pub fn body_timeout(mut self, timeout: Duration) -> Self {
225 self.body_timeout = timeout;
226 self
227 }
228
229 #[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 #[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#[derive(Clone, Debug)]
249pub struct StreamableHttpService {
250 server: Server,
251 config: Arc<HttpConfig>,
252 in_flight: Arc<Semaphore>,
253 hints: Arc<Hints>,
254 pending: Arc<PendingRequests>,
256}
257
258#[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 #[must_use]
272 pub fn new(server: Server) -> Self {
273 Self::with_config(server, HttpConfig::default())
274 }
275
276 #[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 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 let auth = match &self.config.oauth {
432 None => None,
433 Some(oauth) => match oauth.authenticate(&parts.headers).await {
434 Ok(auth) => {
435 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 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 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 let legacy = legacy.filter(|_| {
528 parts.headers.contains_key(MCP_SESSION_ID) || !declares_modern_meta(&message)
529 });
530
531 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 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 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 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 #[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 .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 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 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 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 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 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 let response = state.finished.take()?.await.ok().flatten()?;
830 state.queue.push_back(response.to_vec());
831 }
832 },
833 response = finished => {
834 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 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
865fn 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
878const STREAM_BUFFER: usize = 32;
880
881struct SseState {
883 queue: std::collections::VecDeque<Vec<u8>>,
885 outgoing: mpsc::Receiver<Vec<u8>>,
886 finished: Option<oneshot::Receiver<Option<RpcResponse>>>,
888 _permit: Arc<OwnedSemaphorePermit>,
890 _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
902fn 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 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
972pub 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 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 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 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
1104fn 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 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
1209fn integers_equal(value: &Value, header: &str) -> bool {
1213 const EXACT_F64: f64 = 9_007_199_254_740_992.0; 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 .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
1230fn 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
1246fn 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 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 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
1309fn 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 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
1340fn 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 headers.insert(
1384 header::ACCESS_CONTROL_EXPOSE_HEADERS,
1385 HeaderValue::from_static("www-authenticate, mcp-session-id"),
1386 );
1387 }
1388}
1389
1390fn 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 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}