Skip to main content

rmcp/transport/
streamable_http_client.rs

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        // HeaderName::from_static requires lowercase
128        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    /// Create a new `AuthRequiredError` instance.
144    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    /// Create a new `InsufficientScopeError` instance.
161    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    /// check if scope upgrade is possible (i.e., we know what scope is required)
169    pub fn can_upgrade(&self) -> bool {
170        self.required_scope.is_some()
171    }
172
173    /// get the required scope for upgrade
174    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    /// Session recovery timed out. The server may have processed an interrupted POST.
218    #[error("Session recovery timed out; the server may have processed the POST")]
219    SessionRecoveryTimeout,
220    /// A cancellation or reply POST did not finish in time.
221    #[error("Control POST timed out")]
222    ControlRequestTimeout,
223}
224
225/// Bytes of an unusable JSON body kept in
226/// [`StreamableHttpError::UnexpectedServerResponse`].
227pub(crate) const JSON_RPC_BODY_PREVIEW_LEN: usize = 256;
228
229/// Short, lossy view of a body that failed to parse as JSON-RPC.
230pub(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
245/// Request-POST parse failure, including a body preview so empty / HTML / `{}`
246/// are distinguishable from the serde diagnostic alone.
247pub(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    /// The `WWW-Authenticate` challenge carried by this error, when the
256    /// server answered 401 ([`AuthRequired`](Self::AuthRequired)) or 403
257    /// ([`InsufficientScope`](Self::InsufficientScope)). Feed it to
258    /// [`AuthorizationRequest::with_challenge`](crate::transport::auth::AuthorizationRequest::with_challenge)
259    /// to authorize reactively.
260    #[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            // Tolerate servers that return 200 with JSON for notifications
356            Self::Json(..) => Ok(()),
357            got => Err(StreamableHttpError::UnexpectedServerResponse(
358                format!("expect accepted or json, got {got:?}").into(),
359            )),
360        }
361    }
362}
363
364/// Convert a sessionless discovery rejection into a response the lifecycle
365/// layer can classify. The server's own JSON-RPC error is preserved so the
366/// lifecycle can tell a modern rejection (retry at a supported version) from a
367/// legacy one; `invalid_request` is synthesized only when the body has no error
368/// to keep, as with an empty or plain-text 4xx from middleware.
369///
370/// The id is re-correlated because such middleware rejections cannot echo it
371/// (the Python SDK sends the literal `"server-error"`); otherwise the lifecycle
372/// discards the response as uncorrelated without classifying it.
373///
374/// Keep authentication failures and server errors on their original paths.
375pub(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
408/// HTTP backend used by [`StreamableHttpClientTransport`].
409///
410/// Custom implementations that parse SSE responses must override
411/// [`Self::post_message_with_max_sse_event_size`] and
412/// [`Self::get_stream_with_max_sse_event_size`] to enforce the transport's
413/// configured event-size limit.
414///
415/// For legacy http, the transport keeps an open response stream alive until
416/// its cancellation send finishes or is dropped. This lets a custom client
417/// handle the cancellation using state owned by that stream.
418pub 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    /// Send a message while enforcing a maximum raw SSE event size.
431    ///
432    /// `max_sse_event_size` is not a per-request option: it is the
433    /// transport-wide [`StreamableHttpClientTransportConfig::max_sse_event_size`]
434    /// value, passed identically on every call because the limit must be applied
435    /// inside the client (at the raw byte layer, before SSE parsing) rather than
436    /// by the caller.
437    ///
438    /// Custom clients that parse SSE responses should override this method.
439    /// The default implementation delegates to [`Self::post_message`].
440    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    /// Open an SSE stream, optionally scoped to a legacy session.
461    ///
462    /// `session_id` is `None` when resuming a stateless response using only
463    /// `last_event_id`.
464    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    /// Open an SSE stream while enforcing a maximum raw event size.
479    ///
480    /// `max_sse_event_size` is not a per-request option: it is the
481    /// transport-wide [`StreamableHttpClientTransportConfig::max_sse_event_size`]
482    /// value, passed identically on every call because the limit must be applied
483    /// inside the client (at the raw byte layer, before SSE parsing) rather than
484    /// by the caller.
485    ///
486    /// Custom clients that parse SSE responses should override this method.
487    /// The default implementation delegates to [`Self::get_stream`].
488    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
552/// Info retained for cleaning up the session when the worker exits.
553struct 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    // None means the send future was dropped, or the request or transport was cancelled.
571    response: Option<Result<StreamableHttpPostResponse, StreamableHttpError<C::Error>>>,
572    // The protocol version used to send this POST.
573    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    // Run initialization and protocol-version changes without other ordinary POSTs.
603    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                &notification.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 &notification.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        // Keep only retries that have not already received a stream response.
755        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        // The backend error cannot be cloned. Return it to one caller
771        // and return the original session-expired error to the others.
772        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    /// Convert an SSE stream into JSON-RPC messages with reconnect semantics.
788    ///
789    /// This is used for request-scoped SSE responses as well as the standalone
790    /// GET stream. A request-scoped stream can close before its response arrives,
791    /// and SEP-1699 requires the client to honor `retry` and resume with
792    /// `Last-Event-ID` in that case.
793    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    /// Convert a POST response SSE stream into JSON-RPC messages.
819    ///
820    /// Request-scoped streams resume via GET once the server has supplied an
821    /// event ID. The session header remains optional for stateless transports.
822    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                // Stop reading, but keep the stream until the adapter
860                // handles cancellation or the send is dropped.
861                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            // SEP-2260: mark inbound requests with the stream they arrived on
893            // for the client receive-side association check.
894            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                // Consume the remaining stream so the HTTP/1.1 connection
909                // returns to the pool cleanly.
910                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    /// Performs a transparent re-initialization handshake after a session-expired 404.
984    ///
985    /// Takes an owned clone of the client (avoiding `&self` across `.await` so the
986    /// future remains `Send` without requiring `C: Sync`).  POSTs the saved
987    /// initialize request without a session ID, extracts the new session ID and
988    /// protocol version, sends `notifications/initialized`, and returns the new
989    /// `(session_id, protocol_headers)` pair.  The init result message is **not**
990    /// forwarded to the handler because the handler already processed the original
991    /// initialization.
992    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        // SEP-2243: notifications carry no Mcp-Param-*, so an empty tool cache suffices.
1032        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        // SEP-2243: tool input schemas (name -> schema) cached from tools/list responses,
1161        // used to promote annotated tools/call arguments to Mcp-Param-* headers.
1162        let mut tool_header_cache: HashMap<String, Arc<JsonObject>> = HashMap::new();
1163
1164        // Store session info for cleanup when run() exits (not spawned, so cleanup completes before close() returns)
1165        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        // Each POST uses the session and headers chosen when it starts.
1243        // Only this loop updates the current session and protocol version.
1244        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                // Old POSTs have finished or reached the drain deadline.
1254                // Retry only ordinary POSTs that returned SessionExpired, at most once each.
1255                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                        // Do not send controls queued during recovery to the new session.
1301                        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                        // Do not send old controls to a replacement session.
1428                        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                        // Servers do not assign sessions to `server/discover`, so a
1500                        // fallback initialize starts from a clean slate: no session
1501                        // ID, no cleanup state, and no streams to tear down.
1502                        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                    // The common stream can return a response before this POST finishes.
1587                    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                        // An ordering barrier runs without other ordinary POSTs.
1611                        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                        // A stream response or cancellation already completed this request.
1619                        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                                &notification.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                            // A legacy adapter may need the open stream to handle cancellation.
1695                            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                    // Match against all pending requests, not just open response streams.
1751                    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                    // send the message to the handler
1763                    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        // Stop outstanding http requests before deleting their session.
1800        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        // Cleanup session before returning (ensures close() waits for session deletion)
1808        // Use a timeout to prevent indefinite hangs if the server is unresponsive
1809        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
1854/// A client-agnostic HTTP transport for RMCP that supports streaming responses.
1855///
1856/// This transport allows you to choose your preferred HTTP client implementation
1857/// by implementing the [`StreamableHttpClient`] trait. The transport handles
1858/// session management, SSE streaming, and automatic reconnection.
1859///
1860/// # Usage
1861///
1862/// ## Using reqwest
1863///
1864/// ```rust,no_run
1865/// use rmcp::transport::StreamableHttpClientTransport;
1866///
1867/// // Enable the reqwest feature in Cargo.toml:
1868/// // rmcp = { version = "0.5", features = ["transport-streamable-http-client-reqwest"] }
1869///
1870/// let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp");
1871/// ```
1872///
1873/// ## Using a custom HTTP client
1874///
1875/// ```rust,no_run
1876/// use rmcp::transport::streamable_http_client::{
1877///     StreamableHttpClient,
1878///     StreamableHttpClientTransport,
1879///     StreamableHttpClientTransportConfig
1880/// };
1881/// use std::sync::Arc;
1882/// use std::collections::HashMap;
1883/// use futures::stream::BoxStream;
1884/// use rmcp::model::ClientJsonRpcMessage;
1885/// use http::{HeaderName, HeaderValue};
1886/// use sse_stream::{Sse, Error as SseError};
1887///
1888/// #[derive(Clone)]
1889/// struct MyHttpClient;
1890///
1891/// #[derive(Debug, thiserror::Error)]
1892/// struct MyError;
1893///
1894/// impl std::fmt::Display for MyError {
1895///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1896///         write!(f, "MyError")
1897///     }
1898/// }
1899///
1900/// impl StreamableHttpClient for MyHttpClient {
1901///     type Error = MyError;
1902///
1903///     async fn post_message(
1904///         &self,
1905///         _uri: Arc<str>,
1906///         _message: ClientJsonRpcMessage,
1907///         _session_id: Option<Arc<str>>,
1908///         _auth_header: Option<String>,
1909///         _custom_headers: HashMap<HeaderName, HeaderValue>,
1910///     ) -> Result<rmcp::transport::streamable_http_client::StreamableHttpPostResponse, rmcp::transport::streamable_http_client::StreamableHttpError<Self::Error>> {
1911///         todo!()
1912///     }
1913///
1914///     async fn delete_session(
1915///         &self,
1916///         _uri: Arc<str>,
1917///         _session_id: Arc<str>,
1918///         _auth_header: Option<String>,
1919///         _custom_headers: HashMap<HeaderName, HeaderValue>,
1920///     ) -> Result<(), rmcp::transport::streamable_http_client::StreamableHttpError<Self::Error>> {
1921///         todo!()
1922///     }
1923///
1924///     async fn get_stream(
1925///         &self,
1926///         _uri: Arc<str>,
1927///         _session_id: Option<Arc<str>>,
1928///         _last_event_id: Option<String>,
1929///         _auth_header: Option<String>,
1930///         _custom_headers: HashMap<HeaderName, HeaderValue>,
1931///     ) -> Result<BoxStream<'static, Result<Sse, SseError>>, rmcp::transport::streamable_http_client::StreamableHttpError<Self::Error>> {
1932///         todo!()
1933///     }
1934/// }
1935///
1936/// let transport = StreamableHttpClientTransport::with_client(
1937///     MyHttpClient,
1938///     StreamableHttpClientTransportConfig::with_uri("http://localhost:8000/mcp")
1939/// );
1940/// ```
1941///
1942/// # Feature Flags
1943///
1944/// - `transport-streamable-http-client`: Base feature providing the generic transport infrastructure
1945/// - `transport-streamable-http-client-reqwest`: Includes reqwest HTTP client support with convenience methods
1946pub type StreamableHttpClientTransport<C> = WorkerTransport<StreamableHttpClientWorker<C>>;
1947
1948impl<C: StreamableHttpClient> StreamableHttpClientTransport<C> {
1949    /// Creates a new transport with a custom HTTP client implementation.
1950    ///
1951    /// This method allows you to use any HTTP client that implements the [`StreamableHttpClient`] trait.
1952    /// Use this when you want to use a custom HTTP client or when the reqwest feature is not enabled.
1953    ///
1954    /// # Arguments
1955    ///
1956    /// * `client` - Your HTTP client implementation
1957    /// * `config` - Transport configuration including the server URI
1958    ///
1959    /// # Example
1960    ///
1961    /// ```rust,no_run
1962    /// use rmcp::transport::streamable_http_client::{
1963    ///     StreamableHttpClient,
1964    ///     StreamableHttpClientTransport,
1965    ///     StreamableHttpClientTransportConfig
1966    /// };
1967    /// use std::sync::Arc;
1968    /// use std::collections::HashMap;
1969    /// use futures::stream::BoxStream;
1970    /// use rmcp::model::ClientJsonRpcMessage;
1971    /// use http::{HeaderName, HeaderValue};
1972    /// use sse_stream::{Sse, Error as SseError};
1973    ///
1974    /// // Define your custom client
1975    /// #[derive(Clone)]
1976    /// struct MyHttpClient;
1977    ///
1978    /// #[derive(Debug, thiserror::Error)]
1979    /// struct MyError;
1980    ///
1981    /// impl std::fmt::Display for MyError {
1982    ///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1983    ///         write!(f, "MyError")
1984    ///     }
1985    /// }
1986    ///
1987    /// impl StreamableHttpClient for MyHttpClient {
1988    ///     type Error = MyError;
1989    ///
1990    ///     async fn post_message(
1991    ///         &self,
1992    ///         _uri: Arc<str>,
1993    ///         _message: ClientJsonRpcMessage,
1994    ///         _session_id: Option<Arc<str>>,
1995    ///         _auth_header: Option<String>,
1996    ///         _custom_headers: HashMap<HeaderName, HeaderValue>,
1997    ///     ) -> Result<rmcp::transport::streamable_http_client::StreamableHttpPostResponse, rmcp::transport::streamable_http_client::StreamableHttpError<Self::Error>> {
1998    ///         todo!()
1999    ///     }
2000    ///
2001    ///     async fn delete_session(
2002    ///         &self,
2003    ///         _uri: Arc<str>,
2004    ///         _session_id: Arc<str>,
2005    ///         _auth_header: Option<String>,
2006    ///         _custom_headers: HashMap<HeaderName, HeaderValue>,
2007    ///     ) -> Result<(), rmcp::transport::streamable_http_client::StreamableHttpError<Self::Error>> {
2008    ///         todo!()
2009    ///     }
2010    ///
2011    ///     async fn get_stream(
2012    ///         &self,
2013    ///         _uri: Arc<str>,
2014    ///         _session_id: Option<Arc<str>>,
2015    ///         _last_event_id: Option<String>,
2016    ///         _auth_header: Option<String>,
2017    ///         _custom_headers: HashMap<HeaderName, HeaderValue>,
2018    ///     ) -> Result<BoxStream<'static, Result<Sse, SseError>>, rmcp::transport::streamable_http_client::StreamableHttpError<Self::Error>> {
2019    ///         todo!()
2020    ///     }
2021    /// }
2022    ///
2023    /// let transport = StreamableHttpClientTransport::with_client(
2024    ///     MyHttpClient,
2025    ///     StreamableHttpClientTransportConfig::with_uri("http://localhost:8000/mcp")
2026    /// );
2027    /// ```
2028    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    /// Maximum number of ordinary http POSTs in progress (default: 16).
2040    /// A POST stops counting when it completes or opens an sse response stream.
2041    /// Zero is treated as one. Cancellation and replies use a separate queue
2042    /// with one extra POST slot and use [`Self::control_request_timeout`].
2043    pub max_concurrent_requests: usize,
2044    /// Maximum time a cancellation or reply POST can run after it starts (default: five seconds).
2045    /// Time spent waiting in the control queue does not count toward this timeout.
2046    pub control_request_timeout: Duration,
2047    /// Maximum wait for old POSTs to finish before session recovery (default: five seconds).
2048    /// The new initialization handshake has a separate timeout of the same length.
2049    /// An unfinished old POST returns [`StreamableHttpError::SessionRecoveryTimeout`]
2050    /// and is not retried because the server may have processed it.
2051    pub session_recovery_timeout: Duration,
2052    /// if true, the transport will not require a session to be established
2053    pub allow_stateless: bool,
2054    /// The value to send in the authorization header
2055    pub auth_header: Option<String>,
2056    /// Custom HTTP headers to include with every request
2057    pub custom_headers: HashMap<HeaderName, HeaderValue>,
2058    /// Maximum raw size of one SSE event accepted from the server.
2059    ///
2060    /// The built-in reqwest and Unix socket clients enforce this value. Custom
2061    /// [`StreamableHttpClient`] implementations must override the corresponding
2062    /// `*_with_max_sse_event_size` methods to enforce it.
2063    pub max_sse_event_size: usize,
2064    /// Automatically creates a new session when the server reports an expired
2065    /// session (`http 404`).
2066    ///
2067    /// Ordinary POSTs that fail with `SessionExpired` in the same session share one
2068    /// recovery attempt:
2069    /// 1. Wait for old POSTs, up to [`Self::session_recovery_timeout`].
2070    /// 2. Repeat the original `initialize` handshake and open new streams.
2071    /// 3. Retry each ordinary POST that failed with `SessionExpired` once.
2072    ///
2073    /// Control POSTs and other POST failures are not retried. If recovery or a retry
2074    /// fails, the transport returns an error to the caller.
2075    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    /// Set how many ordinary POSTs can run at once. One keeps them serial; zero also means one.
2087    pub fn max_concurrent_requests(mut self, limit: usize) -> Self {
2088        self.max_concurrent_requests = limit.max(1);
2089        self
2090    }
2091
2092    /// Set the timeout for cancellation and reply POSTs, starting when each POST starts.
2093    pub fn control_request_timeout(mut self, timeout: Duration) -> Self {
2094        self.control_request_timeout = timeout;
2095        self
2096    }
2097
2098    /// Set the separate timeouts for waiting for old POSTs and creating a replacement session.
2099    pub fn session_recovery_timeout(mut self, timeout: Duration) -> Self {
2100        self.session_recovery_timeout = timeout;
2101        self
2102    }
2103
2104    /// Set the authorization header to send with requests
2105    ///
2106    /// # Arguments
2107    ///
2108    /// * `value` - A bearer token without the `Bearer ` prefix
2109    pub fn auth_header<T: Into<String>>(mut self, value: T) -> Self {
2110        // set our authorization header
2111        self.auth_header = Some(value.into());
2112        self
2113    }
2114
2115    /// Set custom HTTP headers to include with every request
2116    ///
2117    /// # Arguments
2118    ///
2119    /// * `custom_headers` - A HashMap of header names to header values
2120    ///
2121    /// # Example
2122    ///
2123    /// ```rust,no_run
2124    /// use std::collections::HashMap;
2125    /// use http::{HeaderName, HeaderValue};
2126    /// use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
2127    ///
2128    /// let mut headers = HashMap::new();
2129    /// headers.insert(
2130    ///     HeaderName::from_static("x-custom-header"),
2131    ///     HeaderValue::from_static("custom-value")
2132    /// );
2133    ///
2134    /// let config = StreamableHttpClientTransportConfig::with_uri("http://localhost:8000")
2135    ///     .custom_headers(headers);
2136    /// ```
2137    pub fn custom_headers(mut self, custom_headers: HashMap<HeaderName, HeaderValue>) -> Self {
2138        self.custom_headers = custom_headers;
2139        self
2140    }
2141
2142    /// Set the maximum raw size of one SSE event accepted from the server.
2143    pub fn max_sse_event_size(mut self, bytes: usize) -> Self {
2144        self.max_sse_event_size = bytes;
2145        self
2146    }
2147
2148    /// Set whether the transport should attempt transparent re-initialization on session expiration
2149    /// See [`Self::reinit_on_expired_session`] for details.
2150    /// # Example
2151    /// ```rust,no_run
2152    /// use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
2153    /// let config = StreamableHttpClientTransportConfig::with_uri("http://localhost:8000")
2154    ///     .reinit_on_expired_session(true);
2155    /// ```
2156    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            // Responses are correlated by JSON-RPC id; no marker needed or added.
2267            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            // Stay open after the response, like a live connection, so the
2421            // post-response drain in `execute_sse_stream` doesn't trigger
2422            // further reconnects.
2423            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    /// SEP-1699 resumes a broken POST SSE stream via GET + Last-Event-ID
2437    /// beneath `execute_sse_stream`, so the SEP-2260 origin marker must span
2438    /// resumes; if reconnection were hoisted above the marker attach point,
2439    /// replayed associated requests would be wrongly rejected with -32602.
2440    #[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}