1use std::{
2 borrow::Cow,
3 collections::{HashMap, HashSet, VecDeque},
4 sync::Arc,
5 time::Duration,
6};
7
8use futures::{
9 Stream, StreamExt,
10 future::BoxFuture,
11 stream::{BoxStream, FuturesUnordered},
12};
13use http::{HeaderName, HeaderValue, StatusCode};
14pub use sse_stream::Error as SseError;
15use sse_stream::Sse;
16use thiserror::Error;
17use tokio_util::sync::CancellationToken;
18use tracing::debug;
19
20use super::common::client_side_sse::{
21 DEFAULT_MAX_SSE_EVENT_SIZE, ExponentialBackoff, SseRetryPolicy, SseStreamReconnect,
22};
23use crate::{
24 RoleClient,
25 model::{
26 ClientJsonRpcMessage, ClientNotification, ClientRequest, ErrorData, GetExtensions, GetMeta,
27 InitializedNotification, JsonObject, ProtocolVersion, RequestId, ServerJsonRpcMessage,
28 ServerResult,
29 },
30 service::InboundStreamOrigin,
31 transport::{
32 common::{client_side_sse::SseAutoReconnectStream, mcp_headers},
33 worker::{
34 RequestCancellationRegistration, Worker, WorkerQuitReason, WorkerSendRequest,
35 WorkerTransport,
36 },
37 },
38};
39
40type BoxedSseStream = BoxStream<'static, Result<Sse, SseError>>;
41type SseTaskResult<E> = (Option<RequestId>, Result<(), StreamableHttpError<E>>);
42const SESSION_CLEANUP_TIMEOUT: Duration = Duration::from_secs(5);
43
44fn build_request_headers(
45 base: &HashMap<HeaderName, HeaderValue>,
46 message: &ClientJsonRpcMessage,
47 tool_cache: &HashMap<String, Arc<JsonObject>>,
48 version: &ProtocolVersion,
49) -> HashMap<HeaderName, HeaderValue> {
50 use serde_json::Value;
51
52 let mut headers = base.clone();
53 if *version >= ProtocolVersion::STANDARD_HEADERS
54 && let Ok(value) = serde_json::to_value(message)
55 {
56 let schema = value
57 .get("method")
58 .and_then(Value::as_str)
59 .filter(|method| *method == "tools/call")
60 .and_then(|_| value.get("params"))
61 .and_then(|params| params.get("name"))
62 .and_then(Value::as_str)
63 .and_then(|name| tool_cache.get(name))
64 .map(Arc::as_ref);
65 for (name, val) in mcp_headers::standard_request_headers(&value, schema) {
66 headers.insert(name, val);
67 }
68 }
69 headers
70}
71
72fn request_version_headers(
73 base: &HashMap<HeaderName, HeaderValue>,
74 message: &ClientJsonRpcMessage,
75 fallback: &ProtocolVersion,
76 tool_cache: &HashMap<String, Arc<JsonObject>>,
77) -> (ProtocolVersion, HashMap<HeaderName, HeaderValue>) {
78 let version = match message {
79 ClientJsonRpcMessage::Request(request) => request
80 .request
81 .get_meta()
82 .protocol_version()
83 .unwrap_or_else(|| fallback.clone()),
84 _ => fallback.clone(),
85 };
86 let mut headers = build_request_headers(base, message, tool_cache, &version);
87 if let Ok(value) = HeaderValue::from_str(version.as_str()) {
88 headers.insert(HeaderName::from_static("mcp-protocol-version"), value);
89 }
90 (version, headers)
91}
92
93fn cache_tools_from_response(
94 cache: &mut HashMap<String, Arc<JsonObject>>,
95 message: &mut ServerJsonRpcMessage,
96 protocol_version: &ProtocolVersion,
97) {
98 if protocol_version < &ProtocolVersion::STANDARD_HEADERS {
99 return;
100 }
101 if let ServerJsonRpcMessage::Response(response) = message
102 && let ServerResult::ListToolsResult(list) = &mut response.result
103 {
104 list.tools.retain(|tool| {
105 let Err(reason) =
106 mcp_headers::validate_param_header_annotations(&tool.input_schema)
107 else {
108 cache.insert(tool.name.to_string(), tool.input_schema.clone());
109 return true;
110 };
111 tracing::warn!(tool = %tool.name, "rejecting invalid x-mcp-header annotations: {reason}");
112 false
113 });
114 }
115}
116
117fn negotiate_version_headers(
118 init_response: &ServerJsonRpcMessage,
119 base: HashMap<HeaderName, HeaderValue>,
120) -> (ProtocolVersion, HashMap<HeaderName, HeaderValue>) {
121 let mut version = ProtocolVersion::default();
122 let mut headers = base;
123 if let ServerJsonRpcMessage::Response(response) = init_response
124 && let ServerResult::InitializeResult(init_result) = &response.result
125 {
126 version = init_result.protocol_version.clone();
127 if let Ok(hv) = HeaderValue::from_str(init_result.protocol_version.as_str()) {
129 headers.insert(HeaderName::from_static("mcp-protocol-version"), hv);
130 }
131 }
132 (version, headers)
133}
134
135#[derive(Debug, Error)]
136#[error("authorization required: {www_authenticate_header}")]
137#[non_exhaustive]
138pub struct AuthRequiredError {
139 pub www_authenticate_header: String,
140}
141
142impl AuthRequiredError {
143 pub fn new(www_authenticate_header: String) -> Self {
145 Self {
146 www_authenticate_header,
147 }
148 }
149}
150
151#[derive(Debug, Error)]
152#[error("insufficient scope: {www_authenticate_header}")]
153#[non_exhaustive]
154pub struct InsufficientScopeError {
155 pub www_authenticate_header: String,
156 pub required_scope: Option<String>,
157}
158
159impl InsufficientScopeError {
160 pub fn new(www_authenticate_header: String, required_scope: Option<String>) -> Self {
162 Self {
163 www_authenticate_header,
164 required_scope,
165 }
166 }
167
168 pub fn can_upgrade(&self) -> bool {
170 self.required_scope.is_some()
171 }
172
173 pub fn get_required_scope(&self) -> Option<&str> {
175 self.required_scope.as_deref()
176 }
177}
178
179#[derive(Error, Debug)]
180#[non_exhaustive]
181pub enum StreamableHttpError<E: std::error::Error + Send + Sync + 'static> {
182 #[error("SSE error: {0}")]
183 Sse(#[from] SseError),
184 #[error("Io error: {0}")]
185 Io(#[from] std::io::Error),
186 #[error("Client error: {0}")]
187 Client(E),
188 #[error("unexpected end of stream")]
189 UnexpectedEndOfStream,
190 #[error("unexpected server response: {0}")]
191 UnexpectedServerResponse(Cow<'static, str>),
192 #[error("Unexpected content type: {0:?}")]
193 UnexpectedContentType(Option<String>),
194 #[error("Server does not support SSE")]
195 ServerDoesNotSupportSse,
196 #[error("Server does not support delete session")]
197 ServerDoesNotSupportDeleteSession,
198 #[error("Tokio join error: {0}")]
199 TokioJoinError(#[from] tokio::task::JoinError),
200 #[error("Deserialize error: {0}")]
201 Deserialize(#[from] serde_json::Error),
202 #[error("Transport channel closed")]
203 TransportChannelClosed,
204 #[error("Missing session id in HTTP response")]
205 MissingSessionIdInResponse,
206 #[cfg(feature = "auth")]
207 #[error("Auth error: {0}")]
208 Auth(#[from] crate::transport::auth::AuthError),
209 #[error("Auth required")]
210 AuthRequired(#[source] AuthRequiredError),
211 #[error("Insufficient scope")]
212 InsufficientScope(#[source] InsufficientScopeError),
213 #[error("Header name '{0}' is reserved and conflicts with default headers")]
214 ReservedHeaderConflict(String),
215 #[error("Session expired (HTTP 404)")]
216 SessionExpired,
217 #[error("Session recovery timed out; the server may have processed the POST")]
219 SessionRecoveryTimeout,
220 #[error("Control POST timed out")]
222 ControlRequestTimeout,
223}
224
225pub(crate) const JSON_RPC_BODY_PREVIEW_LEN: usize = 256;
228
229pub(crate) fn json_rpc_body_preview(body: &[u8]) -> Cow<'static, str> {
231 if body.is_empty() {
232 return Cow::Borrowed("<empty>");
233 }
234 let lossy = String::from_utf8_lossy(body);
235 if lossy.len() <= JSON_RPC_BODY_PREVIEW_LEN {
236 return Cow::Owned(lossy.into_owned());
237 }
238 let mut end = JSON_RPC_BODY_PREVIEW_LEN;
239 while end > 0 && !lossy.is_char_boundary(end) {
240 end -= 1;
241 }
242 Cow::Owned(lossy[..end].to_owned())
243}
244
245pub(crate) fn json_rpc_parse_error_message(err: &serde_json::Error, body: &[u8]) -> String {
248 format!(
249 "could not parse JSON response as ServerJsonRpcMessage: {err}: {}",
250 json_rpc_body_preview(body)
251 )
252}
253
254impl<E: std::error::Error + Send + Sync + 'static> StreamableHttpError<E> {
255 #[cfg(feature = "auth")]
261 pub fn auth_challenge(&self) -> Option<&str> {
262 match self {
263 Self::AuthRequired(error) => Some(&error.www_authenticate_header),
264 Self::InsufficientScope(error) => Some(&error.www_authenticate_header),
265 _ => None,
266 }
267 }
268}
269
270#[derive(Debug, Clone, Error)]
271#[non_exhaustive]
272pub enum StreamableHttpProtocolError {
273 #[error("Missing session id in response")]
274 MissingSessionIdInResponse,
275}
276
277#[expect(
278 clippy::large_enum_variant,
279 reason = "boxing the streaming response would add an allocation to the common response path"
280)]
281#[non_exhaustive]
282pub enum StreamableHttpPostResponse {
283 Accepted,
284 Json(ServerJsonRpcMessage, Option<String>),
285 Sse(BoxedSseStream, Option<String>),
286}
287
288impl std::fmt::Debug for StreamableHttpPostResponse {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 match self {
291 Self::Accepted => write!(f, "Accepted"),
292 Self::Json(arg0, arg1) => f.debug_tuple("Json").field(arg0).field(arg1).finish(),
293 Self::Sse(_, arg1) => f.debug_tuple("Sse").field(arg1).finish(),
294 }
295 }
296}
297
298impl StreamableHttpPostResponse {
299 pub async fn expect_initialized<E>(
300 self,
301 ) -> Result<(ServerJsonRpcMessage, Option<String>), StreamableHttpError<E>>
302 where
303 E: std::error::Error + Send + Sync + 'static,
304 {
305 match self {
306 Self::Json(message, session_id) => Ok((message, session_id)),
307 Self::Sse(mut stream, session_id) => {
308 while let Some(event) = stream.next().await {
309 let event = event?;
310 let payload = event.data.unwrap_or_default();
311 if payload.trim().is_empty() {
312 continue;
313 }
314
315 let message: ServerJsonRpcMessage = serde_json::from_str(&payload)?;
316
317 if matches!(message, ServerJsonRpcMessage::Response(_)) {
318 return Ok((message, session_id));
319 }
320
321 debug!(
322 ?message,
323 "received message before initialize response; continuing to drain stream"
324 );
325 }
326
327 Err(StreamableHttpError::UnexpectedServerResponse(
328 "empty sse stream".into(),
329 ))
330 }
331 _ => Err(StreamableHttpError::UnexpectedServerResponse(
332 "expect initialized, accepted".into(),
333 )),
334 }
335 }
336
337 pub fn expect_json<E>(self) -> Result<ServerJsonRpcMessage, StreamableHttpError<E>>
338 where
339 E: std::error::Error + Send + Sync + 'static,
340 {
341 match self {
342 Self::Json(message, ..) => Ok(message),
343 got => Err(StreamableHttpError::UnexpectedServerResponse(
344 format!("expect json, got {got:?}").into(),
345 )),
346 }
347 }
348
349 pub fn expect_accepted_or_json<E>(self) -> Result<(), StreamableHttpError<E>>
350 where
351 E: std::error::Error + Send + Sync + 'static,
352 {
353 match self {
354 Self::Accepted => Ok(()),
355 Self::Json(..) => Ok(()),
357 got => Err(StreamableHttpError::UnexpectedServerResponse(
358 format!("expect accepted or json, got {got:?}").into(),
359 )),
360 }
361 }
362}
363
364pub(super) fn legacy_discover_response(
376 message: &ClientJsonRpcMessage,
377 session_was_attached: bool,
378 status: StatusCode,
379 body: &str,
380) -> Option<StreamableHttpPostResponse> {
381 if session_was_attached
382 || !status.is_client_error()
383 || matches!(status, StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN)
384 {
385 return None;
386 }
387
388 let ClientJsonRpcMessage::Request(request) = message else {
389 return None;
390 };
391 if !matches!(request.request, ClientRequest::DiscoverRequest(_)) {
392 return None;
393 }
394
395 let error = match serde_json::from_str::<ServerJsonRpcMessage>(body) {
396 Ok(ServerJsonRpcMessage::Error(error)) => error.error,
397 _ => ErrorData::invalid_request(
398 format!("server/discover rejected with HTTP {status}: {body}"),
399 None,
400 ),
401 };
402 Some(StreamableHttpPostResponse::Json(
403 ServerJsonRpcMessage::error(error, Some(request.id.clone())),
404 None,
405 ))
406}
407
408pub trait StreamableHttpClient: Clone + Send + 'static {
419 type Error: std::error::Error + Send + Sync + 'static;
420 fn post_message(
421 &self,
422 uri: Arc<str>,
423 message: ClientJsonRpcMessage,
424 session_id: Option<Arc<str>>,
425 auth_header: Option<String>,
426 custom_headers: HashMap<HeaderName, HeaderValue>,
427 ) -> impl Future<Output = Result<StreamableHttpPostResponse, StreamableHttpError<Self::Error>>>
428 + Send
429 + '_;
430 fn post_message_with_max_sse_event_size(
441 &self,
442 uri: Arc<str>,
443 message: ClientJsonRpcMessage,
444 session_id: Option<Arc<str>>,
445 auth_header: Option<String>,
446 custom_headers: HashMap<HeaderName, HeaderValue>,
447 _max_sse_event_size: usize,
448 ) -> impl Future<Output = Result<StreamableHttpPostResponse, StreamableHttpError<Self::Error>>>
449 + Send
450 + '_ {
451 self.post_message(uri, message, session_id, auth_header, custom_headers)
452 }
453 fn delete_session(
454 &self,
455 uri: Arc<str>,
456 session_id: Arc<str>,
457 auth_header: Option<String>,
458 custom_headers: HashMap<HeaderName, HeaderValue>,
459 ) -> impl Future<Output = Result<(), StreamableHttpError<Self::Error>>> + Send + '_;
460 fn get_stream(
465 &self,
466 uri: Arc<str>,
467 session_id: Option<Arc<str>>,
468 last_event_id: Option<String>,
469 auth_header: Option<String>,
470 custom_headers: HashMap<HeaderName, HeaderValue>,
471 ) -> impl Future<
472 Output = Result<
473 BoxStream<'static, Result<Sse, SseError>>,
474 StreamableHttpError<Self::Error>,
475 >,
476 > + Send
477 + '_;
478 fn get_stream_with_max_sse_event_size(
489 &self,
490 uri: Arc<str>,
491 session_id: Option<Arc<str>>,
492 last_event_id: Option<String>,
493 auth_header: Option<String>,
494 custom_headers: HashMap<HeaderName, HeaderValue>,
495 _max_sse_event_size: usize,
496 ) -> impl Future<
497 Output = Result<
498 BoxStream<'static, Result<Sse, SseError>>,
499 StreamableHttpError<Self::Error>,
500 >,
501 > + Send
502 + '_ {
503 self.get_stream(uri, session_id, last_event_id, auth_header, custom_headers)
504 }
505}
506
507#[non_exhaustive]
508pub struct RetryConfig {
509 pub max_times: Option<usize>,
510 pub min_duration: Duration,
511}
512
513struct StreamableHttpClientReconnect<C> {
514 pub client: C,
515 pub session_id: Option<Arc<str>>,
516 pub uri: Arc<str>,
517 pub auth_header: Option<String>,
518 pub custom_headers: HashMap<HeaderName, HeaderValue>,
519 pub max_sse_event_size: usize,
520}
521
522impl<C: StreamableHttpClient> SseStreamReconnect for StreamableHttpClientReconnect<C> {
523 type Error = StreamableHttpError<C::Error>;
524 type Future = BoxFuture<'static, Result<BoxedSseStream, Self::Error>>;
525 fn retry_connection(&mut self, last_event_id: Option<&str>) -> Self::Future {
526 let client = self.client.clone();
527 let uri = self.uri.clone();
528 let session_id = self.session_id.clone();
529 let auth_header = self.auth_header.clone();
530 let custom_headers = self.custom_headers.clone();
531 let max_sse_event_size = self.max_sse_event_size;
532 let last_event_id = last_event_id.map(|s| s.to_owned());
533 Box::pin(async move {
534 client
535 .get_stream_with_max_sse_event_size(
536 uri,
537 session_id,
538 last_event_id,
539 auth_header,
540 custom_headers,
541 max_sse_event_size,
542 )
543 .await
544 })
545 }
546
547 fn map_fatal_stream_error(&mut self, error: SseError) -> Option<Self::Error> {
548 Some(StreamableHttpError::Sse(error))
549 }
550}
551
552struct SessionCleanupInfo<C> {
554 client: C,
555 uri: Arc<str>,
556 session_id: Arc<str>,
557 auth_header: Option<String>,
558 protocol_headers: HashMap<HeaderName, HeaderValue>,
559}
560
561#[derive(Debug, Clone, Default)]
562#[non_exhaustive]
563pub struct StreamableHttpClientWorker<C: StreamableHttpClient> {
564 pub client: C,
565 pub config: StreamableHttpClientTransportConfig,
566}
567
568struct PostResult<C: StreamableHttpClient> {
569 send_request: WorkerSendRequest<StreamableHttpClientWorker<C>>,
570 response: Option<Result<StreamableHttpPostResponse, StreamableHttpError<C::Error>>>,
572 version: ProtocolVersion,
574}
575
576struct PostSession {
577 id: Option<Arc<str>>,
578 headers: HashMap<HeaderName, HeaderValue>,
579 version: ProtocolVersion,
580 cancellation: CancellationToken,
581}
582
583impl<C: StreamableHttpClient + Default> StreamableHttpClientWorker<C> {
584 pub fn new_simple(url: impl Into<Arc<str>>) -> Self {
585 Self {
586 client: C::default(),
587 config: StreamableHttpClientTransportConfig {
588 uri: url.into(),
589 ..Default::default()
590 },
591 }
592 }
593}
594
595impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
596 pub fn new(client: C, config: StreamableHttpClientTransportConfig) -> Self {
597 Self { client, config }
598 }
599}
600
601impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
602 fn is_ordering_barrier(
604 message: &ClientJsonRpcMessage,
605 negotiated_version: &ProtocolVersion,
606 ) -> bool {
607 match message {
608 ClientJsonRpcMessage::Request(request) => {
609 matches!(
610 &request.request,
611 ClientRequest::InitializeRequest(_) | ClientRequest::DiscoverRequest(_)
612 ) || request
613 .request
614 .get_meta()
615 .protocol_version()
616 .is_some_and(|version| &version != negotiated_version)
617 }
618 ClientJsonRpcMessage::Notification(notification) => matches!(
619 ¬ification.notification,
620 ClientNotification::InitializedNotification(_)
621 ),
622 _ => false,
623 }
624 }
625
626 fn post_request(
627 client: C,
628 config: &StreamableHttpClientTransportConfig,
629 mut send_request: WorkerSendRequest<Self>,
630 session: PostSession,
631 transport_cancellation: CancellationToken,
632 ) -> BoxFuture<'static, PostResult<C>> {
633 let uri = config.uri.clone();
634 let auth_header = config.auth_header.clone();
635 let max_sse_event_size = config.max_sse_event_size;
636 let control_request_timeout = config.control_request_timeout;
637 let is_control = Self::is_control_message(&send_request.message);
638 let cancellation = send_request
639 .cancellation_token()
640 .unwrap_or_else(|| transport_cancellation.child_token());
641 Box::pin(async move {
642 let response = tokio::select! {
643 biased;
644 _ = cancellation.cancelled() => None,
645 _ = send_request.responder.closed() => None,
646 _ = session.cancellation.cancelled() => {
647 Some(Err(StreamableHttpError::SessionRecoveryTimeout))
648 },
649 _ = tokio::time::sleep(control_request_timeout), if is_control => {
650 Some(Err(StreamableHttpError::ControlRequestTimeout))
651 },
652 response = client.post_message_with_max_sse_event_size(
653 uri,
654 send_request.message.clone(),
655 session.id,
656 auth_header,
657 session.headers,
658 max_sse_event_size,
659 ) => Some(response),
660 };
661 PostResult {
662 send_request,
663 response,
664 version: session.version,
665 }
666 })
667 }
668
669 fn cancellation_request_id(message: &ClientJsonRpcMessage) -> Option<&RequestId> {
670 match message {
671 ClientJsonRpcMessage::Notification(notification) => match ¬ification.notification {
672 ClientNotification::CancelledNotification(cancelled) => {
673 cancelled.params.request_id.as_ref()
674 }
675 _ => None,
676 },
677 _ => None,
678 }
679 }
680
681 fn client_request_id(message: &ClientJsonRpcMessage) -> Option<RequestId> {
682 match message {
683 ClientJsonRpcMessage::Request(request) => Some(request.id.clone()),
684 _ => None,
685 }
686 }
687
688 fn server_response_id(message: &ServerJsonRpcMessage) -> Option<&RequestId> {
689 match message {
690 ServerJsonRpcMessage::Response(response) => Some(&response.id),
691 ServerJsonRpcMessage::Error(error) => error.id.as_ref(),
692 _ => None,
693 }
694 }
695
696 fn clear_stream_response_pending(
697 pending_stream_response_ids: &mut HashSet<RequestId>,
698 message: &ServerJsonRpcMessage,
699 ) -> Option<RequestId> {
700 let response_id = Self::server_response_id(message)?;
701 if let Some(id) = pending_stream_response_ids.take(response_id) {
702 return Some(id);
703 }
704 let id = RequestId::Number(response_id.numeric_string_value()?);
705 pending_stream_response_ids.take(&id)
706 }
707
708 async fn drain_queued_stream_messages(
709 sse_worker_rx: &mut tokio::sync::mpsc::Receiver<ServerJsonRpcMessage>,
710 context: &mut super::worker::WorkerContext<Self>,
711 pending_stream_response_ids: &mut HashSet<RequestId>,
712 ) -> Result<(), WorkerQuitReason<StreamableHttpError<C::Error>>> {
713 loop {
714 match sse_worker_rx.try_recv() {
715 Ok(message) => {
716 let _ =
717 Self::clear_stream_response_pending(pending_stream_response_ids, &message);
718 context.send_to_handler(message).await?;
719 }
720 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => return Ok(()),
721 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return Ok(()),
722 }
723 }
724 }
725
726 async fn fail_pending_stream_responses(
727 context: &mut super::worker::WorkerContext<Self>,
728 pending_stream_response_ids: &mut HashSet<RequestId>,
729 ) -> Result<(), WorkerQuitReason<StreamableHttpError<C::Error>>> {
730 if pending_stream_response_ids.is_empty() {
731 return Ok(());
732 }
733
734 let pending_ids = std::mem::take(pending_stream_response_ids);
735 for id in pending_ids {
736 context
737 .send_to_handler(ServerJsonRpcMessage::error(
738 ErrorData::internal_error(
739 "streamable HTTP session was re-initialized before the response arrived",
740 None,
741 ),
742 Some(id),
743 ))
744 .await?;
745 }
746 Ok(())
747 }
748
749 async fn fail_pending_responses_except_retries(
750 context: &mut super::worker::WorkerContext<Self>,
751 pending_stream_response_ids: &mut HashSet<RequestId>,
752 recovery_posts: &VecDeque<WorkerSendRequest<Self>>,
753 ) -> Result<(), WorkerQuitReason<StreamableHttpError<C::Error>>> {
754 let retry_ids: Vec<_> = recovery_posts
756 .iter()
757 .filter_map(|request| Self::client_request_id(&request.message))
758 .filter(|id| pending_stream_response_ids.remove(id))
759 .collect();
760 Self::fail_pending_stream_responses(context, pending_stream_response_ids).await?;
761 pending_stream_response_ids.extend(retry_ids);
762 Ok(())
763 }
764
765 fn fail_recovery_posts(
766 recovery_posts: &mut VecDeque<WorkerSendRequest<Self>>,
767 pending_stream_response_ids: &mut HashSet<RequestId>,
768 error: StreamableHttpError<C::Error>,
769 ) {
770 let mut recovery_error = Some(error);
773 for send_request in recovery_posts.drain(..) {
774 let pending = Self::client_request_id(&send_request.message)
775 .is_none_or(|id| pending_stream_response_ids.remove(&id));
776 let result = if pending {
777 Err(recovery_error
778 .take()
779 .unwrap_or(StreamableHttpError::SessionExpired))
780 } else {
781 Ok(())
782 };
783 let _ = send_request.responder.send(result);
784 }
785 }
786
787 fn reconnecting_sse_to_jsonrpc(
794 stream: BoxedSseStream,
795 client: C,
796 session_id: Option<Arc<str>>,
797 uri: Arc<str>,
798 auth_header: Option<String>,
799 custom_headers: HashMap<HeaderName, HeaderValue>,
800 max_sse_event_size: usize,
801 retry_config: Arc<dyn SseRetryPolicy>,
802 ) -> impl Stream<Item = Result<ServerJsonRpcMessage, StreamableHttpError<C::Error>>> + Send + 'static
803 {
804 SseAutoReconnectStream::new_after_event_id(
805 stream,
806 StreamableHttpClientReconnect {
807 client,
808 session_id,
809 uri,
810 auth_header,
811 custom_headers,
812 max_sse_event_size,
813 },
814 retry_config,
815 )
816 }
817
818 fn response_sse_to_jsonrpc(
823 stream: BoxedSseStream,
824 session_id: Option<Arc<str>>,
825 client: C,
826 uri: Arc<str>,
827 auth_header: Option<String>,
828 custom_headers: HashMap<HeaderName, HeaderValue>,
829 max_sse_event_size: usize,
830 retry_config: Arc<dyn SseRetryPolicy>,
831 ) -> BoxStream<'static, Result<ServerJsonRpcMessage, StreamableHttpError<C::Error>>> {
832 Self::reconnecting_sse_to_jsonrpc(
833 stream,
834 client,
835 session_id,
836 uri,
837 auth_header,
838 custom_headers,
839 max_sse_event_size,
840 retry_config,
841 )
842 .boxed()
843 }
844
845 async fn run_response_stream(
846 mut sse_stream: BoxStream<
847 'static,
848 Result<ServerJsonRpcMessage, StreamableHttpError<C::Error>>,
849 >,
850 sse_worker_tx: tokio::sync::mpsc::Sender<ServerJsonRpcMessage>,
851 origin: InboundStreamOrigin,
852 request_ct: CancellationToken,
853 stream_ct: CancellationToken,
854 uses_modern_http: bool,
855 ) -> Result<(), StreamableHttpError<C::Error>> {
856 tokio::select! {
857 biased;
858 _ = request_ct.cancelled(), if !uses_modern_http => {
859 stream_ct.cancelled().await;
862 Ok(())
863 }
864 result = Self::execute_sse_stream(
865 sse_stream.as_mut(), sse_worker_tx, origin, true, stream_ct.clone(),
866 ) => result,
867 }
868 }
869
870 async fn execute_sse_stream(
871 sse_stream: impl Stream<Item = Result<ServerJsonRpcMessage, StreamableHttpError<C::Error>>>
872 + Send,
873 sse_worker_tx: tokio::sync::mpsc::Sender<ServerJsonRpcMessage>,
874 origin: InboundStreamOrigin,
875 close_on_response: bool,
876 ct: CancellationToken,
877 ) -> Result<(), StreamableHttpError<C::Error>> {
878 let mut sse_stream = std::pin::pin!(sse_stream);
879 loop {
880 let message = tokio::select! {
881 event = sse_stream.next() => {
882 event
883 }
884 _ = ct.cancelled() => {
885 tracing::debug!("cancelled");
886 break;
887 }
888 };
889 let Some(mut message) = message.transpose()? else {
890 break;
891 };
892 if let ServerJsonRpcMessage::Request(request) = &mut message {
895 request.request.extensions_mut().insert(origin.clone());
896 }
897 let is_response = matches!(
898 message,
899 ServerJsonRpcMessage::Response(_) | ServerJsonRpcMessage::Error(_)
900 );
901 let yield_result = sse_worker_tx.send(message).await;
902 if yield_result.is_err() {
903 tracing::trace!("streamable http transport worker dropped, exiting");
904 break;
905 }
906 if close_on_response && is_response {
907 tracing::debug!("got response, draining sse stream for connection reuse");
908 let _ = tokio::time::timeout(std::time::Duration::from_millis(50), async {
911 while sse_stream.next().await.is_some() {}
912 })
913 .await;
914 break;
915 }
916 }
917 Ok(())
918 }
919
920 fn spawn_common_stream(
921 streams: &mut tokio::task::JoinSet<SseTaskResult<C::Error>>,
922 client: C,
923 session_id: Arc<str>,
924 config: &StreamableHttpClientTransportConfig,
925 protocol_headers: HashMap<HeaderName, HeaderValue>,
926 sse_worker_tx: tokio::sync::mpsc::Sender<ServerJsonRpcMessage>,
927 transport_task_ct: CancellationToken,
928 ) {
929 let uri = config.uri.clone();
930 let auth_header = config.auth_header.clone();
931 let retry_config = config.retry_config.clone();
932 let reconnect_uri = config.uri.clone();
933 let reconnect_auth_header = config.auth_header.clone();
934 let max_sse_event_size = config.max_sse_event_size;
935
936 streams.spawn(async move {
937 let result = match client
938 .get_stream_with_max_sse_event_size(
939 uri,
940 Some(session_id.clone()),
941 None,
942 auth_header,
943 protocol_headers.clone(),
944 max_sse_event_size,
945 )
946 .await
947 {
948 Ok(stream) => {
949 let sse_stream = SseAutoReconnectStream::new(
950 stream,
951 StreamableHttpClientReconnect {
952 client,
953 session_id: Some(session_id),
954 uri: reconnect_uri,
955 auth_header: reconnect_auth_header,
956 custom_headers: protocol_headers,
957 max_sse_event_size,
958 },
959 retry_config,
960 );
961 Self::execute_sse_stream(
962 sse_stream,
963 sse_worker_tx,
964 InboundStreamOrigin::Unassociated,
965 false,
966 transport_task_ct.child_token(),
967 )
968 .await
969 }
970 Err(StreamableHttpError::ServerDoesNotSupportSse) => {
971 tracing::debug!("server doesn't support sse, skip common stream");
972 Ok(())
973 }
974 Err(error) => {
975 tracing::error!("fail to get common stream: {error}");
976 Err(error)
977 }
978 };
979 (None, result)
980 });
981 }
982
983 async fn perform_reinitialization(
993 client: C,
994 saved_init_request: ClientJsonRpcMessage,
995 uri: Arc<str>,
996 auth_header: Option<String>,
997 custom_headers: HashMap<HeaderName, HeaderValue>,
998 max_sse_event_size: usize,
999 ) -> Result<
1000 (
1001 Option<Arc<str>>,
1002 ProtocolVersion,
1003 HashMap<HeaderName, HeaderValue>,
1004 ),
1005 StreamableHttpError<C::Error>,
1006 > {
1007 let (init_msg, new_session_id_str) = client
1008 .post_message_with_max_sse_event_size(
1009 uri.clone(),
1010 saved_init_request,
1011 None,
1012 auth_header.clone(),
1013 custom_headers.clone(),
1014 max_sse_event_size,
1015 )
1016 .await?
1017 .expect_initialized::<C::Error>()
1018 .await?;
1019
1020 let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
1021
1022 let (negotiated_version, new_protocol_headers) =
1023 negotiate_version_headers(&init_msg, custom_headers);
1024
1025 let initialized_notification = ClientJsonRpcMessage::notification(
1026 ClientNotification::InitializedNotification(InitializedNotification {
1027 method: Default::default(),
1028 extensions: Default::default(),
1029 }),
1030 );
1031 let initialized_headers = build_request_headers(
1033 &new_protocol_headers,
1034 &initialized_notification,
1035 &HashMap::new(),
1036 &negotiated_version,
1037 );
1038 client
1039 .post_message_with_max_sse_event_size(
1040 uri,
1041 initialized_notification,
1042 new_session_id.clone(),
1043 auth_header,
1044 initialized_headers,
1045 max_sse_event_size,
1046 )
1047 .await?
1048 .expect_accepted_or_json::<C::Error>()?;
1049
1050 Ok((new_session_id, negotiated_version, new_protocol_headers))
1051 }
1052}
1053
1054impl<C: StreamableHttpClient> Worker for StreamableHttpClientWorker<C> {
1055 type Role = RoleClient;
1056 type Error = StreamableHttpError<C::Error>;
1057 fn is_control_message(message: &ClientJsonRpcMessage) -> bool {
1058 match message {
1059 ClientJsonRpcMessage::Response(_) | ClientJsonRpcMessage::Error(_) => true,
1060 ClientJsonRpcMessage::Notification(notification) => matches!(
1061 notification.notification,
1062 ClientNotification::CancelledNotification(_)
1063 ),
1064 ClientJsonRpcMessage::Request(_) => false,
1065 }
1066 }
1067 fn supports_request_cancellation() -> bool {
1068 true
1069 }
1070 fn err_closed() -> Self::Error {
1071 StreamableHttpError::TransportChannelClosed
1072 }
1073 fn err_join(e: tokio::task::JoinError) -> Self::Error {
1074 StreamableHttpError::TokioJoinError(e)
1075 }
1076 fn config(&self) -> super::worker::WorkerConfig {
1077 super::worker::WorkerConfig {
1078 name: Some("StreamableHttpClientWorker".into()),
1079 channel_buffer_capacity: self.config.channel_buffer_capacity,
1080 }
1081 }
1082 async fn run(
1083 self,
1084 mut context: super::worker::WorkerContext<Self>,
1085 ) -> Result<(), WorkerQuitReason<Self::Error>> {
1086 let channel_buffer_capacity = self.config.channel_buffer_capacity;
1087 let (sse_worker_tx, mut sse_worker_rx) =
1088 tokio::sync::mpsc::channel::<ServerJsonRpcMessage>(channel_buffer_capacity);
1089 let config = self.config.clone();
1090 let transport_task_ct = context.cancellation_token.clone();
1091 let _drop_guard = transport_task_ct.clone().drop_guard();
1092 let WorkerSendRequest {
1093 responder,
1094 message: startup_request,
1095 ..
1096 } = context.recv_from_handler().await?;
1097 let is_legacy_startup = matches!(
1098 &startup_request,
1099 ClientJsonRpcMessage::Request(request)
1100 if matches!(&request.request, ClientRequest::InitializeRequest(_))
1101 );
1102 let mut saved_init_request = is_legacy_startup.then(|| startup_request.clone());
1103 let empty_tool_cache = HashMap::new();
1104 let (bootstrap_version, bootstrap_headers) = if is_legacy_startup {
1105 (ProtocolVersion::default(), config.custom_headers.clone())
1106 } else {
1107 request_version_headers(
1108 &config.custom_headers,
1109 &startup_request,
1110 &ProtocolVersion::default(),
1111 &empty_tool_cache,
1112 )
1113 };
1114 let (message, session_id) = match self
1115 .client
1116 .post_message_with_max_sse_event_size(
1117 config.uri.clone(),
1118 startup_request,
1119 None,
1120 config.auth_header.clone(),
1121 bootstrap_headers.clone(),
1122 config.max_sse_event_size,
1123 )
1124 .await
1125 {
1126 Ok(res) => {
1127 let _ = responder.send(Ok(()));
1128 res.expect_initialized::<C::Error>().await.map_err(
1129 WorkerQuitReason::fatal_context("process initialize response"),
1130 )?
1131 }
1132 Err(err) => {
1133 let msg = format!("{:?}", err);
1134 let _ = responder.send(Err(err));
1135 return Err(WorkerQuitReason::fatal(
1136 StreamableHttpError::TransportChannelClosed,
1137 msg,
1138 ));
1139 }
1140 };
1141 let mut uses_modern_http = !is_legacy_startup;
1142 let mut session_id: Option<Arc<str>> = if uses_modern_http {
1143 None
1144 } else if let Some(session_id) = session_id {
1145 Some(session_id.into())
1146 } else {
1147 if !self.config.allow_stateless {
1148 return Err(WorkerQuitReason::fatal(
1149 StreamableHttpError::<C::Error>::MissingSessionIdInResponse,
1150 "process initialize response",
1151 ));
1152 }
1153 None
1154 };
1155 let (mut negotiated_version, mut protocol_headers) = if is_legacy_startup {
1156 negotiate_version_headers(&message, config.custom_headers.clone())
1157 } else {
1158 (bootstrap_version, bootstrap_headers)
1159 };
1160 let mut tool_header_cache: HashMap<String, Arc<JsonObject>> = HashMap::new();
1163
1164 let mut session_cleanup_info = session_id.as_ref().map(|sid| SessionCleanupInfo {
1166 client: self.client.clone(),
1167 uri: config.uri.clone(),
1168 session_id: sid.clone(),
1169 auth_header: config.auth_header.clone(),
1170 protocol_headers: protocol_headers.clone(),
1171 });
1172
1173 context.send_to_handler(message).await?;
1174 if is_legacy_startup {
1175 let initialized_notification = context.recv_from_handler().await?;
1176 let initialized_headers = build_request_headers(
1177 &protocol_headers,
1178 &initialized_notification.message,
1179 &tool_header_cache,
1180 &negotiated_version,
1181 );
1182 self.client
1183 .post_message_with_max_sse_event_size(
1184 config.uri.clone(),
1185 initialized_notification.message,
1186 session_id.clone(),
1187 config.auth_header.clone(),
1188 initialized_headers,
1189 config.max_sse_event_size,
1190 )
1191 .await
1192 .map_err(WorkerQuitReason::fatal_context(
1193 "send initialized notification",
1194 ))?
1195 .expect_accepted_or_json::<C::Error>()
1196 .map_err(WorkerQuitReason::fatal_context(
1197 "process initialized notification response",
1198 ))?;
1199 let _ = initialized_notification.responder.send(Ok(()));
1200 }
1201 #[expect(
1202 clippy::large_enum_variant,
1203 reason = "the event is short-lived and boxing would add allocation in the event loop"
1204 )]
1205 enum Event<C: StreamableHttpClient> {
1206 ClientMessage(WorkerSendRequest<StreamableHttpClientWorker<C>>),
1207 ControlMessage(WorkerSendRequest<StreamableHttpClientWorker<C>>),
1208 StartPost(WorkerSendRequest<StreamableHttpClientWorker<C>>),
1209 PostResult(PostResult<C>),
1210 RecoveryTimeout,
1211 ServerMessage(ServerJsonRpcMessage),
1212 StreamResult {
1213 request_id: Option<RequestId>,
1214 result: Result<(), StreamableHttpError<C::Error>>,
1215 },
1216 }
1217 let mut streams = tokio::task::JoinSet::new();
1218 let mut pending_stream_response_ids = HashSet::new();
1219 let mut request_stream_cancellations =
1220 HashMap::<RequestId, Arc<RequestCancellationRegistration>>::new();
1221 let mut posts = FuturesUnordered::<BoxFuture<'static, PostResult<C>>>::new();
1222 let mut control_posts = FuturesUnordered::<BoxFuture<'static, PostResult<C>>>::new();
1223 let mut session_cancellation = CancellationToken::new();
1224 let mut pending_message: Option<WorkerSendRequest<Self>> = None;
1225 let mut recovery_posts = VecDeque::<WorkerSendRequest<Self>>::new();
1226 let mut recovery_deadline: Option<tokio::time::Instant> = None;
1227 let mut retrying_recovery = false;
1228 let mut barrier_in_flight = false;
1229 let max_concurrent_requests = config.max_concurrent_requests.max(1);
1230 let mut awaiting_fallback_initialized = false;
1231 if let Some(session_id) = &session_id {
1232 Self::spawn_common_stream(
1233 &mut streams,
1234 self.client.clone(),
1235 session_id.clone(),
1236 &config,
1237 protocol_headers.clone(),
1238 sse_worker_tx.clone(),
1239 transport_task_ct.clone(),
1240 );
1241 }
1242 let loop_result: Result<(), WorkerQuitReason<Self::Error>> = 'main_loop: loop {
1245 if retrying_recovery && recovery_posts.is_empty() && posts.is_empty() {
1246 retrying_recovery = false;
1247 }
1248 if !retrying_recovery
1249 && !recovery_posts.is_empty()
1250 && posts.is_empty()
1251 && control_posts.is_empty()
1252 {
1253 session_cancellation.cancel();
1256 recovery_deadline = None;
1257 let recovery = tokio::select! {
1258 _ = transport_task_ct.cancelled() => {
1259 break 'main_loop Err(WorkerQuitReason::Cancelled);
1260 }
1261 result = tokio::time::timeout(
1262 config.session_recovery_timeout,
1263 Self::perform_reinitialization(
1264 self.client.clone(),
1265 saved_init_request.clone().expect("session recovery requires an initialize request"),
1266 config.uri.clone(),
1267 config.auth_header.clone(),
1268 config.custom_headers.clone(),
1269 config.max_sse_event_size,
1270 ),
1271 ) => result.unwrap_or(Err(StreamableHttpError::SessionRecoveryTimeout)),
1272 };
1273 match recovery {
1274 Ok((new_session_id, new_version, new_headers)) => {
1275 streams.abort_all();
1276 while streams.join_next().await.is_some() {}
1277 request_stream_cancellations.clear();
1278 Self::drain_queued_stream_messages(
1279 &mut sse_worker_rx,
1280 &mut context,
1281 &mut pending_stream_response_ids,
1282 )
1283 .await?;
1284 Self::fail_pending_responses_except_retries(
1285 &mut context,
1286 &mut pending_stream_response_ids,
1287 &recovery_posts,
1288 )
1289 .await?;
1290 session_id = new_session_id;
1291 negotiated_version = new_version;
1292 protocol_headers = new_headers;
1293 session_cleanup_info = session_id.as_ref().map(|sid| SessionCleanupInfo {
1294 client: self.client.clone(),
1295 uri: config.uri.clone(),
1296 session_id: sid.clone(),
1297 auth_header: config.auth_header.clone(),
1298 protocol_headers: protocol_headers.clone(),
1299 });
1300 context.advance_control_generation();
1302 session_cancellation = CancellationToken::new();
1303 if let Some(session_id) = &session_id {
1304 Self::spawn_common_stream(
1305 &mut streams,
1306 self.client.clone(),
1307 session_id.clone(),
1308 &config,
1309 protocol_headers.clone(),
1310 sse_worker_tx.clone(),
1311 transport_task_ct.clone(),
1312 );
1313 }
1314 retrying_recovery = true;
1315 }
1316 Err(error) => {
1317 session_cancellation = CancellationToken::new();
1318 Self::fail_recovery_posts(
1319 &mut recovery_posts,
1320 &mut pending_stream_response_ids,
1321 error,
1322 );
1323 }
1324 }
1325 continue;
1326 }
1327
1328 let has_post_capacity = posts.len() < max_concurrent_requests;
1329 let may_start = (retrying_recovery || recovery_posts.is_empty())
1330 && !barrier_in_flight
1331 && has_post_capacity;
1332 let may_receive = may_start && pending_message.is_none() && !retrying_recovery;
1333 let queued = if retrying_recovery {
1334 recovery_posts.front_mut()
1335 } else {
1336 pending_message.as_mut()
1337 };
1338 let has_queued = queued.is_some();
1339 let can_process_queued = queued.as_ref().is_some_and(|request| {
1340 let retry_completed = retrying_recovery
1341 && Self::client_request_id(&request.message)
1342 .is_some_and(|id| !pending_stream_response_ids.contains(&id));
1343 let ordering_satisfied =
1344 !Self::is_ordering_barrier(&request.message, &negotiated_version)
1345 || (posts.is_empty() && control_posts.is_empty());
1346 retry_completed || (may_start && ordering_satisfied)
1347 });
1348 let event = tokio::select! {
1349 _ = async {
1350 if can_process_queued {
1351 return;
1352 }
1353 let request = queued.expect("a POST is queued");
1354 let cancellation = request.cancellation_token().unwrap_or_default();
1355 tokio::select! {
1356 _ = request.responder.closed() => {}
1357 _ = cancellation.cancelled() => {}
1358 }
1359 }, if has_queued => {
1360 let request = if retrying_recovery {
1361 recovery_posts.pop_front()
1362 } else {
1363 pending_message.take()
1364 };
1365 Event::StartPost(request.expect("a POST is ready to start"))
1366 }
1367 _ = transport_task_ct.cancelled() => {
1368 tracing::debug!("cancelled");
1369 break 'main_loop Err(WorkerQuitReason::Cancelled);
1370 }
1371 message = context.from_handler_rx.recv(), if may_receive => {
1372 match message {
1373 Some(msg) => Event::ClientMessage(msg),
1374 None => break 'main_loop Err(WorkerQuitReason::HandlerTerminated),
1375 }
1376 },
1377 message = context.control_from_handler_rx.recv(),
1378 if control_posts.is_empty() && !session_cancellation.is_cancelled() => {
1379 match message {
1380 Some(msg) => Event::ControlMessage(msg),
1381 None => break 'main_loop Err(WorkerQuitReason::HandlerTerminated),
1382 }
1383 },
1384 Some(result) = posts.next(), if !posts.is_empty() => {
1385 Event::PostResult(result)
1386 },
1387 Some(result) = control_posts.next(), if !control_posts.is_empty() => {
1388 Event::PostResult(result)
1389 },
1390 _ = async {
1391 if let Some(deadline) = recovery_deadline {
1392 tokio::time::sleep_until(deadline).await;
1393 }
1394 }, if recovery_deadline.is_some() => Event::RecoveryTimeout,
1395 message = sse_worker_rx.recv() => {
1396 let Some(message) = message else {
1397 tracing::trace!("transport dropped, exiting");
1398 break 'main_loop Err(WorkerQuitReason::HandlerTerminated);
1399 };
1400 Event::ServerMessage(message)
1401 },
1402 terminated_stream = streams.join_next(), if !streams.is_empty() => {
1403 match terminated_stream {
1404 Some(Ok((request_id, result))) => {
1405 Event::StreamResult { request_id, result }
1406 }
1407 Some(Err(error)) => Event::StreamResult {
1408 request_id: None,
1409 result: Err(StreamableHttpError::TokioJoinError(error)),
1410 },
1411 None => continue,
1412 }
1413 }
1414 };
1415 match event {
1416 Event::ClientMessage(send_request) => {
1417 pending_message = Some(send_request);
1418 }
1419 Event::ControlMessage(send_request) => {
1420 if send_request.responder.is_closed() {
1421 continue;
1422 }
1423 let cancellation_request_id =
1424 Self::cancellation_request_id(&send_request.message);
1425 let stale = send_request.control_generation() != context.control_generation();
1426 if stale {
1427 let result = match cancellation_request_id {
1429 Some(_) => Ok(()),
1430 None => Err(StreamableHttpError::SessionExpired),
1431 };
1432 let _ = send_request.responder.send(result);
1433 continue;
1434 }
1435 if let Some(request_id) = cancellation_request_id {
1436 drop(request_stream_cancellations.remove(request_id));
1437 pending_stream_response_ids.remove(request_id);
1438 if uses_modern_http {
1439 let _ = send_request.responder.send(Ok(()));
1440 continue;
1441 }
1442 }
1443 let (version, headers) = request_version_headers(
1444 &protocol_headers,
1445 &send_request.message,
1446 &negotiated_version,
1447 &tool_header_cache,
1448 );
1449 control_posts.push(Self::post_request(
1450 self.client.clone(),
1451 &config,
1452 send_request,
1453 PostSession {
1454 id: session_id.clone(),
1455 headers,
1456 version,
1457 cancellation: session_cancellation.clone(),
1458 },
1459 transport_task_ct.clone(),
1460 ));
1461 }
1462 Event::RecoveryTimeout => {
1463 recovery_deadline = None;
1464 session_cancellation.cancel();
1465 tracing::warn!("old-session POSTs did not finish before the recovery deadline");
1466 }
1467 Event::StartPost(send_request) => {
1468 let request_id = Self::client_request_id(&send_request.message);
1469 let send_cancelled = send_request.responder.is_closed()
1470 || send_request
1471 .cancellation_token()
1472 .is_some_and(|token| token.is_cancelled());
1473 let retry_completed = retrying_recovery
1474 && request_id
1475 .as_ref()
1476 .is_some_and(|id| !pending_stream_response_ids.contains(id));
1477 if send_cancelled || retry_completed {
1478 if retrying_recovery && let Some(id) = &request_id {
1479 pending_stream_response_ids.remove(id);
1480 }
1481 let _ = send_request.responder.send(Ok(()));
1482 continue;
1483 }
1484 let message = &send_request.message;
1485 let is_fallback_initialize = saved_init_request.is_none()
1486 && matches!(
1487 message,
1488 ClientJsonRpcMessage::Request(request)
1489 if matches!(
1490 &request.request,
1491 ClientRequest::InitializeRequest(_)
1492 )
1493 );
1494 if is_fallback_initialize {
1495 saved_init_request = Some(message.clone());
1496 let WorkerSendRequest {
1497 message, responder, ..
1498 } = send_request;
1499 debug_assert!(
1503 session_id.is_none()
1504 && session_cleanup_info.is_none()
1505 && streams.is_empty(),
1506 "discover bootstrap must not create session state"
1507 );
1508 uses_modern_http = false;
1509
1510 let response = self
1511 .client
1512 .post_message_with_max_sse_event_size(
1513 config.uri.clone(),
1514 message,
1515 None,
1516 config.auth_header.clone(),
1517 config.custom_headers.clone(),
1518 config.max_sse_event_size,
1519 )
1520 .await;
1521 let response = match response {
1522 Ok(response) => {
1523 let _ = responder.send(Ok(()));
1524 response
1525 }
1526 Err(error) => {
1527 let _ = responder.send(Err(error));
1528 continue;
1529 }
1530 };
1531 let (initialize_response, new_session_id) = response
1532 .expect_initialized::<C::Error>()
1533 .await
1534 .map_err(WorkerQuitReason::fatal_context(
1535 "process fallback initialize response",
1536 ))?;
1537 session_id = new_session_id.map(Arc::from);
1538 if session_id.is_none() && !config.allow_stateless {
1539 return Err(WorkerQuitReason::fatal(
1540 StreamableHttpError::<C::Error>::MissingSessionIdInResponse,
1541 "process fallback initialize response",
1542 ));
1543 }
1544 (negotiated_version, protocol_headers) = negotiate_version_headers(
1545 &initialize_response,
1546 config.custom_headers.clone(),
1547 );
1548 session_cleanup_info =
1549 session_id.as_ref().map(|session_id| SessionCleanupInfo {
1550 client: self.client.clone(),
1551 uri: config.uri.clone(),
1552 session_id: session_id.clone(),
1553 auth_header: config.auth_header.clone(),
1554 protocol_headers: protocol_headers.clone(),
1555 });
1556 context.send_to_handler(initialize_response).await?;
1557 awaiting_fallback_initialized = true;
1558 continue;
1559 }
1560
1561 let barrier = Self::is_ordering_barrier(message, &negotiated_version);
1562 debug_assert!(!barrier || (posts.is_empty() && control_posts.is_empty()));
1563 let inline_version = match message {
1564 ClientJsonRpcMessage::Request(request) => {
1565 request.request.get_meta().protocol_version()
1566 }
1567 _ => None,
1568 };
1569 let (request_version, request_headers) = request_version_headers(
1570 &protocol_headers,
1571 message,
1572 &negotiated_version,
1573 &tool_header_cache,
1574 );
1575 if inline_version.is_some() {
1576 negotiated_version = request_version.clone();
1577 if let Ok(value) = HeaderValue::from_str(request_version.as_str()) {
1578 protocol_headers
1579 .insert(HeaderName::from_static("mcp-protocol-version"), value);
1580 }
1581 if let Some(cleanup) = &mut session_cleanup_info {
1582 cleanup.protocol_headers = protocol_headers.clone();
1583 }
1584 }
1585 barrier_in_flight = barrier;
1586 if let Some(request_id) = request_id {
1588 pending_stream_response_ids.insert(request_id);
1589 }
1590 posts.push(Self::post_request(
1591 self.client.clone(),
1592 &config,
1593 send_request,
1594 PostSession {
1595 id: session_id.clone(),
1596 headers: request_headers,
1597 version: request_version,
1598 cancellation: session_cancellation.clone(),
1599 },
1600 transport_task_ct.clone(),
1601 ));
1602 }
1603 Event::PostResult(PostResult {
1604 send_request,
1605 response,
1606 version,
1607 }) => {
1608 let is_control = Self::is_control_message(&send_request.message);
1609 if !is_control {
1610 barrier_in_flight = false;
1612 }
1613 let request_id = Self::client_request_id(&send_request.message);
1614 if request_id
1615 .as_ref()
1616 .is_some_and(|id| !pending_stream_response_ids.contains(id))
1617 {
1618 let _ = send_request.responder.send(Ok(()));
1620 continue;
1621 }
1622 let will_retry =
1623 matches!(&response, Some(Err(StreamableHttpError::SessionExpired)))
1624 && !is_control
1625 && !retrying_recovery
1626 && config.reinit_on_expired_session
1627 && saved_init_request.is_some();
1628 let awaits_stream_response = matches!(
1629 &response,
1630 Some(Ok(StreamableHttpPostResponse::Accepted
1631 | StreamableHttpPostResponse::Sse(..)))
1632 );
1633 if !will_retry
1634 && !awaits_stream_response
1635 && let Some(id) = &request_id
1636 {
1637 pending_stream_response_ids.remove(id);
1638 }
1639 let Some(response) = response else {
1640 let _ = send_request.responder.send(Ok(()));
1641 continue;
1642 };
1643 if will_retry {
1644 if recovery_posts.is_empty() {
1645 recovery_deadline =
1646 Some(tokio::time::Instant::now() + config.session_recovery_timeout);
1647 }
1648 recovery_posts.push_back(send_request);
1649 continue;
1650 }
1651 let request_cancellation = send_request.cancellation_registration();
1652 let WorkerSendRequest {
1653 message, responder, ..
1654 } = send_request;
1655 let is_initialized_notification = matches!(
1656 &message,
1657 ClientJsonRpcMessage::Notification(notification)
1658 if matches!(
1659 ¬ification.notification,
1660 ClientNotification::InitializedNotification(_)
1661 )
1662 );
1663 let send_result = match response {
1664 Err(e) => Err(e),
1665 Ok(StreamableHttpPostResponse::Accepted) => {
1666 tracing::trace!("client message accepted");
1667 Ok(())
1668 }
1669 Ok(StreamableHttpPostResponse::Json(mut message, ..)) => {
1670 cache_tools_from_response(
1671 &mut tool_header_cache,
1672 &mut message,
1673 &version,
1674 );
1675 context.send_to_handler(message).await?;
1676 Ok(())
1677 }
1678 Ok(StreamableHttpPostResponse::Sse(stream, ..)) => {
1679 let stream_request_id = request_id;
1680 let sse_stream = Self::response_sse_to_jsonrpc(
1681 stream,
1682 session_id.clone(),
1683 self.client.clone(),
1684 config.uri.clone(),
1685 config.auth_header.clone(),
1686 protocol_headers.clone(),
1687 config.max_sse_event_size,
1688 self.config.retry_config.clone(),
1689 );
1690 let request_ct = request_cancellation
1691 .as_ref()
1692 .map(|registration| registration.token())
1693 .unwrap_or_else(|| transport_task_ct.child_token());
1694 let stream_ct = if uses_modern_http {
1696 request_ct.clone()
1697 } else {
1698 request_cancellation
1699 .as_ref()
1700 .map(|registration| registration.lifetime_token())
1701 .unwrap_or_else(|| request_ct.clone())
1702 };
1703 if let (Some(request_id), Some(registration)) =
1704 (stream_request_id.as_ref(), request_cancellation)
1705 {
1706 request_stream_cancellations
1707 .insert(request_id.clone(), registration);
1708 }
1709 let stream_tx = sse_worker_tx.clone();
1710 let origin = match &stream_request_id {
1711 Some(id) => InboundStreamOrigin::OutboundRequest(id.clone()),
1712 None => InboundStreamOrigin::Unassociated,
1713 };
1714 streams.spawn(async move {
1715 let result = Self::run_response_stream(
1716 sse_stream,
1717 stream_tx,
1718 origin,
1719 request_ct,
1720 stream_ct,
1721 uses_modern_http,
1722 )
1723 .await;
1724 (stream_request_id, result)
1725 });
1726 tracing::trace!("got new sse stream");
1727 Ok(())
1728 }
1729 };
1730 if send_result.is_ok()
1731 && awaiting_fallback_initialized
1732 && is_initialized_notification
1733 {
1734 if let Some(session_id) = &session_id {
1735 Self::spawn_common_stream(
1736 &mut streams,
1737 self.client.clone(),
1738 session_id.clone(),
1739 &config,
1740 protocol_headers.clone(),
1741 sse_worker_tx.clone(),
1742 transport_task_ct.clone(),
1743 );
1744 }
1745 awaiting_fallback_initialized = false;
1746 }
1747 let _ = responder.send(send_result);
1748 }
1749 Event::ServerMessage(mut json_rpc_message) => {
1750 if let Some(request_id) = Self::clear_stream_response_pending(
1752 &mut pending_stream_response_ids,
1753 &json_rpc_message,
1754 ) {
1755 drop(request_stream_cancellations.remove(&request_id));
1756 }
1757 cache_tools_from_response(
1758 &mut tool_header_cache,
1759 &mut json_rpc_message,
1760 &negotiated_version,
1761 );
1762 if let Err(e) = context.send_to_handler(json_rpc_message).await {
1764 break 'main_loop Err(e);
1765 }
1766 }
1767 Event::StreamResult { request_id, result } => {
1768 if let Some(request_id) = request_id {
1769 Self::drain_queued_stream_messages(
1770 &mut sse_worker_rx,
1771 &mut context,
1772 &mut pending_stream_response_ids,
1773 )
1774 .await?;
1775 let cancelled = request_stream_cancellations
1776 .remove(&request_id)
1777 .is_some_and(|registration| registration.token().is_cancelled());
1778 if pending_stream_response_ids.remove(&request_id) && !cancelled {
1779 context
1780 .send_to_handler(ServerJsonRpcMessage::error(
1781 ErrorData::transport_closed(
1782 "streamable HTTP response stream closed before its final response",
1783 ),
1784 Some(request_id),
1785 ))
1786 .await?;
1787 }
1788 }
1789 if result.is_err() {
1790 tracing::warn!(
1791 "sse client event stream terminated with error: {:?}",
1792 result
1793 );
1794 }
1795 }
1796 }
1797 };
1798
1799 transport_task_ct.cancel();
1801 drop(posts);
1802 drop(control_posts);
1803 drop(pending_message);
1804 drop(recovery_posts);
1805 streams.abort_all();
1806
1807 if let Some(cleanup) = session_cleanup_info {
1810 let cleanup_session_id = cleanup.session_id.clone();
1811 match tokio::time::timeout(
1812 SESSION_CLEANUP_TIMEOUT,
1813 cleanup.client.delete_session(
1814 cleanup.uri,
1815 cleanup.session_id,
1816 cleanup.auth_header,
1817 cleanup.protocol_headers,
1818 ),
1819 )
1820 .await
1821 {
1822 Ok(Ok(_)) => {
1823 tracing::info!(
1824 session_id = cleanup_session_id.as_ref(),
1825 "delete session success"
1826 )
1827 }
1828 Ok(Err(StreamableHttpError::ServerDoesNotSupportDeleteSession)) => {
1829 tracing::info!(
1830 session_id = cleanup_session_id.as_ref(),
1831 "server doesn't support delete session"
1832 )
1833 }
1834 Ok(Err(e)) => {
1835 tracing::error!(
1836 session_id = cleanup_session_id.as_ref(),
1837 "fail to delete session: {e}"
1838 );
1839 }
1840 Err(_elapsed) => {
1841 tracing::warn!(
1842 session_id = cleanup_session_id.as_ref(),
1843 "session cleanup timed out after {:?}",
1844 SESSION_CLEANUP_TIMEOUT
1845 );
1846 }
1847 }
1848 }
1849
1850 loop_result
1851 }
1852}
1853
1854pub type StreamableHttpClientTransport<C> = WorkerTransport<StreamableHttpClientWorker<C>>;
1947
1948impl<C: StreamableHttpClient> StreamableHttpClientTransport<C> {
1949 pub fn with_client(client: C, config: StreamableHttpClientTransportConfig) -> Self {
2029 let worker = StreamableHttpClientWorker::new(client, config);
2030 WorkerTransport::spawn(worker)
2031 }
2032}
2033#[derive(Debug, Clone)]
2034#[non_exhaustive]
2035pub struct StreamableHttpClientTransportConfig {
2036 pub uri: Arc<str>,
2037 pub retry_config: Arc<dyn SseRetryPolicy>,
2038 pub channel_buffer_capacity: usize,
2039 pub max_concurrent_requests: usize,
2044 pub control_request_timeout: Duration,
2047 pub session_recovery_timeout: Duration,
2052 pub allow_stateless: bool,
2054 pub auth_header: Option<String>,
2056 pub custom_headers: HashMap<HeaderName, HeaderValue>,
2058 pub max_sse_event_size: usize,
2064 pub reinit_on_expired_session: bool,
2076}
2077
2078impl StreamableHttpClientTransportConfig {
2079 pub fn with_uri(uri: impl Into<Arc<str>>) -> Self {
2080 Self {
2081 uri: uri.into(),
2082 ..Default::default()
2083 }
2084 }
2085
2086 pub fn max_concurrent_requests(mut self, limit: usize) -> Self {
2088 self.max_concurrent_requests = limit.max(1);
2089 self
2090 }
2091
2092 pub fn control_request_timeout(mut self, timeout: Duration) -> Self {
2094 self.control_request_timeout = timeout;
2095 self
2096 }
2097
2098 pub fn session_recovery_timeout(mut self, timeout: Duration) -> Self {
2100 self.session_recovery_timeout = timeout;
2101 self
2102 }
2103
2104 pub fn auth_header<T: Into<String>>(mut self, value: T) -> Self {
2110 self.auth_header = Some(value.into());
2112 self
2113 }
2114
2115 pub fn custom_headers(mut self, custom_headers: HashMap<HeaderName, HeaderValue>) -> Self {
2138 self.custom_headers = custom_headers;
2139 self
2140 }
2141
2142 pub fn max_sse_event_size(mut self, bytes: usize) -> Self {
2144 self.max_sse_event_size = bytes;
2145 self
2146 }
2147
2148 pub fn reinit_on_expired_session(mut self, enable: bool) -> Self {
2157 self.reinit_on_expired_session = enable;
2158 self
2159 }
2160}
2161
2162impl Default for StreamableHttpClientTransportConfig {
2163 fn default() -> Self {
2164 Self {
2165 uri: "localhost".into(),
2166 retry_config: Arc::new(ExponentialBackoff::default()),
2167 channel_buffer_capacity: 16,
2168 max_concurrent_requests: 16,
2169 control_request_timeout: Duration::from_secs(5),
2170 session_recovery_timeout: Duration::from_secs(5),
2171 allow_stateless: true,
2172 auth_header: None,
2173 custom_headers: HashMap::new(),
2174 max_sse_event_size: DEFAULT_MAX_SSE_EVENT_SIZE,
2175 reinit_on_expired_session: true,
2176 }
2177 }
2178}
2179
2180#[cfg(test)]
2181mod tests {
2182 use std::sync::Mutex;
2183
2184 use serde_json::json;
2185
2186 use super::*;
2187 use crate::{
2188 model::{
2189 GetExtensions, ListToolsResult, NumberOrString, ServerRequest, ServerResult, Tool,
2190 },
2191 service::InboundStreamOrigin,
2192 };
2193
2194 #[test]
2195 fn json_rpc_body_preview_empty_html_and_object() {
2196 assert_eq!(json_rpc_body_preview(b""), "<empty>");
2197 assert_eq!(json_rpc_body_preview(b"{}"), "{}");
2198 assert_eq!(
2199 json_rpc_body_preview(b"<html>not json</html>"),
2200 "<html>not json</html>"
2201 );
2202 }
2203
2204 #[test]
2205 fn json_rpc_body_preview_truncates_long_bodies() {
2206 let long = vec![b'x'; JSON_RPC_BODY_PREVIEW_LEN + 40];
2207 let preview = json_rpc_body_preview(&long);
2208 assert_eq!(preview.len(), JSON_RPC_BODY_PREVIEW_LEN);
2209 assert!(preview.chars().all(|c| c == 'x'));
2210 }
2211
2212 #[test]
2213 fn json_rpc_parse_error_message_includes_preview() {
2214 let err = serde_json::from_slice::<ServerJsonRpcMessage>(b"{}").unwrap_err();
2215 let msg = json_rpc_parse_error_message(&err, b"{}");
2216 assert!(msg.contains("could not parse JSON response as ServerJsonRpcMessage"));
2217 assert!(msg.contains("{}"), "missing body preview: {msg}");
2218 }
2219
2220 #[expect(
2221 deprecated,
2222 reason = "Sampling is deprecated by SEP-2577 but remains the canonical restricted request"
2223 )]
2224 fn sampling_request_message(id: i64) -> ServerJsonRpcMessage {
2225 use crate::model::{CreateMessageRequest, CreateMessageRequestParams, SamplingMessage};
2226 ServerJsonRpcMessage::request(
2227 ServerRequest::CreateMessageRequest(CreateMessageRequest::new(
2228 CreateMessageRequestParams::new(vec![SamplingMessage::user_text("hi")], 16),
2229 )),
2230 NumberOrString::Number(id),
2231 )
2232 }
2233
2234 #[tokio::test]
2235 async fn execute_sse_stream_marks_inbound_requests_with_origin() {
2236 for origin in [
2237 InboundStreamOrigin::Unassociated,
2238 InboundStreamOrigin::OutboundRequest(RequestId::Number(3)),
2239 ] {
2240 let response = ServerJsonRpcMessage::response(
2241 ServerResult::ListToolsResult(ListToolsResult::default()),
2242 NumberOrString::Number(1),
2243 );
2244 let stream = futures::stream::iter([Ok(sampling_request_message(9)), Ok(response)]);
2245 let (tx, mut rx) = tokio::sync::mpsc::channel(4);
2246 StreamableHttpClientWorker::<StatelessReconnectClient>::execute_sse_stream(
2247 stream,
2248 tx,
2249 origin.clone(),
2250 false,
2251 CancellationToken::new(),
2252 )
2253 .await
2254 .expect("stream completes");
2255
2256 let ServerJsonRpcMessage::Request(request) =
2257 rx.recv().await.expect("request forwarded")
2258 else {
2259 panic!("expected request first");
2260 };
2261 assert_eq!(
2262 request.request.extensions().get::<InboundStreamOrigin>(),
2263 Some(&origin),
2264 "inbound requests must carry their stream origin"
2265 );
2266 assert!(matches!(
2268 rx.recv().await.expect("response forwarded"),
2269 ServerJsonRpcMessage::Response(_)
2270 ));
2271 }
2272 }
2273
2274 type ReconnectAttempt = (Option<String>, Option<String>);
2275
2276 #[derive(Clone, Default)]
2277 struct StatelessReconnectClient {
2278 reconnects: Arc<Mutex<Vec<ReconnectAttempt>>>,
2279 }
2280
2281 impl StreamableHttpClient for StatelessReconnectClient {
2282 type Error = std::io::Error;
2283
2284 async fn post_message(
2285 &self,
2286 _uri: Arc<str>,
2287 _message: ClientJsonRpcMessage,
2288 _session_id: Option<Arc<str>>,
2289 _auth_header: Option<String>,
2290 _custom_headers: HashMap<HeaderName, HeaderValue>,
2291 ) -> Result<StreamableHttpPostResponse, StreamableHttpError<Self::Error>> {
2292 Err(StreamableHttpError::UnexpectedServerResponse(
2293 "unexpected POST".into(),
2294 ))
2295 }
2296
2297 async fn delete_session(
2298 &self,
2299 _uri: Arc<str>,
2300 _session_id: Arc<str>,
2301 _auth_header: Option<String>,
2302 _custom_headers: HashMap<HeaderName, HeaderValue>,
2303 ) -> Result<(), StreamableHttpError<Self::Error>> {
2304 Ok(())
2305 }
2306
2307 async fn get_stream(
2308 &self,
2309 _uri: Arc<str>,
2310 session_id: Option<Arc<str>>,
2311 last_event_id: Option<String>,
2312 _auth_header: Option<String>,
2313 _custom_headers: HashMap<HeaderName, HeaderValue>,
2314 ) -> Result<BoxedSseStream, StreamableHttpError<Self::Error>> {
2315 self.reconnects
2316 .lock()
2317 .expect("lock reconnects")
2318 .push((session_id.map(|id| id.to_string()), last_event_id));
2319 let response = ServerJsonRpcMessage::response(
2320 ServerResult::ListToolsResult(ListToolsResult::default()),
2321 NumberOrString::Number(1),
2322 );
2323 Ok(futures::stream::once(async move {
2324 Ok(Sse {
2325 event: None,
2326 data: Some(serde_json::to_string(&response).expect("serialize response")),
2327 id: Some("event-1".into()),
2328 retry: None,
2329 })
2330 })
2331 .boxed())
2332 }
2333 }
2334
2335 #[tokio::test]
2336 async fn stateless_response_reconnects_with_last_event_id() {
2337 let initial = futures::stream::iter([Ok(Sse {
2338 event: None,
2339 data: None,
2340 id: Some("event-0".into()),
2341 retry: Some(0),
2342 })])
2343 .boxed();
2344 let client = StatelessReconnectClient::default();
2345 let reconnects = client.reconnects.clone();
2346 let stream =
2347 StreamableHttpClientWorker::<StatelessReconnectClient>::response_sse_to_jsonrpc(
2348 initial,
2349 None,
2350 client,
2351 Arc::from("http://localhost/mcp"),
2352 None,
2353 HashMap::new(),
2354 DEFAULT_MAX_SSE_EVENT_SIZE,
2355 Arc::new(ExponentialBackoff {
2356 max_times: Some(1),
2357 base_duration: Duration::ZERO,
2358 max_delay: None,
2359 }),
2360 );
2361 let mut stream = std::pin::pin!(stream);
2362
2363 let message = stream.next().await.expect("replayed response").unwrap();
2364
2365 assert!(matches!(message, ServerJsonRpcMessage::Response(_)));
2366 assert_eq!(
2367 reconnects.lock().expect("lock reconnects").as_slice(),
2368 &[(None, Some("event-0".into()))]
2369 );
2370 }
2371
2372 #[derive(Clone, Default)]
2373 struct ResumedRequestClient {
2374 reconnects: Arc<Mutex<Vec<ReconnectAttempt>>>,
2375 }
2376
2377 impl StreamableHttpClient for ResumedRequestClient {
2378 type Error = std::io::Error;
2379
2380 async fn post_message(
2381 &self,
2382 _uri: Arc<str>,
2383 _message: ClientJsonRpcMessage,
2384 _session_id: Option<Arc<str>>,
2385 _auth_header: Option<String>,
2386 _custom_headers: HashMap<HeaderName, HeaderValue>,
2387 ) -> Result<StreamableHttpPostResponse, StreamableHttpError<Self::Error>> {
2388 Err(StreamableHttpError::UnexpectedServerResponse(
2389 "unexpected POST".into(),
2390 ))
2391 }
2392
2393 async fn delete_session(
2394 &self,
2395 _uri: Arc<str>,
2396 _session_id: Arc<str>,
2397 _auth_header: Option<String>,
2398 _custom_headers: HashMap<HeaderName, HeaderValue>,
2399 ) -> Result<(), StreamableHttpError<Self::Error>> {
2400 Ok(())
2401 }
2402
2403 async fn get_stream(
2404 &self,
2405 _uri: Arc<str>,
2406 session_id: Option<Arc<str>>,
2407 last_event_id: Option<String>,
2408 _auth_header: Option<String>,
2409 _custom_headers: HashMap<HeaderName, HeaderValue>,
2410 ) -> Result<BoxedSseStream, StreamableHttpError<Self::Error>> {
2411 self.reconnects
2412 .lock()
2413 .expect("lock reconnects")
2414 .push((session_id.map(|id| id.to_string()), last_event_id));
2415 let request = sampling_request_message(9);
2416 let response = ServerJsonRpcMessage::response(
2417 ServerResult::ListToolsResult(ListToolsResult::default()),
2418 NumberOrString::Number(1),
2419 );
2420 Ok(futures::stream::iter([request, response].map(|message| {
2424 Ok(Sse {
2425 event: None,
2426 data: Some(serde_json::to_string(&message).expect("serialize message")),
2427 id: None,
2428 retry: None,
2429 })
2430 }))
2431 .chain(futures::stream::pending())
2432 .boxed())
2433 }
2434 }
2435
2436 #[tokio::test]
2441 async fn resumed_post_stream_requests_keep_outbound_origin() {
2442 let initial = futures::stream::iter([Ok(Sse {
2443 event: None,
2444 data: None,
2445 id: Some("e1".into()),
2446 retry: Some(0),
2447 })])
2448 .boxed();
2449 let client = ResumedRequestClient::default();
2450 let reconnects = client.reconnects.clone();
2451 let sse_stream =
2452 StreamableHttpClientWorker::<ResumedRequestClient>::response_sse_to_jsonrpc(
2453 initial,
2454 None,
2455 client,
2456 Arc::from("http://localhost/mcp"),
2457 None,
2458 HashMap::new(),
2459 DEFAULT_MAX_SSE_EVENT_SIZE,
2460 Arc::new(ExponentialBackoff {
2461 max_times: Some(1),
2462 base_duration: Duration::ZERO,
2463 max_delay: None,
2464 }),
2465 );
2466
2467 let origin = InboundStreamOrigin::OutboundRequest(RequestId::Number(3));
2468 let (tx, mut rx) = tokio::sync::mpsc::channel(4);
2469 StreamableHttpClientWorker::<ResumedRequestClient>::execute_sse_stream(
2470 sse_stream,
2471 tx,
2472 origin.clone(),
2473 true,
2474 CancellationToken::new(),
2475 )
2476 .await
2477 .expect("stream completes");
2478
2479 assert_eq!(
2480 reconnects.lock().expect("lock reconnects").as_slice(),
2481 &[(None, Some("e1".into()))],
2482 "the request must arrive on the resumed connection"
2483 );
2484 let ServerJsonRpcMessage::Request(request) = rx.recv().await.expect("request forwarded")
2485 else {
2486 panic!("expected request first");
2487 };
2488 assert_eq!(
2489 request.request.extensions().get::<InboundStreamOrigin>(),
2490 Some(&origin),
2491 "origin marker must survive SSE resumption"
2492 );
2493 assert!(matches!(
2494 rx.recv().await.expect("response forwarded"),
2495 ServerJsonRpcMessage::Response(_)
2496 ));
2497 }
2498
2499 fn tool(name: &'static str, annotation: serde_json::Value) -> Tool {
2500 let schema = json!({
2501 "type": "object",
2502 "properties": {
2503 "value": annotation,
2504 },
2505 });
2506 Tool::new(
2507 name,
2508 name,
2509 Arc::new(schema.as_object().expect("object schema").clone()),
2510 )
2511 }
2512
2513 #[test]
2514 fn cache_tools_removes_invalid_header_annotations() {
2515 let valid = tool(
2516 "valid",
2517 json!({ "type": "string", "x-mcp-header": "Value" }),
2518 );
2519 let invalid = tool("invalid", json!({ "type": "string", "x-mcp-header": "" }));
2520 let mut message = ServerJsonRpcMessage::response(
2521 ServerResult::ListToolsResult(ListToolsResult::with_all_items(vec![valid, invalid])),
2522 NumberOrString::Number(1),
2523 );
2524 let mut cache = HashMap::new();
2525
2526 cache_tools_from_response(&mut cache, &mut message, &ProtocolVersion::V_2026_07_28);
2527
2528 let ServerJsonRpcMessage::Response(response) = &mut message else {
2529 panic!("expected tools/list response");
2530 };
2531 let ServerResult::ListToolsResult(result) = &mut response.result else {
2532 panic!("expected tools/list result");
2533 };
2534 assert_eq!(
2535 (
2536 result
2537 .tools
2538 .iter()
2539 .map(|tool| tool.name.as_ref())
2540 .collect::<Vec<_>>(),
2541 cache.keys().map(String::as_str).collect::<Vec<_>>(),
2542 ),
2543 (vec!["valid"], vec!["valid"])
2544 );
2545 }
2546
2547 #[test]
2548 fn cache_tools_preserves_pre_standard_header_results() {
2549 let invalid = tool("legacy", json!({ "type": "string", "x-mcp-header": "" }));
2550 let mut message = ServerJsonRpcMessage::response(
2551 ServerResult::ListToolsResult(ListToolsResult::with_all_items(vec![invalid])),
2552 NumberOrString::Number(1),
2553 );
2554 let mut cache = HashMap::new();
2555
2556 cache_tools_from_response(&mut cache, &mut message, &ProtocolVersion::V_2025_11_25);
2557
2558 let ServerJsonRpcMessage::Response(response) = message else {
2559 panic!("expected tools/list response");
2560 };
2561 let ServerResult::ListToolsResult(result) = response.result else {
2562 panic!("expected tools/list result");
2563 };
2564 assert_eq!(
2565 result
2566 .tools
2567 .iter()
2568 .map(|tool| tool.name.as_ref())
2569 .collect::<Vec<_>>(),
2570 vec!["legacy"]
2571 );
2572 }
2573
2574 #[cfg(feature = "transport-streamable-http-client-reqwest")]
2575 #[test]
2576 fn clear_stream_response_pending_accepts_stringified_numeric_id() {
2577 let mut pending = HashSet::from([NumberOrString::Number(1)]);
2578 let response = ServerJsonRpcMessage::response(
2579 ServerResult::ListToolsResult(ListToolsResult::default()),
2580 NumberOrString::String("1".into()),
2581 );
2582
2583 let matched_id =
2584 StreamableHttpClientWorker::<reqwest::Client>::clear_stream_response_pending(
2585 &mut pending,
2586 &response,
2587 );
2588
2589 assert_eq!(matched_id, Some(NumberOrString::Number(1)));
2590 assert!(pending.is_empty());
2591 }
2592
2593 #[cfg(feature = "transport-streamable-http-client-reqwest")]
2594 #[test]
2595 fn clear_stream_response_pending_prefers_exact_string_id() {
2596 let string_id = NumberOrString::String("1".into());
2597 let mut pending = HashSet::from([NumberOrString::Number(1), string_id.clone()]);
2598 let response = ServerJsonRpcMessage::response(
2599 ServerResult::ListToolsResult(ListToolsResult::default()),
2600 string_id.clone(),
2601 );
2602
2603 let matched_id =
2604 StreamableHttpClientWorker::<reqwest::Client>::clear_stream_response_pending(
2605 &mut pending,
2606 &response,
2607 );
2608
2609 assert_eq!(matched_id, Some(string_id));
2610 assert_eq!(pending, HashSet::from([NumberOrString::Number(1)]));
2611 }
2612}