Skip to main content

yapi_core/mcp/
http.rs

1//! The streamable HTTP transport: each message is a POST whose reply is JSON
2//! or an event stream, plus a GET stream for messages the server starts. Port
3//! of `transports/streamable-http.ts` in pi-mcp `v1.0.0`.
4
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7use std::time::{Duration, Instant};
8
9use futures_util::future::BoxFuture;
10use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
11use serde_json::{Value, json};
12use tokio_util::sync::CancellationToken;
13use yapi_types::sync::lock;
14
15use super::jsonrpc::{INTERNAL_ERROR, Incoming, McpError, classify};
16use super::oauth::is_insufficient_scope;
17use super::sign_in::McpAuth;
18use super::transport::{Event, Events, MAX_MESSAGE_BYTES};
19
20const MAX_ERROR_BODY_BYTES: usize = 8 * 1024;
21const ERROR_MESSAGE_BODY_CHARS: usize = 500;
22const RECONNECT_INITIAL_DELAY_MS: u64 = 1_000;
23const RECONNECT_MAX_DELAY_MS: u64 = 30_000;
24const RECONNECT_MAX_RETRIES: u32 = 5;
25
26/// The current token of a pi provider (`/login`), by provider id.
27pub type ProviderToken = Arc<dyn Fn(String) -> BoxFuture<'static, Option<String>> + Send + Sync>;
28
29/// pi's `AuthProvider`: where a server's bearer token comes from.
30#[derive(Clone)]
31pub enum HttpAuth {
32    /// OAuth credentials, refreshed after a 401.
33    OAuth(Arc<McpAuth>),
34    /// The token of a provider (`auth.provider`), read for every request.
35    Provider {
36        /// The provider id.
37        provider: String,
38        /// Reads the token. Without it, no token is sent.
39        token: Option<ProviderToken>,
40    },
41}
42
43impl HttpAuth {
44    async fn token(&self) -> Result<Option<String>, McpError> {
45        match self {
46            HttpAuth::OAuth(auth) => auth.token().await,
47            HttpAuth::Provider { provider, token } => Ok(match token {
48                Some(token) => token(provider.clone()).await,
49                None => None,
50            }),
51        }
52    }
53}
54
55/// How to reach a streamable HTTP server.
56#[derive(Clone, Default)]
57pub struct HttpOptions {
58    /// The endpoint.
59    pub url: String,
60    /// Headers sent with every request.
61    pub headers: Vec<(String, String)>,
62    /// Supplies the bearer token.
63    pub auth: Option<HttpAuth>,
64}
65
66/// A streamable HTTP connection. Cheap to clone, and clones share it.
67#[derive(Clone)]
68pub struct HttpTransport {
69    inner: Arc<Inner>,
70}
71
72struct Inner {
73    options: HttpOptions,
74    cancel: CancellationToken,
75    session: Mutex<Option<String>>,
76    protocol_version: Mutex<Option<String>>,
77    events: Mutex<Option<Events>>,
78    started: AtomicBool,
79    closed: AtomicBool,
80    get_stream_started: AtomicBool,
81}
82
83fn network(error: reqwest::Error) -> McpError {
84    let mut message = error.to_string();
85    let mut source = std::error::Error::source(&error);
86    while let Some(cause) = source {
87        message = format!("{message}: {cause}");
88        source = cause.source();
89    }
90    McpError::Network(message)
91}
92
93/// A request that got no response, with the message of Node's `fetch`,
94/// which pi shows.
95fn fetch_failed(_error: reqwest::Error) -> McpError {
96    McpError::Network("fetch failed".to_owned())
97}
98
99fn content_type(response: &reqwest::Response) -> Option<String> {
100    response
101        .headers()
102        .get(reqwest::header::CONTENT_TYPE)
103        .and_then(|value| value.to_str().ok())
104        .and_then(|value| value.split(';').next())
105        .map(|value| value.trim().to_ascii_lowercase())
106}
107
108fn describe_failure(status: u16, body: &str) -> String {
109    let text = body.trim();
110    let snippet = if yapi_types::js::len(text) > ERROR_MESSAGE_BODY_CHARS {
111        format!(
112            "{}...",
113            yapi_types::js::slice(text, 0, ERROR_MESSAGE_BODY_CHARS - 3)
114        )
115    } else {
116        text.to_owned()
117    };
118    if snippet.is_empty() {
119        format!("MCP HTTP request failed with status {status}")
120    } else {
121        format!("MCP HTTP request failed with status {status}: {snippet}")
122    }
123}
124
125/// Node's `TypeError`, which pi's transport treats as a network failure.
126fn type_error(message: String) -> McpError {
127    McpError::Network(message)
128}
129
130/// Checks `text` as Node's `Headers` converts it to a `ByteString`.
131fn byte_string(text: &str) -> Result<(), McpError> {
132    match text
133        .encode_utf16()
134        .enumerate()
135        .find(|(_, unit)| *unit > 255)
136    {
137        Some((index, unit)) => Err(type_error(format!(
138            "Cannot convert argument to a ByteString because the character at index {index} has a value of {unit} which is greater than 255."
139        ))),
140        None => Ok(()),
141    }
142}
143
144/// `value` as Node's `Headers.<method>` stores it: trimmed of HTTP
145/// whitespace, and refused with a line break or NUL left inside.
146fn header_value<'v>(method: &str, value: &'v str) -> Result<&'v str, McpError> {
147    byte_string(value)?;
148    let value = value.trim_matches([' ', '\t', '\r', '\n']);
149    if value.contains(['\r', '\n', '\0']) {
150        return Err(type_error(format!(
151            "Headers.{method}: \"{value}\" is an invalid header value."
152        )));
153    }
154    Ok(value)
155}
156
157/// The configured headers as pi's `new Headers(headers)` takes them: every
158/// name and value is checked as a byte string first, then each name as a
159/// token and each value as [`header_value`] does.
160fn configured_headers(headers: &[(String, String)]) -> Result<Vec<(HeaderName, String)>, McpError> {
161    for text in headers.iter().flat_map(|(name, value)| [name, value]) {
162        byte_string(text)?;
163    }
164    headers
165        .iter()
166        .map(|(name, value)| {
167            let name = HeaderName::from_bytes(name.as_bytes()).map_err(|_| {
168                type_error(format!(
169                    "Headers.append: \"{name}\" is an invalid header name."
170                ))
171            })?;
172            Ok((name, header_value("append", value)?.to_owned()))
173        })
174        .collect()
175}
176
177/// Where a stream is, for resuming it with `Last-Event-ID`.
178#[derive(Default)]
179struct Cursor {
180    last_event_id: Option<String>,
181    retry_ms: Option<u64>,
182    /// An event arrived since the stream was (re)opened.
183    received: bool,
184}
185
186/// pi-mcp's reader of server-sent events (SSE): LF-separated lines with an
187/// optional CR, `id` reported for every event, events without data dropped.
188#[derive(Default)]
189struct SseParser {
190    buffer: Vec<u8>,
191    event: Option<String>,
192    data: Vec<String>,
193    data_bytes: usize,
194}
195
196struct SseEvent {
197    event: Option<String>,
198    data: String,
199}
200
201impl SseParser {
202    fn push(&mut self, chunk: &[u8], cursor: &mut Cursor) -> Result<Vec<SseEvent>, String> {
203        self.buffer.extend_from_slice(chunk);
204        let mut events = Vec::new();
205        while let Some(newline) = self.buffer.iter().position(|byte| *byte == b'\n') {
206            let line: Vec<u8> = self.buffer.drain(..=newline).collect();
207            let line = String::from_utf8_lossy(&line[..line.len() - 1]).into_owned();
208            self.line(&line, cursor, &mut events)?;
209        }
210        if self.buffer.len() > MAX_MESSAGE_BYTES {
211            return Err(format!("MCP SSE event exceeds {MAX_MESSAGE_BYTES} bytes"));
212        }
213        Ok(events)
214    }
215
216    fn finish(&mut self, cursor: &mut Cursor) -> Result<Vec<SseEvent>, String> {
217        let mut events = Vec::new();
218        if !self.buffer.is_empty() {
219            let line = String::from_utf8_lossy(&std::mem::take(&mut self.buffer)).into_owned();
220            self.line(&line, cursor, &mut events)?;
221        }
222        events.extend(self.dispatch());
223        Ok(events)
224    }
225
226    fn dispatch(&mut self) -> Option<SseEvent> {
227        let event = self.event.take();
228        if self.data.is_empty() {
229            return None;
230        }
231        self.data_bytes = 0;
232        Some(SseEvent {
233            event,
234            data: std::mem::take(&mut self.data).join("\n"),
235        })
236    }
237
238    fn line(
239        &mut self,
240        line: &str,
241        cursor: &mut Cursor,
242        events: &mut Vec<SseEvent>,
243    ) -> Result<(), String> {
244        let line = line.strip_suffix('\r').unwrap_or(line);
245        if line.is_empty() {
246            events.extend(self.dispatch());
247            return Ok(());
248        }
249        if line.starts_with(':') {
250            return Ok(());
251        }
252        let (field, value) = match line.split_once(':') {
253            Some((field, value)) => (field, value.strip_prefix(' ').unwrap_or(value)),
254            None => (line, ""),
255        };
256        match field {
257            "data" => {
258                self.data_bytes += value.len() + usize::from(!self.data.is_empty());
259                if self.data_bytes > MAX_MESSAGE_BYTES {
260                    return Err(format!("MCP SSE event exceeds {MAX_MESSAGE_BYTES} bytes"));
261                }
262                self.data.push(value.to_owned());
263            }
264            "event" => self.event = Some(value.to_owned()),
265            "id" if !value.contains('\0') => cursor.last_event_id = Some(value.to_owned()),
266            "retry" if !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()) => {
267                cursor.retry_ms = value.parse().ok();
268            }
269            _ => {}
270        }
271        Ok(())
272    }
273}
274
275impl HttpTransport {
276    /// A transport for `options`.
277    pub fn new(options: HttpOptions) -> HttpTransport {
278        HttpTransport {
279            inner: Arc::new(Inner {
280                options,
281                cancel: CancellationToken::new(),
282                session: Mutex::new(None),
283                protocol_version: Mutex::new(None),
284                events: Mutex::new(None),
285                started: AtomicBool::new(false),
286                closed: AtomicBool::new(false),
287                get_stream_started: AtomicBool::new(false),
288            }),
289        }
290    }
291
292    /// The session id the server assigned.
293    pub fn session_id(&self) -> Option<String> {
294        lock(&self.inner.session).clone()
295    }
296
297    /// Begins delivering messages to `events`.
298    pub fn start(&self, events: Events) -> Result<(), McpError> {
299        if self.inner.closed.load(Ordering::SeqCst) {
300            return Err(McpError::closed());
301        }
302        *lock(&self.inner.events) = Some(events);
303        self.inner.started.store(true, Ordering::SeqCst);
304        Ok(())
305    }
306
307    /// Sends `MCP-Protocol-Version` from now on.
308    pub fn set_protocol_version(&self, version: &str) {
309        *lock(&self.inner.protocol_version) = Some(version.to_owned());
310    }
311
312    fn emit(&self, event: Event) {
313        if let Some(events) = lock(&self.inner.events).as_ref() {
314            let _ = events.send(event);
315        }
316    }
317
318    /// Posts one message. Its reply arrives as events.
319    pub async fn send(&self, message: &Value) -> Result<(), McpError> {
320        if !self.inner.started.load(Ordering::SeqCst) || self.inner.closed.load(Ordering::SeqCst) {
321            return Err(McpError::closed());
322        }
323        let body = yapi_types::json::stringify(message);
324        let headers = vec![
325            (
326                reqwest::header::ACCEPT,
327                "application/json, text/event-stream".to_owned(),
328            ),
329            (reqwest::header::CONTENT_TYPE, "application/json".to_owned()),
330        ];
331        let response = self
332            .authorized(reqwest::Method::POST, Some(body), headers)
333            .await?;
334        let response = self.check(response).await?;
335        self.capture_session(&response);
336        let request_id = match classify(message) {
337            Some(Incoming::Request { id, .. }) => id.clone(),
338            _ => {
339                // Notifications and responses get 202 and no reply.
340                if message["method"] == "notifications/initialized" {
341                    self.start_get_stream();
342                }
343                return Ok(());
344            }
345        };
346        let status = response.status().as_u16();
347        if status == 202 || status == 204 {
348            return Err(McpError::Http {
349                status,
350                message: format!(
351                    "MCP server accepted request {} without a response",
352                    message["method"].as_str().unwrap_or_default()
353                ),
354            });
355        }
356        match content_type(&response).as_deref() {
357            Some("application/json") => {
358                let bytes = response.bytes().await.map_err(network)?;
359                let body: Value = serde_json::from_slice(&bytes)
360                    .map_err(|error| McpError::Other(error.to_string()))?;
361                let items = match body {
362                    Value::Array(items) => items,
363                    item => vec![item],
364                };
365                for item in items {
366                    match classify(&item) {
367                        Some(_) => self.emit(Event::Message(item)),
368                        None => return Err(McpError::invalid("Invalid JSON-RPC message")),
369                    }
370                }
371                Ok(())
372            }
373            Some("text/event-stream") => {
374                let transport = self.clone();
375                tokio::spawn(async move { transport.read_response(response, request_id).await });
376                Ok(())
377            }
378            other => Err(McpError::Http {
379                status,
380                message: format!(
381                    "Unsupported MCP response content type: {}",
382                    other.unwrap_or("missing")
383                ),
384            }),
385        }
386    }
387
388    /// Ends the session: pending reads stop, and a DELETE tells the server.
389    pub async fn close(&self) {
390        if self.inner.closed.swap(true, Ordering::SeqCst) {
391            return;
392        }
393        self.inner.cancel.cancel();
394        if self.inner.started.load(Ordering::SeqCst) && self.session_id().is_some() {
395            let request = async {
396                let Ok(configured) = configured_headers(&self.inner.options.headers) else {
397                    return;
398                };
399                // Without a token, the session expires on the server.
400                let Ok(token) = self.token().await else {
401                    return;
402                };
403                let Ok(headers) = self.headers(configured, &[], token.as_deref()) else {
404                    return;
405                };
406                let _ = yapi_ai::http::client()
407                    .delete(&self.inner.options.url)
408                    .headers(headers)
409                    .send()
410                    .await;
411            };
412            let _ = tokio::time::timeout(Duration::from_secs(1), request).await;
413        }
414        self.emit(Event::Closed);
415    }
416
417    async fn token(&self) -> Result<Option<String>, McpError> {
418        match &self.inner.options.auth {
419            Some(auth) => auth.token().await,
420            None => Ok(None),
421        }
422    }
423
424    /// A request's headers as pi builds them with Node's `Headers` and
425    /// `fetch` sends them: the `configured` ones, then `extra`, the session,
426    /// the protocol version and the bearer `token`, each replacing its name.
427    fn headers(
428        &self,
429        mut entries: Vec<(HeaderName, String)>,
430        extra: &[(HeaderName, String)],
431        token: Option<&str>,
432    ) -> Result<HeaderMap, McpError> {
433        let mut set = |name: HeaderName, value: &str| {
434            let value = header_value("set", value)?.to_owned();
435            entries.retain(|(other, _)| *other != name);
436            entries.push((name, value));
437            Ok::<_, McpError>(())
438        };
439        for (name, value) in extra {
440            set(name.clone(), value)?;
441        }
442        if let Some(session) = self.session_id() {
443            set(HeaderName::from_static("mcp-session-id"), &session)?;
444        }
445        if let Some(version) = lock(&self.inner.protocol_version).clone() {
446            set(HeaderName::from_static("mcp-protocol-version"), &version)?;
447        }
448        if let Some(token) = token.filter(|token| !token.is_empty()) {
449            set(reqwest::header::AUTHORIZATION, &format!("Bearer {token}"))?;
450        }
451        // `fetch` sends a byte per character and fails on other control
452        // characters.
453        let mut headers = HeaderMap::new();
454        for (name, value) in entries {
455            let bytes: Vec<u8> = value.chars().filter_map(|c| u8::try_from(c).ok()).collect();
456            let value = HeaderValue::from_bytes(&bytes)
457                .map_err(|_| McpError::Network("fetch failed".to_owned()))?;
458            headers.append(name, value);
459        }
460        Ok(headers)
461    }
462
463    /// Sends a request with the auth headers. A 401, or a 403 asking for more
464    /// scope, goes to the OAuth credentials once, and the request is retried
465    /// with whatever they left behind.
466    async fn authorized(
467        &self,
468        method: reqwest::Method,
469        body: Option<String>,
470        extra: Vec<(HeaderName, String)>,
471    ) -> Result<reqwest::Response, McpError> {
472        let mut attempt = 0;
473        loop {
474            let configured = configured_headers(&self.inner.options.headers)?;
475            let token = self.token().await?;
476            let mut request = yapi_ai::http::client()
477                .request(method.clone(), &self.inner.options.url)
478                .headers(self.headers(configured, &extra, token.as_deref())?);
479            if let Some(body) = &body {
480                request = request.body(body.clone());
481            }
482            let response = tokio::select! {
483                () = self.inner.cancel.cancelled() => Err(McpError::closed()),
484                response = request.send() => response.map_err(fetch_failed),
485            }?;
486            let Some(HttpAuth::OAuth(auth)) = &self.inner.options.auth else {
487                return Ok(response);
488            };
489            let challenge = response
490                .headers()
491                .get(reqwest::header::WWW_AUTHENTICATE)
492                .and_then(|value| value.to_str().ok())
493                .map(str::to_owned);
494            let status = response.status().as_u16();
495            let unauthorized =
496                status == 401 || (status == 403 && is_insufficient_scope(challenge.as_deref()));
497            if attempt > 0 || !unauthorized {
498                return Ok(response);
499            }
500            drop(response);
501            auth.on_unauthorized(challenge.as_deref(), token.as_deref())
502                .await?;
503            attempt += 1;
504        }
505    }
506
507    async fn check(&self, response: reqwest::Response) -> Result<reqwest::Response, McpError> {
508        if response.status().is_success() {
509            return Ok(response);
510        }
511        let status = response.status().as_u16();
512        let challenge = response
513            .headers()
514            .get(reqwest::header::WWW_AUTHENTICATE)
515            .and_then(|value| value.to_str().ok())
516            .map(str::to_owned);
517        let body = response.text().await.unwrap_or_default();
518        let body: String = body.chars().take(MAX_ERROR_BODY_BYTES).collect();
519        if status == 401 {
520            return Err(McpError::AuthRequired { challenge });
521        }
522        if status == 404 && self.session_id().is_some() {
523            return Err(McpError::SessionExpired);
524        }
525        Err(McpError::Http {
526            status,
527            message: describe_failure(status, &body),
528        })
529    }
530
531    fn capture_session(&self, response: &reqwest::Response) {
532        if let Some(session) = response
533            .headers()
534            .get("mcp-session-id")
535            .and_then(|value| value.to_str().ok())
536            .filter(|value| !value.is_empty())
537        {
538            *lock(&self.inner.session) = Some(session.to_owned());
539        }
540    }
541
542    /// Reads an event stream to its end. Returns whether it carried the
543    /// response to `request_id`.
544    async fn consume(
545        &self,
546        mut response: reqwest::Response,
547        cursor: &mut Cursor,
548        request_id: Option<&Value>,
549    ) -> Result<bool, McpError> {
550        let mut parser = SseParser::default();
551        let mut answered = false;
552        loop {
553            let chunk = tokio::select! {
554                () = self.inner.cancel.cancelled() => return Ok(answered),
555                chunk = response.chunk() => chunk.map_err(network)?,
556            };
557            let done = chunk.is_none();
558            let events = match chunk {
559                Some(bytes) => parser.push(&bytes, cursor),
560                None => parser.finish(cursor),
561            }
562            .map_err(McpError::Other)?;
563            for event in events {
564                cursor.received = true;
565                if event.data.trim().is_empty()
566                    || event.event.as_deref().is_some_and(|name| name != "message")
567                {
568                    continue;
569                }
570                let message = match serde_json::from_str::<Value>(&event.data) {
571                    Ok(message) if classify(&message).is_some() => message,
572                    Ok(_) => {
573                        self.emit(Event::Error("Invalid JSON-RPC message".into()));
574                        continue;
575                    }
576                    Err(error) => {
577                        self.emit(Event::Error(error.to_string()));
578                        continue;
579                    }
580                };
581                if let (Some(id), Some(Incoming::Response { id: answer, .. })) =
582                    (request_id, classify(&message))
583                    && answer == id
584                {
585                    answered = true;
586                }
587                self.emit(Event::Message(message));
588            }
589            if done {
590                return Ok(answered);
591            }
592        }
593    }
594
595    /// The stream answering one request. When it ends or breaks first and the
596    /// server assigned event ids, it resumes with `Last-Event-ID`. Otherwise
597    /// the request fails.
598    async fn read_response(&self, response: reqwest::Response, request_id: Value) {
599        let mut cursor = Cursor::default();
600        let mut stream = Some(response);
601        let mut failure: Option<McpError> = None;
602        let mut attempt = 0;
603        loop {
604            if let Some(response) = stream.take() {
605                match self.consume(response, &mut cursor, Some(&request_id)).await {
606                    Ok(true) => return,
607                    Ok(false) => failure = None,
608                    Err(error) => failure = Some(error),
609                }
610            }
611            if self.inner.closed.load(Ordering::SeqCst) {
612                return;
613            }
614            if failure.as_ref().is_some_and(|error| !retryable(error)) {
615                break;
616            }
617            if cursor.last_event_id.is_none() || attempt >= RECONNECT_MAX_RETRIES {
618                break;
619            }
620            if cursor.received {
621                attempt = 0;
622            }
623            cursor.received = false;
624            let delay = reconnect_delay(attempt, cursor.retry_ms);
625            attempt += 1;
626            if !self.sleep(delay).await {
627                return;
628            }
629            match self.open_stream(cursor.last_event_id.clone()).await {
630                Ok(response) => stream = response,
631                Err(error) => {
632                    let stop = !retryable(&error);
633                    failure = Some(error);
634                    if stop {
635                        break;
636                    }
637                }
638            }
639        }
640        if self.inner.closed.load(Ordering::SeqCst) {
641            return;
642        }
643        let reason = failure.map_or_else(
644            || "stream ended without a response".to_owned(),
645            |error| error.to_string(),
646        );
647        self.emit(Event::Message(json!({
648            "jsonrpc": "2.0",
649            "id": request_id,
650            "error": {"code": INTERNAL_ERROR, "message": format!("MCP response stream failed: {reason}")},
651        })));
652    }
653
654    fn start_get_stream(&self) {
655        if self.inner.closed.load(Ordering::SeqCst)
656            || self.inner.get_stream_started.swap(true, Ordering::SeqCst)
657        {
658            return;
659        }
660        let transport = self.clone();
661        tokio::spawn(async move { transport.run_get_stream().await });
662    }
663
664    /// Keeps the server-to-client stream open, reconnecting with backoff.
665    async fn run_get_stream(&self) {
666        let mut cursor = Cursor::default();
667        let mut attempt = 0;
668        while !self.inner.closed.load(Ordering::SeqCst) {
669            match self.open_stream(cursor.last_event_id.clone()).await {
670                // The server offers no GET stream.
671                Ok(None) => return,
672                Ok(Some(response)) => {
673                    let opened = Instant::now();
674                    let result = self.consume(response, &mut cursor, None).await;
675                    if let Err(error) = result {
676                        if self.inner.closed.load(Ordering::SeqCst) {
677                            return;
678                        }
679                        if !retryable(&error) {
680                            self.emit(Event::Error(error.to_string()));
681                            return;
682                        }
683                    } else if cursor.received
684                        || opened.elapsed() > Duration::from_millis(RECONNECT_MAX_DELAY_MS)
685                    {
686                        attempt = 0;
687                    }
688                }
689                Err(error) => {
690                    if self.inner.closed.load(Ordering::SeqCst) {
691                        return;
692                    }
693                    if !retryable(&error) {
694                        self.emit(Event::Error(error.to_string()));
695                        return;
696                    }
697                }
698            }
699            cursor.received = false;
700            if attempt >= RECONNECT_MAX_RETRIES {
701                self.emit(Event::Error(
702                    "MCP server-to-client stream dropped and could not be reopened".into(),
703                ));
704                return;
705            }
706            let delay = reconnect_delay(attempt, cursor.retry_ms);
707            attempt += 1;
708            if !self.sleep(delay).await {
709                return;
710            }
711        }
712    }
713
714    /// Opens a GET stream, or `None` when the server answers 405.
715    async fn open_stream(
716        &self,
717        last_event_id: Option<String>,
718    ) -> Result<Option<reqwest::Response>, McpError> {
719        let mut headers = vec![(reqwest::header::ACCEPT, "text/event-stream".to_owned())];
720        if let Some(id) = last_event_id {
721            headers.push((HeaderName::from_static("last-event-id"), id));
722        }
723        let response = self.authorized(reqwest::Method::GET, None, headers).await?;
724        if response.status().as_u16() == 405 {
725            return Ok(None);
726        }
727        let response = self.check(response).await?;
728        self.capture_session(&response);
729        match content_type(&response).as_deref() {
730            Some("text/event-stream") => Ok(Some(response)),
731            other => Err(McpError::Http {
732                status: response.status().as_u16(),
733                message: format!(
734                    "Unsupported MCP GET response content type: {}",
735                    other.unwrap_or("missing")
736                ),
737            }),
738        }
739    }
740
741    /// False when the transport closed while waiting.
742    async fn sleep(&self, delay: Duration) -> bool {
743        tokio::select! {
744            () = self.inner.cancel.cancelled() => false,
745            () = tokio::time::sleep(delay) => true,
746        }
747    }
748}
749
750fn retryable(error: &McpError) -> bool {
751    match error {
752        McpError::Network(_) => true,
753        error => error
754            .status()
755            .is_some_and(|status| status == 408 || status == 429 || status >= 500),
756    }
757}
758
759fn reconnect_delay(attempt: u32, server_ms: Option<u64>) -> Duration {
760    let ms = server_ms.unwrap_or_else(|| {
761        RECONNECT_INITIAL_DELAY_MS
762            .saturating_mul(2u64.saturating_pow(attempt))
763            .min(RECONNECT_MAX_DELAY_MS)
764    });
765    Duration::from_millis(ms)
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771
772    /// The events of `chunks` as `(event, data)`, and where the stream ended.
773    fn parse(chunks: &[&str]) -> (Vec<(Option<String>, String)>, Cursor) {
774        let mut parser = SseParser::default();
775        let mut cursor = Cursor::default();
776        let mut events = Vec::new();
777        for chunk in chunks {
778            events.extend(parser.push(chunk.as_bytes(), &mut cursor).unwrap());
779        }
780        events.extend(parser.finish(&mut cursor).unwrap());
781        let events = events
782            .into_iter()
783            .map(|event| (event.event, event.data))
784            .collect();
785        (events, cursor)
786    }
787
788    #[test]
789    fn sse_line_endings() {
790        let (events, cursor) = parse(&[
791            "event: message\r\ndata: {\"a\":\r",
792            "\ndata: 1}\r\nid: 7\r\n\r\n",
793            ": comment\n\ndata:tail",
794        ]);
795        assert_eq!(
796            events,
797            [
798                (Some("message".into()), "{\"a\":\n1}".into()),
799                (None, "tail".into()),
800            ]
801        );
802        assert_eq!(cursor.last_event_id.as_deref(), Some("7"));
803    }
804
805    #[test]
806    fn sse_retry_and_ids() {
807        let (events, cursor) =
808            parse(&["retry: 2500\nid: 1\n\nretry: soon\nid: a\0b\nevent: ping\n\n"]);
809        assert!(events.is_empty());
810        assert_eq!(cursor.retry_ms, Some(2500));
811        assert_eq!(cursor.last_event_id.as_deref(), Some("1"));
812    }
813}