1use std::borrow::Cow;
35use std::convert::Infallible;
36use std::future::Future;
37use std::io;
38use std::pin::Pin;
39use std::sync::Arc;
40use std::task::{Context, Poll};
41use std::time::Duration;
42
43use base64::Engine as _;
44use bytes::Bytes;
45use http::header::{self, HeaderMap, HeaderValue};
46use http::{Method, Request, Response, StatusCode};
47use http_body::Body;
48use http_body_util::{BodyExt as _, Full};
49use serde::Deserialize;
50use serde_json::Value;
51use serde_json::value::RawValue;
52use tokio::net::TcpListener;
53use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
54use tokio_util::sync::{CancellationToken, DropGuard};
55
56use super::oauth::Challenge;
57pub use super::oauth::{OAuth, OAuthBuilder, OAuthError, ProtectedResourceMetadata};
58use crate::auth::AuthContext;
59use crate::context::TransportContext;
60use crate::error::{ErrorKind, McpError};
61use crate::hints::{self, Once};
62use crate::jsonrpc::{self, Message, Rejection, Response as RpcResponse};
63use crate::legacy::{self, ChannelPeer, LegacySession, PendingRequests};
64use crate::mirror::ParamKind;
65use crate::notify::NotifyChannel;
66use crate::server::{McpRequest, Server};
67use crate::spec::{RequestId, methods};
68
69pub struct HttpBody(BodyKind);
72
73enum BodyKind {
74 Full(Full<Bytes>),
75 Stream(Pin<Box<dyn Body<Data = Bytes, Error = Infallible> + Send>>),
76}
77
78impl HttpBody {
79 pub(crate) fn full(bytes: impl Into<Bytes>) -> Self {
80 Self(BodyKind::Full(Full::new(bytes.into())))
81 }
82}
83
84impl std::fmt::Debug for HttpBody {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match &self.0 {
87 BodyKind::Full(_) => f.write_str("HttpBody::Full"),
88 BodyKind::Stream(_) => f.write_str("HttpBody::Stream"),
89 }
90 }
91}
92
93impl Body for HttpBody {
94 type Data = Bytes;
95 type Error = Infallible;
96
97 fn poll_frame(
98 self: Pin<&mut Self>,
99 cx: &mut Context<'_>,
100 ) -> Poll<Option<Result<http_body::Frame<Bytes>, Infallible>>> {
101 match &mut self.get_mut().0 {
102 BodyKind::Full(body) => Pin::new(body).poll_frame(cx),
103 BodyKind::Stream(body) => body.as_mut().poll_frame(cx),
104 }
105 }
106
107 fn is_end_stream(&self) -> bool {
108 match &self.0 {
109 BodyKind::Full(body) => body.is_end_stream(),
110 BodyKind::Stream(body) => body.is_end_stream(),
111 }
112 }
113
114 fn size_hint(&self) -> http_body::SizeHint {
115 match &self.0 {
116 BodyKind::Full(body) => body.size_hint(),
117 BodyKind::Stream(body) => body.size_hint(),
118 }
119 }
120}
121
122const MCP_PROTOCOL_VERSION: &str = "mcp-protocol-version";
123const MCP_METHOD: &str = "mcp-method";
124const MCP_NAME: &str = "mcp-name";
125const MCP_PARAM_PREFIX: &str = "mcp-param-";
126const MCP_SESSION_ID: &str = "mcp-session-id";
127const BASE64_PREFIX: &str = "=?base64?";
128const BASE64_SUFFIX: &str = "?=";
129const ALLOWED_REQUEST_HEADERS: &str =
130 "content-type, accept, authorization, mcp-protocol-version, mcp-method, mcp-name";
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134#[non_exhaustive]
135pub enum AllowedOrigins {
136 Localhost,
139 List(Vec<String>),
141 Any,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148#[non_exhaustive]
149pub enum AllowedHosts {
150 Localhost,
152 List(Vec<String>),
154 Any,
156}
157
158#[derive(Debug, Clone)]
160pub struct HttpConfig {
161 oauth: Option<OAuth>,
162 origins: AllowedOrigins,
163 hosts: AllowedHosts,
164 header_timeout: Duration,
165 body_timeout: Duration,
166 max_in_flight: usize,
167 max_body_bytes_in_flight: usize,
168 max_connections: usize,
169}
170
171impl Default for HttpConfig {
172 fn default() -> Self {
173 Self {
174 oauth: None,
175 origins: AllowedOrigins::Localhost,
176 hosts: AllowedHosts::Localhost,
177 header_timeout: Duration::from_secs(10),
178 body_timeout: Duration::from_secs(30),
179 max_in_flight: 4096,
180 max_body_bytes_in_flight: 64 * 1024 * 1024,
181 max_connections: 4096,
182 }
183 }
184}
185
186impl HttpConfig {
187 #[must_use]
189 pub fn new() -> Self {
190 Self::default()
191 }
192
193 #[must_use]
196 pub fn oauth(mut self, oauth: OAuth) -> Self {
197 self.oauth = Some(oauth);
198 self
199 }
200
201 #[must_use]
203 pub fn allowed_origins(mut self, origins: AllowedOrigins) -> Self {
204 self.origins = origins;
205 self
206 }
207
208 #[must_use]
211 pub fn allowed_hosts(mut self, hosts: AllowedHosts) -> Self {
212 self.hosts = hosts;
213 self
214 }
215
216 #[must_use]
220 pub fn header_timeout(mut self, timeout: Duration) -> Self {
221 self.header_timeout = timeout;
222 self
223 }
224
225 #[must_use]
228 pub fn body_timeout(mut self, timeout: Duration) -> Self {
229 self.body_timeout = timeout;
230 self
231 }
232
233 #[must_use]
238 pub fn max_in_flight(mut self, requests: usize) -> Self {
239 self.max_in_flight = requests.max(1);
240 self
241 }
242
243 #[must_use]
249 pub fn max_body_bytes_in_flight(mut self, bytes: usize) -> Self {
250 self.max_body_bytes_in_flight = bytes;
251 self
252 }
253
254 #[must_use]
257 pub fn max_connections(mut self, connections: usize) -> Self {
258 self.max_connections = connections.max(1);
259 self
260 }
261}
262
263#[derive(Clone, Debug)]
265pub struct StreamableHttpService {
266 server: Server,
267 config: Arc<HttpConfig>,
268 in_flight: Arc<Semaphore>,
269 body_bytes: Arc<Semaphore>,
272 hints: Arc<Hints>,
273 pending: Arc<PendingRequests>,
275}
276
277#[derive(Debug, Default)]
279struct Hints {
280 origin: Once,
281 host: Once,
282 busy: Once,
283 slow_body: Once,
284 too_large: Once,
285 session: Once,
286}
287
288impl StreamableHttpService {
289 #[must_use]
291 pub fn new(server: Server) -> Self {
292 Self::with_config(server, HttpConfig::default())
293 }
294
295 #[must_use]
297 pub fn with_config(server: Server, config: HttpConfig) -> Self {
298 match &config.oauth {
299 None if server.has_scopes() => tracing::warn!(
300 "handlers declare OAuth scopes, but this HTTP service does not require OAuth: \
301 the scopes are not enforced\n = help: add HttpConfig::new().oauth(OAuth::\
302 builder(..)..build()?), or remove the scopes"
303 ),
304 Some(oauth) => {
305 let host = oauth
306 .resource()
307 .split_once("://")
308 .map(|(_, rest)| rest.split(['/', '?']).next().unwrap_or_default())
309 .unwrap_or_default();
310 if !host_name_allowed(&config.hosts, strip_port(host)) {
311 tracing::warn!(
312 "the OAuth resource {} is not on an allowed host, so its requests \
313 will get HTTP 403\n = help: list the host with \
314 HttpConfig::allowed_hosts(AllowedHosts::List(..))",
315 oauth.resource()
316 );
317 }
318 }
319 None => {}
320 }
321 let body_budget = config
324 .max_body_bytes_in_flight
325 .max(server.limits().max_message_bytes)
326 .min(Semaphore::MAX_PERMITS);
327 Self {
328 body_bytes: Arc::new(Semaphore::new(body_budget)),
329 server,
330 in_flight: Arc::new(Semaphore::new(config.max_in_flight)),
331 config: Arc::new(config),
332 hints: Arc::default(),
333 pending: Arc::default(),
334 }
335 }
336
337 async fn handle<B>(self, request: Request<B>) -> Response<HttpBody>
338 where
339 B: Body,
340 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
341 {
342 let (mut parts, body) = request.into_parts();
343
344 let cors = match parts.headers.get(header::ORIGIN) {
345 None => None,
346 Some(origin) if origin_allowed(&self.config.origins, origin) => Some(origin.clone()),
347 Some(origin) => {
348 hints::warn_once(
349 &self.hints.origin,
350 format_args!(
351 "rejected a request from Origin {} (HTTP 403)",
352 hints::client_text(origin.to_str().unwrap_or("<not ASCII>"))
353 ),
354 match self.config.origins {
355 AllowedOrigins::Localhost => {
356 "only localhost origins are accepted by default (DNS-rebinding \
357 protection); to allow a web app, list it: \
358 HttpConfig::new().allowed_origins(AllowedOrigins::List(vec![\
359 \"https://app.example.com\".into()]))"
360 }
361 _ => "add the origin to HttpConfig::allowed_origins if it is legitimate",
362 },
363 );
364 return rpc_error(
365 StatusCode::FORBIDDEN,
366 None,
367 &McpError::invalid_request("Origin not allowed"),
368 None,
369 );
370 }
371 };
372 if !host_allowed(&self.config.hosts, &parts) {
373 let host = parts
374 .headers
375 .get(header::HOST)
376 .and_then(|h| h.to_str().ok())
377 .or_else(|| parts.uri.authority().map(http::uri::Authority::as_str))
378 .unwrap_or("<none>");
379 hints::warn_once(
380 &self.hints.host,
381 format_args!(
382 "rejected a request for Host {} (HTTP 403)",
383 hints::client_text(host)
384 ),
385 match self.config.hosts {
386 AllowedHosts::Localhost => {
387 "only localhost is accepted by default (DNS-rebinding protection); to \
388 serve other machines, list your host names: \
389 HttpConfig::new().allowed_hosts(AllowedHosts::List(vec![\
390 \"mcp.example.com\".into()]))"
391 }
392 _ => "add the host name to HttpConfig::allowed_hosts if it is legitimate",
393 },
394 );
395 return rpc_error(
396 StatusCode::FORBIDDEN,
397 None,
398 &McpError::invalid_request("Host not allowed"),
399 None,
400 );
401 }
402
403 match parts.method {
404 Method::POST => {}
405 Method::OPTIONS if cors.is_some() => return preflight(cors, &parts.headers),
406 _ => {
407 let mut response = plain(StatusCode::METHOD_NOT_ALLOWED, cors.as_ref());
408 response
409 .headers_mut()
410 .insert(header::ALLOW, HeaderValue::from_static("POST"));
411 return response;
412 }
413 }
414
415 if !is_json_content_type(parts.headers.get(header::CONTENT_TYPE)) {
416 return rpc_error(
417 StatusCode::UNSUPPORTED_MEDIA_TYPE,
418 None,
419 &McpError::invalid_request("Content-Type must be application/json"),
420 cors.as_ref(),
421 );
422 }
423 if !accepts_json(&parts.headers) {
424 return rpc_error(
425 StatusCode::NOT_ACCEPTABLE,
426 None,
427 &McpError::invalid_request("Accept must allow application/json"),
428 cors.as_ref(),
429 );
430 }
431
432 let Ok(permit) = Arc::clone(&self.in_flight).try_acquire_owned() else {
434 hints::warn_once(
435 &self.hints.busy,
436 format_args!(
437 "rejected a request: {} requests already in flight (HTTP 503)",
438 self.config.max_in_flight
439 ),
440 "raise HttpConfig::max_in_flight(..) if the machine has room, or run more \
441 instances",
442 );
443 return busy(cors.as_ref());
444 };
445
446 let auth = match &self.config.oauth {
449 None => None,
450 Some(oauth) => match oauth.authenticate(&parts.headers).await {
451 Ok(auth) => {
452 parts.headers.remove(header::AUTHORIZATION);
455 Some(auth)
456 }
457 Err(challenge) => return challenge_response(oauth, &challenge, cors.as_ref()),
458 },
459 };
460
461 let limits = self.server.limits();
462 let declared = parts
463 .headers
464 .get(header::CONTENT_LENGTH)
465 .and_then(|v| v.to_str().ok())
466 .and_then(|v| v.parse::<u64>().ok());
467 let too_large = || {
468 hints::warn_once(
469 &self.hints.too_large,
470 format_args!(
471 "rejected a request body over the {}-byte message limit (HTTP 413)",
472 limits.max_message_bytes
473 ),
474 "raise ServerBuilder::max_message_bytes(..) if clients legitimately send larger arguments",
475 );
476 rpc_error(
477 StatusCode::PAYLOAD_TOO_LARGE,
478 None,
479 &McpError::invalid_request("Message too large"),
480 cors.as_ref(),
481 )
482 };
483 if declared.is_some_and(|len| len > limits.max_message_bytes as u64) {
484 return too_large();
485 }
486 let read = read_body(body, limits.max_message_bytes, Arc::clone(&self.body_bytes));
489 let Ok(read) = tokio::time::timeout(self.config.body_timeout, read).await else {
490 hints::warn_once(
491 &self.hints.slow_body,
492 format_args!(
493 "a client did not send its request body within {:?} (HTTP 408)",
494 self.config.body_timeout
495 ),
496 "raise HttpConfig::body_timeout(..) if clients on slow networks are expected",
497 );
498 return rpc_error(
499 StatusCode::REQUEST_TIMEOUT,
500 None,
501 &McpError::invalid_request("Request body not received in time"),
502 cors.as_ref(),
503 );
504 };
505 let (bytes, _body_permit) = match read {
507 Ok(read) => read,
508 Err(BodyError::TooLarge) => return too_large(),
509 Err(BodyError::Busy) => {
510 hints::warn_once(
511 &self.hints.busy,
512 format_args!(
513 "rejected a request: request bodies in flight reached {} bytes (HTTP 503)",
514 self.config.max_body_bytes_in_flight
515 ),
516 "raise HttpConfig::max_body_bytes_in_flight(..) if the machine has memory to spare, or run more instances",
517 );
518 return busy(cors.as_ref());
519 }
520 Err(BodyError::Read) => {
521 return rpc_error(
522 StatusCode::BAD_REQUEST,
523 None,
524 &McpError::invalid_request("Could not read the request body"),
525 cors.as_ref(),
526 );
527 }
528 };
529
530 let legacy = match self.legacy_session(&parts.headers, auth.as_ref()) {
532 Ok(legacy) => legacy,
533 Err(response) => return *response,
534 };
535
536 let message = match jsonrpc::parse(&bytes, limits) {
537 Ok(message) => message,
538 Err(Rejection::Error { id, error }) => {
539 return rpc_error(status_for(&error), id, &error, cors.as_ref());
540 }
541 Err(Rejection::UnexpectedResponse) if legacy.is_some() => {
542 let owner = session_owner(&parts.headers);
544 let routed = jsonrpc::parse_client_response(&bytes)
545 .is_some_and(|response| self.pending.complete(response, owner));
546 return if routed {
547 plain(StatusCode::ACCEPTED, cors.as_ref())
548 } else {
549 rpc_error(
550 StatusCode::BAD_REQUEST,
551 None,
552 &McpError::invalid_request("The response answers no pending request"),
553 cors.as_ref(),
554 )
555 };
556 }
557 Err(Rejection::UnexpectedResponse) => {
558 return rpc_error(
559 StatusCode::BAD_REQUEST,
560 None,
561 &McpError::invalid_request("Clients must not send JSON-RPC responses"),
562 cors.as_ref(),
563 );
564 }
565 };
566
567 let legacy = legacy.filter(|_| {
570 parts.headers.contains_key(MCP_SESSION_ID) || !declares_modern_meta(&message)
571 });
572
573 let modern = parts
577 .headers
578 .get(MCP_PROTOCOL_VERSION)
579 .and_then(|v| v.to_str().ok())
580 .is_some_and(|v| self.server.supported_versions().iter().any(|s| s == v));
581 if let Message::Request(request) = &message
582 && request.method == "initialize"
583 {
584 if self.server.legacy_clients_enabled() && !modern {
585 return self.legacy_initialize(request, cors.as_ref(), auth.as_ref());
586 }
587 if modern {
588 return rpc_error(
589 StatusCode::NOT_FOUND,
590 Some(request.id.clone()),
591 &McpError::method_not_found_explained(
592 "Method not found: `initialize` is not part of MCP 2026-07-28",
593 ),
594 cors.as_ref(),
595 );
596 }
597 }
598 if let Some(session) = legacy {
599 let mut transport = TransportContext::new();
600 let owner = session_owner(&parts.headers);
601 transport.insert(parts);
602 if let Some(auth) = auth {
603 transport.insert(auth);
604 }
605 return self
606 .handle_legacy(message, session, owner, transport, permit, cors)
607 .await;
608 }
609
610 let mut wants_notifications = false;
614 if let Message::Request(request) = &message
615 && request.method != "initialize"
616 {
617 match validate_headers(&self.server, &parts.headers, request) {
618 Ok(wants) => wants_notifications = wants,
619 Err(error) => {
620 return rpc_error(
621 StatusCode::BAD_REQUEST,
622 Some(request.id.clone()),
623 &error,
624 cors.as_ref(),
625 );
626 }
627 }
628 }
629
630 let mut transport = TransportContext::new();
631 transport.insert(parts);
632 if let Some(auth) = auth {
633 transport.insert(auth);
634 }
635 if wants_notifications {
636 let cancellation = CancellationToken::new();
639 let guard = cancellation.clone().drop_guard();
640 let server = self.server.clone();
641 return self
642 .respond_streaming(permit, cors, Some(guard), move |out| {
643 let mut transport = transport;
644 transport.insert(NotifyChannel(out));
645 Box::pin(async move {
646 server
647 .handle(McpRequest {
648 message,
649 transport,
650 cancellation,
651 })
652 .await
653 })
654 })
655 .await;
656 }
657 let response = self
658 .server
659 .handle(McpRequest {
660 message,
661 transport,
662 cancellation: CancellationToken::new(),
663 })
664 .await;
665 self.reply(response, cors.as_ref())
666 }
667
668 fn reply(
671 &self,
672 response: Option<RpcResponse>,
673 cors: Option<&HeaderValue>,
674 ) -> Response<HttpBody> {
675 let Some(response) = response else {
676 return plain(StatusCode::ACCEPTED, cors);
677 };
678 let status = response.as_error().map_or(StatusCode::OK, status_for);
679 let challenge = match (&self.config.oauth, response.as_error()) {
680 (Some(oauth), Some(error)) if error.kind() == ErrorKind::Forbidden => oauth
681 .www_authenticate(&Challenge::InsufficientScope(
682 error.required_scopes().join(" "),
683 )),
684 _ => None,
685 };
686 let mut http = json(status, response.to_vec(), cors);
687 if let Some(challenge) = challenge {
688 http.headers_mut()
689 .insert(header::WWW_AUTHENTICATE, challenge);
690 }
691 http
692 }
693}
694
695impl StreamableHttpService {
696 #[allow(clippy::result_large_err)]
701 fn legacy_session(
702 &self,
703 headers: &HeaderMap,
704 auth: Option<&AuthContext>,
705 ) -> Result<Option<LegacySession>, Box<Response<HttpBody>>> {
706 if !self.server.legacy_clients_enabled() {
707 return Ok(None);
708 }
709 let version = headers
710 .get(MCP_PROTOCOL_VERSION)
711 .and_then(|v| v.to_str().ok());
712 if version.is_some_and(|v| self.server.supported_versions().iter().any(|s| s == v)) {
713 return Ok(None);
714 }
715 match headers.get(MCP_SESSION_ID).and_then(|v| v.to_str().ok()) {
716 Some(token) => match self
717 .server
718 .open_session(token)
719 .filter(|s| s.principal.as_deref() == auth.map(|a| a.digest.as_str()))
721 {
722 Some(session) => Ok(Some(session)),
723 None => {
724 hints::warn_once(
725 &self.hints.session,
726 format_args!("rejected an unknown or expired MCP-Session-Id (HTTP 404)"),
727 "the client will initialize again; if this server runs as several \
728 instances, give all of them the same ServerBuilder::request_state_key(..)",
729 );
730 Err(Box::new(rpc_error(
731 StatusCode::NOT_FOUND,
732 None,
733 &McpError::invalid_request("Session not found or expired"),
734 None,
735 )))
736 }
737 },
738 None => Ok(version
739 .filter(|v| legacy::is_legacy_version(v))
740 .map(LegacySession::sessionless)),
741 }
742 }
743
744 fn legacy_initialize(
745 &self,
746 request: &jsonrpc::Request,
747 cors: Option<&HeaderValue>,
748 auth: Option<&AuthContext>,
749 ) -> Response<HttpBody> {
750 match self.server.legacy_initialize(request.params.as_deref()) {
751 Ok((result, mut session)) => {
752 session.principal = auth.map(|a| a.digest.clone());
753 let body = RpcResponse::Result {
754 id: request.id.clone(),
755 result,
756 }
757 .to_vec();
758 let mut response = json(StatusCode::OK, body, cors);
759 if let Some(token) = self.server.seal_session(&session)
760 && let Ok(value) = HeaderValue::from_str(&token)
761 {
762 response.headers_mut().insert(MCP_SESSION_ID, value);
763 }
764 response
765 }
766 Err(error) => rpc_error(status_for(&error), Some(request.id.clone()), &error, cors),
767 }
768 }
769
770 async fn handle_legacy(
773 &self,
774 message: Message,
775 session: LegacySession,
776 owner: u64,
777 transport: TransportContext,
778 permit: OwnedSemaphorePermit,
779 cors: Option<HeaderValue>,
780 ) -> Response<HttpBody> {
781 let Message::Request(request) = message else {
782 return plain(StatusCode::ACCEPTED, cors.as_ref());
784 };
785 let pending = Arc::clone(&self.pending);
786 let timeout = self.server.limits().tool_timeout;
787 let server = self.server.clone();
788 self.respond_streaming(permit, cors, None, move |out| {
790 let mut transport = transport;
791 transport.insert(NotifyChannel(out.clone()));
792 let peer = ChannelPeer {
793 pending,
794 owner,
795 out,
796 timeout,
797 };
798 Box::pin(async move {
799 server
800 .handle_legacy(
801 request,
802 &session,
803 transport,
804 CancellationToken::new(),
805 &peer,
806 )
807 .await
808 })
809 })
810 .await
811 }
812
813 async fn respond_streaming<F>(
819 &self,
820 permit: OwnedSemaphorePermit,
821 cors: Option<HeaderValue>,
822 cancel: Option<DropGuard>,
823 run: F,
824 ) -> Response<HttpBody>
825 where
826 F: FnOnce(
827 mpsc::Sender<Vec<u8>>,
828 ) -> Pin<Box<dyn Future<Output = Option<RpcResponse>> + Send>>,
829 {
830 let (out, mut outgoing) = mpsc::channel::<Vec<u8>>(STREAM_BUFFER);
831 let (done, mut finished) = oneshot::channel::<Option<RpcResponse>>();
832 let task = run(out);
833 let permit = Arc::new(permit);
837 let task_permit = Arc::clone(&permit);
838 tokio::spawn(async move {
839 let _ = done.send(task.await);
840 drop(task_permit);
841 });
842
843 let reply = |response: Option<RpcResponse>| self.reply(response, cors.as_ref());
844 let first = tokio::select! {
845 biased;
846 first = outgoing.recv() => first,
847 response = &mut finished => return reply(response.ok().flatten()),
848 };
849 let Some(first) = first else {
850 return reply(finished.await.ok().flatten());
851 };
852 let state = SseState {
853 queue: std::collections::VecDeque::from([first]),
854 outgoing,
855 finished: Some(finished),
856 _permit: permit,
857 _cancel: cancel,
858 };
859 let stream = futures_util::stream::unfold(state, |mut state| async move {
860 loop {
861 if let Some(message) = state.queue.pop_front() {
862 return Some((Ok(sse_event(&message)), state));
863 }
864 let finished = state.finished.as_mut()?;
865 tokio::select! {
866 biased;
867 message = state.outgoing.recv() => match message {
868 Some(message) => state.queue.push_back(message),
869 None => {
870 let response = state.finished.take()?.await.ok().flatten()?;
872 state.queue.push_back(response.to_vec());
873 }
874 },
875 response = finished => {
876 state.finished = None;
878 while let Ok(message) = state.outgoing.try_recv() {
879 state.queue.push_back(message);
880 }
881 if let Ok(Some(response)) = response {
882 state.queue.push_back(response.to_vec());
883 }
884 }
885 }
886 }
887 });
888 let body = http_body_util::StreamBody::new(stream);
889 let mut response = Response::new(HttpBody(BodyKind::Stream(Box::pin(body))));
890 let headers = response.headers_mut();
891 headers.insert(
892 header::CONTENT_TYPE,
893 HeaderValue::from_static("text/event-stream"),
894 );
895 headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
896 headers.insert("x-accel-buffering", HeaderValue::from_static("no"));
898 headers.insert(
899 header::X_CONTENT_TYPE_OPTIONS,
900 HeaderValue::from_static("nosniff"),
901 );
902 with_cors(&mut response, cors.as_ref());
903 response
904 }
905}
906
907fn declares_modern_meta(message: &Message) -> bool {
909 let Message::Request(request) = message else {
910 return false;
911 };
912 request.params.as_deref().is_some_and(|params| {
913 serde_json::from_str::<PeekParams<'_>>(params.get())
914 .ok()
915 .and_then(|p| p.meta)
916 .is_some_and(|meta| meta.protocol_version.is_some())
917 })
918}
919
920const STREAM_BUFFER: usize = 32;
922
923struct SseState {
925 queue: std::collections::VecDeque<Vec<u8>>,
927 outgoing: mpsc::Receiver<Vec<u8>>,
928 finished: Option<oneshot::Receiver<Option<RpcResponse>>>,
930 _permit: Arc<OwnedSemaphorePermit>,
932 _cancel: Option<DropGuard>,
934}
935
936fn sse_event(message: &[u8]) -> http_body::Frame<Bytes> {
937 let mut event = Vec::with_capacity(message.len() + 24);
938 event.extend_from_slice(b"event: message\ndata: ");
939 event.extend_from_slice(message);
940 event.extend_from_slice(b"\n\n");
941 http_body::Frame::data(Bytes::from(event))
942}
943
944fn session_owner(headers: &HeaderMap) -> u64 {
947 use std::hash::{BuildHasher as _, BuildHasherDefault, DefaultHasher};
948 headers.get(MCP_SESSION_ID).map_or(0, |token| {
949 BuildHasherDefault::<DefaultHasher>::default().hash_one(token.as_bytes())
950 })
951}
952
953impl<B> tower_service::Service<Request<B>> for StreamableHttpService
954where
955 B: Body + Send + 'static,
956 B::Data: Send,
957 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
958{
959 type Response = Response<HttpBody>;
960 type Error = Infallible;
961 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Infallible>> + Send>>;
962
963 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
964 Poll::Ready(Ok(()))
965 }
966
967 fn call(&mut self, request: Request<B>) -> Self::Future {
968 let service = self.clone();
969 Box::pin(async move { Ok(service.handle(request).await) })
970 }
971}
972
973impl Server {
974 pub async fn serve_http(
984 self,
985 addr: impl tokio::net::ToSocketAddrs + Clone + std::fmt::Debug,
986 ) -> io::Result<()> {
987 let listener = TcpListener::bind(addr.clone()).await.map_err(|error| {
988 let help = match error.kind() {
989 io::ErrorKind::AddrInUse => {
990 "another program (or another copy of this server) is \
991 already using this port; stop it or choose another port"
992 }
993 io::ErrorKind::PermissionDenied => {
994 "ports below 1024 usually need elevated \
995 privileges; choose a port such as 8000"
996 }
997 _ => "check the address, for example `127.0.0.1:8000`",
998 };
999 io::Error::new(
1000 error.kind(),
1001 format!("could not listen on {addr:?}: {error}\n = help: {help}"),
1002 )
1003 })?;
1004 serve(
1005 listener,
1006 StreamableHttpService::new(self),
1007 "/mcp",
1008 std::future::pending(),
1009 )
1010 .await
1011 }
1012}
1013
1014pub async fn serve(
1021 listener: TcpListener,
1022 service: StreamableHttpService,
1023 path: &str,
1024 shutdown: impl Future<Output = ()>,
1025) -> io::Result<()> {
1026 use hyper::server::conn::http1;
1027 use hyper_util::rt::{TokioIo, TokioTimer};
1028 use hyper_util::server::graceful::GracefulShutdown;
1029
1030 if let Ok(addr) = listener.local_addr()
1031 && !addr.ip().is_loopback()
1032 && service.config.hosts == AllowedHosts::Localhost
1033 {
1034 tracing::warn!(
1035 "listening on {addr}, but only requests addressed to localhost are accepted\n \
1036 = help: other machines will get HTTP 403; list the host names they use with \
1037 HttpConfig::allowed_hosts and serve with StreamableHttpService::with_config, or \
1038 bind 127.0.0.1"
1039 );
1040 }
1041
1042 let path: Arc<str> = Arc::from(path);
1043 let metadata = service.config.oauth.as_ref().map(|oauth| {
1044 (
1045 Arc::<[String]>::from(oauth.metadata_paths()),
1046 oauth.metadata_service(),
1047 )
1048 });
1049 let grace = service.server.limits().tool_timeout;
1050 let connections = Arc::new(Semaphore::new(service.config.max_connections));
1051 let mut builder = http1::Builder::new();
1056 builder
1057 .timer(TokioTimer::new())
1058 .header_read_timeout(service.config.header_timeout);
1059 let graceful = GracefulShutdown::new();
1060 tokio::pin!(shutdown);
1061
1062 loop {
1063 let permit = tokio::select! {
1065 permit = Arc::clone(&connections).acquire_owned() => match permit {
1066 Ok(permit) => permit,
1067 Err(_closed) => break,
1068 },
1069 () = &mut shutdown => break,
1070 };
1071 let (stream, peer) = tokio::select! {
1072 accepted = listener.accept() => match accepted {
1073 Ok(accepted) => accepted,
1074 Err(error) => {
1075 tracing::warn!(%error, "failed to accept a connection");
1078 tokio::time::sleep(Duration::from_millis(50)).await;
1079 continue;
1080 }
1081 },
1082 () = &mut shutdown => break,
1083 };
1084 let service = service.clone();
1085 let path = Arc::clone(&path);
1086 let metadata = metadata.clone();
1087 let hyper_service =
1088 hyper::service::service_fn(move |request: Request<hyper::body::Incoming>| {
1089 let mut service = service.clone();
1090 let path = Arc::clone(&path);
1091 let metadata = metadata.clone();
1092 async move {
1093 let requested = request.uri().path();
1094 if requested == &*path {
1095 return tower_service::Service::call(&mut service, request).await;
1096 }
1097 match &metadata {
1098 Some((paths, metadata)) if paths.iter().any(|p| p == requested) => {
1099 Ok(metadata.respond(request.method()))
1100 }
1101 _ => Ok(plain(StatusCode::NOT_FOUND, None)),
1102 }
1103 }
1104 });
1105 let connection = builder.serve_connection(TokioIo::new(stream), hyper_service);
1106 let connection = graceful.watch(connection);
1107 tokio::spawn(async move {
1108 if let Err(error) = connection.await {
1109 tracing::debug!(%peer, %error, "connection closed with an error");
1110 }
1111 drop(permit);
1112 });
1113 }
1114
1115 if tokio::time::timeout(grace, graceful.shutdown())
1116 .await
1117 .is_err()
1118 {
1119 tracing::warn!("open connections did not close before the shutdown deadline");
1120 }
1121 Ok(())
1122}
1123
1124#[derive(Deserialize)]
1125struct PeekParams<'a> {
1126 #[serde(rename = "_meta", borrow, default)]
1127 meta: Option<PeekMeta<'a>>,
1128 #[serde(borrow, default)]
1129 name: Option<Cow<'a, str>>,
1130 #[serde(borrow, default)]
1131 uri: Option<Cow<'a, str>>,
1132 #[serde(borrow, default)]
1133 arguments: Option<&'a RawValue>,
1134}
1135
1136#[derive(Deserialize)]
1137struct PeekMeta<'a> {
1138 #[serde(rename = "io.modelcontextprotocol/protocolVersion", borrow, default)]
1139 protocol_version: Option<Cow<'a, str>>,
1140 #[serde(rename = "progressToken", borrow, default)]
1141 progress_token: Option<&'a RawValue>,
1142 #[serde(rename = "io.modelcontextprotocol/logLevel", borrow, default)]
1143 log_level: Option<&'a RawValue>,
1144}
1145
1146fn validate_headers(
1150 server: &Server,
1151 headers: &HeaderMap,
1152 request: &jsonrpc::Request,
1153) -> Result<bool, McpError> {
1154 let version = single_header(headers, MCP_PROTOCOL_VERSION)?
1155 .ok_or_else(|| McpError::header_mismatch("Missing MCP-Protocol-Version header"))?;
1156 let method = single_header(headers, MCP_METHOD)?
1157 .ok_or_else(|| McpError::header_mismatch("Missing Mcp-Method header"))?;
1158 if method != request.method {
1159 return Err(McpError::header_mismatch(
1160 "Mcp-Method header does not match the request method",
1161 ));
1162 }
1163
1164 let Ok(params) = serde_json::from_str::<PeekParams<'_>>(
1166 request.params.as_deref().map_or("{}", RawValue::get),
1167 ) else {
1168 return Ok(false);
1169 };
1170 let wants_notifications = params
1171 .meta
1172 .as_ref()
1173 .is_some_and(|m| m.progress_token.is_some() || m.log_level.is_some());
1174
1175 if let Some(body_version) = params
1176 .meta
1177 .as_ref()
1178 .and_then(|m| m.protocol_version.as_deref())
1179 && body_version != version
1180 {
1181 return Err(McpError::header_mismatch(
1182 "MCP-Protocol-Version header does not match `_meta`",
1183 ));
1184 }
1185
1186 let name_source = match request.method.as_str() {
1187 methods::TOOLS_CALL | methods::PROMPTS_GET => Some(params.name.as_deref()),
1188 methods::RESOURCES_READ => Some(params.uri.as_deref()),
1189 _ => None,
1190 };
1191 if let Some(body_name) = name_source {
1192 let header_name = single_header(headers, MCP_NAME)?
1193 .ok_or_else(|| McpError::header_mismatch("Missing Mcp-Name header"))?;
1194 if Some(decode_header_value(header_name)?.as_ref()) != body_name {
1195 return Err(McpError::header_mismatch(
1196 "Mcp-Name header does not match the request body",
1197 ));
1198 }
1199 }
1200
1201 if request.method == methods::TOOLS_CALL
1202 && let Some(name) = params.name.as_deref()
1203 {
1204 validate_param_headers(server, headers, name, params.arguments)?;
1205 }
1206 Ok(wants_notifications)
1207}
1208
1209fn validate_param_headers(
1210 server: &Server,
1211 headers: &HeaderMap,
1212 tool: &str,
1213 arguments: Option<&RawValue>,
1214) -> Result<(), McpError> {
1215 let Some(mirrored) = server.mirrored_params(tool).filter(|m| !m.is_empty()) else {
1216 return Ok(());
1217 };
1218 let arguments: Value = match arguments {
1219 Some(raw) => serde_json::from_str(raw.get()).unwrap_or(Value::Null),
1220 None => Value::Null,
1221 };
1222 for param in mirrored {
1223 let header_name = format!("{MCP_PARAM_PREFIX}{}", param.header().to_ascii_lowercase());
1224 let header = single_header(headers, &header_name)?;
1225 let value = param.value_in(&arguments).filter(|v| !v.is_null());
1226 let matches = match (value, header) {
1227 (None, None) => true,
1228 (None, Some(_)) | (Some(_), None) => false,
1229 (Some(value), Some(header)) => {
1230 let header = decode_header_value(header)?;
1231 match param.kind() {
1232 ParamKind::String => value.as_str() == Some(header.as_ref()),
1233 ParamKind::Boolean => {
1234 value.as_bool().map(|b| if b { "true" } else { "false" })
1235 == Some(header.as_ref())
1236 }
1237 ParamKind::Integer => integers_equal(value, &header),
1238 }
1239 }
1240 };
1241 if !matches {
1242 return Err(McpError::header_mismatch(format!(
1243 "Mcp-Param-{} header does not match the request body",
1244 param.header()
1245 )));
1246 }
1247 }
1248 Ok(())
1249}
1250
1251fn integers_equal(value: &Value, header: &str) -> bool {
1255 const EXACT_F64: f64 = 9_007_199_254_740_992.0; let as_integer = |text: &str, float: Option<f64>| -> Option<i128> {
1257 text.parse::<i128>().ok().or_else(|| {
1258 float
1259 .filter(|f| f.fract() == 0.0 && f.abs() <= EXACT_F64)
1260 .map(|f| f as i128)
1262 })
1263 };
1264 let body = match value {
1265 Value::Number(n) => as_integer(&n.to_string(), n.as_f64()),
1266 _ => None,
1267 };
1268 let header = as_integer(header, header.parse::<f64>().ok());
1269 matches!((body, header), (Some(a), Some(b)) if a == b)
1270}
1271
1272fn single_header<'h>(headers: &'h HeaderMap, name: &str) -> Result<Option<&'h str>, McpError> {
1275 let mut values = headers.get_all(name).iter();
1276 let Some(first) = values.next() else {
1277 return Ok(None);
1278 };
1279 if values.next().is_some() {
1280 return Err(McpError::header_mismatch("Duplicate MCP header"));
1281 }
1282 first
1283 .to_str()
1284 .map(Some)
1285 .map_err(|_| McpError::header_mismatch("MCP header contains invalid characters"))
1286}
1287
1288fn decode_header_value(value: &str) -> Result<Cow<'_, str>, McpError> {
1290 match value
1291 .strip_prefix(BASE64_PREFIX)
1292 .and_then(|v| v.strip_suffix(BASE64_SUFFIX))
1293 {
1294 None => Ok(Cow::Borrowed(value)),
1295 Some(encoded) => base64::engine::general_purpose::STANDARD
1296 .decode(encoded)
1297 .ok()
1298 .and_then(|bytes| String::from_utf8(bytes).ok())
1299 .map(Cow::Owned)
1300 .ok_or_else(|| McpError::header_mismatch("Invalid base64-encoded MCP header")),
1301 }
1302}
1303
1304fn status_for(error: &McpError) -> StatusCode {
1305 match error.kind() {
1306 ErrorKind::BadRequest => StatusCode::BAD_REQUEST,
1307 ErrorKind::MethodNotFound => StatusCode::NOT_FOUND,
1308 ErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
1309 ErrorKind::Forbidden => StatusCode::FORBIDDEN,
1310 _ => StatusCode::OK,
1311 }
1312}
1313
1314fn origin_allowed(allowed: &AllowedOrigins, origin: &HeaderValue) -> bool {
1315 let Ok(origin) = origin.to_str() else {
1316 return false;
1317 };
1318 match allowed {
1319 AllowedOrigins::Any => true,
1320 AllowedOrigins::List(list) => list.iter().any(|o| o.eq_ignore_ascii_case(origin)),
1321 AllowedOrigins::Localhost => {
1322 let Some((scheme, rest)) = origin.split_once("://") else {
1323 return false;
1324 };
1325 matches!(scheme, "http" | "https") && is_localhost(strip_port(rest))
1326 }
1327 }
1328}
1329
1330fn host_allowed(allowed: &AllowedHosts, parts: &http::request::Parts) -> bool {
1331 if parts.headers.get_all(header::HOST).iter().nth(1).is_some() {
1334 return false;
1335 }
1336 if *allowed == AllowedHosts::Any {
1337 return true;
1338 }
1339 let host = parts
1341 .headers
1342 .get(header::HOST)
1343 .and_then(|h| h.to_str().ok())
1344 .or_else(|| parts.uri.authority().map(http::uri::Authority::as_str));
1345 let Some(host) = host else {
1346 return false;
1347 };
1348 host_name_allowed(allowed, strip_port(host))
1349}
1350
1351fn host_name_allowed(allowed: &AllowedHosts, host: &str) -> bool {
1353 match allowed {
1354 AllowedHosts::Any => true,
1355 AllowedHosts::List(list) => list.iter().any(|h| h.eq_ignore_ascii_case(host)),
1356 AllowedHosts::Localhost => is_localhost(host),
1357 }
1358}
1359
1360pub(crate) fn strip_port(authority: &str) -> &str {
1361 if authority.starts_with('[') {
1362 return authority
1364 .find(']')
1365 .and_then(|end| authority.get(..=end))
1366 .unwrap_or(authority);
1367 }
1368 authority.split(':').next().unwrap_or(authority)
1369}
1370
1371pub(crate) fn is_localhost(host: &str) -> bool {
1372 host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1" || host == "[::1]"
1373}
1374
1375fn is_json_content_type(value: Option<&HeaderValue>) -> bool {
1376 value
1377 .and_then(|v| v.to_str().ok())
1378 .and_then(|v| v.split(';').next())
1379 .is_some_and(|mime| mime.trim().eq_ignore_ascii_case("application/json"))
1380}
1381
1382fn accepts_json(headers: &HeaderMap) -> bool {
1384 let mut values = headers.get_all(header::ACCEPT).iter().peekable();
1385 if values.peek().is_none() {
1386 return true;
1387 }
1388 values
1389 .filter_map(|v| v.to_str().ok())
1390 .flat_map(|v| v.split(','))
1391 .filter_map(|item| item.split(';').next())
1392 .map(str::trim)
1393 .any(|mime| {
1394 mime.eq_ignore_ascii_case("application/json")
1395 || mime.eq_ignore_ascii_case("application/*")
1396 || mime == "*/*"
1397 })
1398}
1399
1400fn preflight(origin: Option<HeaderValue>, request_headers: &HeaderMap) -> Response<HttpBody> {
1401 let mut response = plain(StatusCode::NO_CONTENT, origin.as_ref());
1402 let headers = response.headers_mut();
1403 headers.insert(
1404 header::ACCESS_CONTROL_ALLOW_METHODS,
1405 HeaderValue::from_static("POST"),
1406 );
1407 let allowed = request_headers
1408 .get(header::ACCESS_CONTROL_REQUEST_HEADERS)
1409 .cloned()
1410 .unwrap_or_else(|| HeaderValue::from_static(ALLOWED_REQUEST_HEADERS));
1411 headers.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, allowed);
1412 headers.insert(
1413 header::ACCESS_CONTROL_MAX_AGE,
1414 HeaderValue::from_static("600"),
1415 );
1416 response
1417}
1418
1419fn with_cors(response: &mut Response<HttpBody>, origin: Option<&HeaderValue>) {
1420 if let Some(origin) = origin {
1421 let headers = response.headers_mut();
1422 headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone());
1423 headers.insert(header::VARY, HeaderValue::from_static("Origin"));
1424 headers.insert(
1426 header::ACCESS_CONTROL_EXPOSE_HEADERS,
1427 HeaderValue::from_static("www-authenticate, mcp-session-id"),
1428 );
1429 }
1430}
1431
1432enum BodyError {
1434 TooLarge,
1436 Busy,
1438 Read,
1440}
1441
1442async fn read_body<B>(
1445 body: B,
1446 limit: usize,
1447 budget: Arc<Semaphore>,
1448) -> Result<(Bytes, Option<OwnedSemaphorePermit>), BodyError>
1449where
1450 B: Body,
1451{
1452 let mut body = std::pin::pin!(body);
1453 let mut buffer = bytes::BytesMut::new();
1454 let mut reserved: Option<OwnedSemaphorePermit> = None;
1455 while let Some(frame) = body.frame().await {
1456 let frame = frame.map_err(|_| BodyError::Read)?;
1457 let Ok(data) = frame.into_data() else {
1458 continue; };
1460 let len = bytes::Buf::remaining(&data);
1461 if len == 0 {
1462 continue;
1463 }
1464 if buffer.len().saturating_add(len) > limit {
1465 return Err(BodyError::TooLarge);
1466 }
1467 let size = u32::try_from(len).map_err(|_| BodyError::TooLarge)?;
1468 let permit = Arc::clone(&budget)
1469 .try_acquire_many_owned(size)
1470 .map_err(|_| BodyError::Busy)?;
1471 match &mut reserved {
1472 Some(held) => held.merge(permit),
1473 None => reserved = Some(permit),
1474 }
1475 bytes::BufMut::put(&mut buffer, data);
1476 }
1477 Ok((buffer.freeze(), reserved))
1478}
1479
1480fn busy(cors: Option<&HeaderValue>) -> Response<HttpBody> {
1482 let mut response = rpc_error(
1483 StatusCode::SERVICE_UNAVAILABLE,
1484 None,
1485 &McpError::internal_with_message("The server is busy; retry later."),
1486 cors,
1487 );
1488 response
1489 .headers_mut()
1490 .insert(header::RETRY_AFTER, HeaderValue::from_static("1"));
1491 response
1492}
1493
1494fn challenge_response(
1496 oauth: &OAuth,
1497 challenge: &Challenge,
1498 cors: Option<&HeaderValue>,
1499) -> Response<HttpBody> {
1500 let error = match challenge {
1501 Challenge::Unavailable => McpError::internal_with_message(challenge.message()),
1502 _ => McpError::invalid_request(challenge.message()),
1503 };
1504 let mut response = rpc_error(challenge.status(), None, &error, cors);
1505 if let Some(value) = oauth.www_authenticate(challenge) {
1506 response
1507 .headers_mut()
1508 .insert(header::WWW_AUTHENTICATE, value);
1509 }
1510 if *challenge == Challenge::Unavailable {
1511 response
1512 .headers_mut()
1513 .insert(header::RETRY_AFTER, HeaderValue::from_static("5"));
1514 }
1515 response
1516}
1517
1518fn plain(status: StatusCode, cors: Option<&HeaderValue>) -> Response<HttpBody> {
1519 let mut response = Response::new(HttpBody::full(Bytes::new()));
1520 *response.status_mut() = status;
1521 with_cors(&mut response, cors);
1522 response
1523}
1524
1525fn json(status: StatusCode, body: Vec<u8>, cors: Option<&HeaderValue>) -> Response<HttpBody> {
1526 let mut response = Response::new(HttpBody::full(body));
1527 *response.status_mut() = status;
1528 response.headers_mut().insert(
1529 header::CONTENT_TYPE,
1530 HeaderValue::from_static("application/json"),
1531 );
1532 response.headers_mut().insert(
1533 header::X_CONTENT_TYPE_OPTIONS,
1534 HeaderValue::from_static("nosniff"),
1535 );
1536 with_cors(&mut response, cors);
1537 response
1538}
1539
1540fn rpc_error(
1541 status: StatusCode,
1542 id: Option<RequestId>,
1543 error: &McpError,
1544 cors: Option<&HeaderValue>,
1545) -> Response<HttpBody> {
1546 json(status, RpcResponse::error(id, error.clone()).to_vec(), cors)
1547}
1548
1549#[cfg(test)]
1550mod tests {
1551 use super::*;
1552
1553 #[test]
1554 fn decodes_base64_sentinel() {
1555 assert_eq!(decode_header_value("us-west1").unwrap(), "us-west1");
1556 assert_eq!(
1557 decode_header_value("=?base64?SGVsbG8sIOS4lueVjA==?=").unwrap(),
1558 "Hello, 世界"
1559 );
1560 assert_eq!(
1561 decode_header_value("=?base64?PT9iYXNlNjQ/bGl0ZXJhbD89?=").unwrap(),
1562 "=?base64?literal?="
1563 );
1564 assert!(decode_header_value("=?base64?***?=").is_err());
1565 }
1566
1567 #[test]
1568 fn origin_rules() {
1569 let localhost = AllowedOrigins::Localhost;
1570 let ok = |o: &'static str| origin_allowed(&localhost, &HeaderValue::from_static(o));
1571 assert!(ok("http://localhost:6274"));
1572 assert!(ok("http://127.0.0.1"));
1573 assert!(ok("https://[::1]:8443"));
1574 assert!(!ok("http://localhost.evil.com"));
1575 assert!(!ok("http://evil.com"));
1576 assert!(!ok("null"));
1577 }
1578
1579 fn parts(host: Option<&'static str>, uri: &str) -> http::request::Parts {
1580 let mut builder = Request::builder().uri(uri);
1581 if let Some(host) = host {
1582 builder = builder.header(header::HOST, host);
1583 }
1584 builder.body(()).unwrap().into_parts().0
1585 }
1586
1587 #[test]
1588 fn host_rules() {
1589 let local = parts(Some("127.0.0.1:8000"), "/mcp");
1590 assert!(host_allowed(&AllowedHosts::Localhost, &local));
1591 let evil = parts(Some("attacker.example"), "/mcp");
1592 assert!(!host_allowed(&AllowedHosts::Localhost, &evil));
1593 assert!(host_allowed(
1594 &AllowedHosts::List(vec!["attacker.example".into()]),
1595 &evil
1596 ));
1597 let h2 = parts(None, "http://localhost:8000/mcp");
1599 assert!(host_allowed(&AllowedHosts::Localhost, &h2));
1600 assert!(!host_allowed(
1601 &AllowedHosts::Localhost,
1602 &parts(None, "/mcp")
1603 ));
1604 }
1605
1606 #[test]
1607 fn accept_rules() {
1608 let mut headers = HeaderMap::new();
1609 assert!(accepts_json(&headers));
1610 headers.insert(
1611 header::ACCEPT,
1612 HeaderValue::from_static("application/json, text/event-stream"),
1613 );
1614 assert!(accepts_json(&headers));
1615 headers.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
1616 assert!(!accepts_json(&headers));
1617 }
1618}