//! RMCP Streamable HTTP adapter built on top of the shared `HttpClient` //! capability. //! //! This module runs in the orchestrator process. It turns high-level RMCP //! operations like `post_message` and `get_stream` into calls on //! `Arc`, which may be: //! - a local HTTP client that issues requests from the orchestrator, or //! - a remote HTTP client that forwards requests to the remote runtime use std::collections::HashMap; use std::io; use std::sync::Arc; use bytes::Bytes; use codex_api::SharedAuthProvider; use codex_exec_server::ExecServerError; use codex_exec_server::HttpClient; use codex_exec_server::HttpHeader; use codex_exec_server::HttpRequestParams; use codex_exec_server::HttpResponseBodyStream; use futures::StreamExt; use futures::stream; use futures::stream::BoxStream; use reqwest::StatusCode; use reqwest::header::ACCEPT; use reqwest::header::AUTHORIZATION; use reqwest::header::CONTENT_TYPE; use reqwest::header::HeaderMap; use reqwest::header::HeaderName; use rmcp::model::ClientJsonRpcMessage; use rmcp::model::JsonRpcMessage; use rmcp::model::ServerJsonRpcMessage; use rmcp::transport::streamable_http_client::AuthRequiredError; use rmcp::transport::streamable_http_client::InsufficientScopeError; use rmcp::transport::streamable_http_client::StreamableHttpClient; use rmcp::transport::streamable_http_client::StreamableHttpError; use rmcp::transport::streamable_http_client::StreamableHttpPostResponse; use sse_stream::Sse; use sse_stream::SseStream; mod www_authenticate; use self::www_authenticate::insufficient_scope_challenge; const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream"; const JSON_MIME_TYPE: &str = "application/json"; const HEADER_SESSION_ID: &str = "Mcp-Session-Id"; const NON_JSON_RESPONSE_BODY_PREVIEW_BYTES: usize = 8_192; #[derive(Clone)] pub(crate) struct StreamableHttpClientAdapter { http_client: Arc, default_headers: HeaderMap, auth_provider: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum StreamableHttpClientAdapterError { #[error("streamable HTTP session expired with 404 Not Found")] SessionExpired404, #[error(transparent)] HttpRequest(#[from] ExecServerError), #[error("invalid HTTP header: {0}")] Header(String), } impl StreamableHttpClientAdapter { pub(crate) fn new( http_client: Arc, default_headers: HeaderMap, auth_provider: Option, ) -> Self { Self { http_client, default_headers, auth_provider, } } } impl StreamableHttpClient for StreamableHttpClientAdapter { type Error = StreamableHttpClientAdapterError; async fn post_message( &self, uri: Arc, message: ClientJsonRpcMessage, session_id: Option>, auth_token: Option, custom_headers: HashMap, ) -> std::result::Result> { let (mcp_method, mcp_request_id) = client_jsonrpc_message_fields(&message); let has_session_id = session_id.is_some(); let mut headers = self.default_headers.clone(); headers.extend(custom_headers); self.add_auth_headers(&mut headers); insert_header( &mut headers, ACCEPT, [EVENT_STREAM_MIME_TYPE, JSON_MIME_TYPE].join(", "), StreamableHttpClientAdapterError::Header, )?; insert_header( &mut headers, CONTENT_TYPE, JSON_MIME_TYPE.to_string(), StreamableHttpClientAdapterError::Header, )?; if let Some(auth_token) = auth_token { insert_header( &mut headers, AUTHORIZATION, format!("Bearer {auth_token}"), StreamableHttpClientAdapterError::Header, )?; } if let Some(session_id_value) = session_id.as_ref() { insert_header( &mut headers, HeaderName::from_static("mcp-session-id"), session_id_value.to_string(), StreamableHttpClientAdapterError::Header, )?; } let body = serde_json::to_vec(&message).map_err(StreamableHttpError::Deserialize)?; let has_authorization_header = headers.contains_key(AUTHORIZATION); let response = self .http_client .http_request_stream(HttpRequestParams { method: "POST".to_string(), url: uri.to_string(), headers: protocol_headers(&headers), body: Some(body.into()), timeout_ms: None, request_id: "buffered-request".to_string(), stream_response: true, }) .await; let (response, mut body_stream) = match response { Ok(response) => response, Err(error) => { log_post_message_http_error( &uri, mcp_method.as_deref(), mcp_request_id.as_deref(), has_session_id, has_authorization_header, ); return Err(StreamableHttpError::Client( StreamableHttpClientAdapterError::from(error), )); } }; if response.status == StatusCode::NOT_FOUND.as_u16() && session_id.is_some() { return Err(StreamableHttpError::Client( StreamableHttpClientAdapterError::SessionExpired404, )); } if response.status == StatusCode::UNAUTHORIZED.as_u16() && let Some(header) = response_header(&response.headers, reqwest::header::WWW_AUTHENTICATE) { return Err(StreamableHttpError::AuthRequired(AuthRequiredError::new( header, ))); } if response.status == StatusCode::FORBIDDEN.as_u16() && let Some(challenge) = insufficient_scope_challenge(&response.headers) { return Err(StreamableHttpError::InsufficientScope( InsufficientScopeError::new( challenge.www_authenticate_header, challenge.required_scope, ), )); } if matches!( StatusCode::from_u16(response.status).ok(), Some(StatusCode::ACCEPTED | StatusCode::NO_CONTENT) ) { return Ok(StreamableHttpPostResponse::Accepted); } let content_type = response_header(&response.headers, CONTENT_TYPE); let session_id = response_header(&response.headers, HEADER_SESSION_ID); match content_type.as_deref() { Some(content_type) if content_type.starts_with(EVENT_STREAM_MIME_TYPE) => { let event_stream = sse_stream_from_body(body_stream); Ok(StreamableHttpPostResponse::Sse(event_stream, session_id)) } Some(content_type) if content_type.starts_with(JSON_MIME_TYPE) => { let body = collect_body(&mut body_stream).await?; let message: ServerJsonRpcMessage = serde_json::from_slice(&body).map_err(StreamableHttpError::Deserialize)?; Ok(StreamableHttpPostResponse::Json(message, session_id)) } _ => { let body = collect_body(&mut body_stream).await?; let content_type = content_type.unwrap_or_else(|| "missing-content-type".into()); Err(StreamableHttpError::UnexpectedContentType(Some(format!( "{content_type}; body: {}", body_preview(String::from_utf8_lossy(&body).to_string()) )))) } } } async fn delete_session( &self, uri: Arc, session: Arc, auth_token: Option, custom_headers: HashMap, ) -> std::result::Result<(), StreamableHttpError> { let mut headers = self.default_headers.clone(); headers.extend(custom_headers); self.add_auth_headers(&mut headers); if let Some(auth_token) = auth_token { insert_header( &mut headers, AUTHORIZATION, format!("Bearer {auth_token}"), StreamableHttpClientAdapterError::Header, )?; } insert_header( &mut headers, HeaderName::from_static("mcp-session-id"), session.to_string(), StreamableHttpClientAdapterError::Header, )?; let response = self .http_client .http_request(HttpRequestParams { method: "DELETE".to_string(), url: uri.to_string(), headers: protocol_headers(&headers), body: None, timeout_ms: None, request_id: "buffered-request".to_string(), stream_response: false, }) .await .map_err(StreamableHttpClientAdapterError::from) .map_err(StreamableHttpError::Client)?; if response.status == StatusCode::METHOD_NOT_ALLOWED.as_u16() { return Ok(()); } if !status_is_success(response.status) { return Err(StreamableHttpError::UnexpectedServerResponse( format!("DELETE returned HTTP {}", response.status).into(), )); } Ok(()) } async fn get_stream( &self, uri: Arc, session_id: Arc, last_event_id: Option, auth_token: Option, custom_headers: HashMap, ) -> std::result::Result< BoxStream<'static, std::result::Result>, StreamableHttpError, > { let mut headers = self.default_headers.clone(); headers.extend(custom_headers); self.add_auth_headers(&mut headers); insert_header( &mut headers, ACCEPT, [EVENT_STREAM_MIME_TYPE, JSON_MIME_TYPE].join(", "), StreamableHttpClientAdapterError::Header, )?; insert_header( &mut headers, HeaderName::from_static("mcp-session-id"), session_id.to_string(), StreamableHttpClientAdapterError::Header, )?; if let Some(last_event_id) = last_event_id { insert_header( &mut headers, HeaderName::from_static("last-event-id"), last_event_id, StreamableHttpClientAdapterError::Header, )?; } if let Some(auth_token) = auth_token { insert_header( &mut headers, AUTHORIZATION, format!("Bearer {auth_token}"), StreamableHttpClientAdapterError::Header, )?; } let (response, body_stream) = self .http_client .http_request_stream(HttpRequestParams { method: "GET".to_string(), url: uri.to_string(), headers: protocol_headers(&headers), body: None, timeout_ms: None, request_id: "buffered-request".to_string(), stream_response: true, }) .await .map_err(StreamableHttpClientAdapterError::from) .map_err(StreamableHttpError::Client)?; if response.status == StatusCode::METHOD_NOT_ALLOWED.as_u16() { return Err(StreamableHttpError::ServerDoesNotSupportSse); } if response.status == StatusCode::NOT_FOUND.as_u16() { return Err(StreamableHttpError::Client( StreamableHttpClientAdapterError::SessionExpired404, )); } if !status_is_success(response.status) { return Err(StreamableHttpError::UnexpectedServerResponse( format!("GET returned HTTP {}", response.status).into(), )); } match response_header(&response.headers, CONTENT_TYPE).as_deref() { Some(content_type) if is_streamable_http_content_type(content_type) => {} Some(content_type) => { return Err(StreamableHttpError::UnexpectedContentType(Some( content_type.to_string(), ))); } None => { return Err(StreamableHttpError::UnexpectedContentType(None)); } } Ok(sse_stream_from_body(body_stream)) } } impl StreamableHttpClientAdapter { fn add_auth_headers(&self, headers: &mut HeaderMap) { if let Some(auth_provider) = &self.auth_provider { headers.extend(auth_provider.to_auth_headers()); } } } fn body_preview(body: impl Into) -> String { let mut body_preview = body.into(); let body_len = body_preview.len(); if body_len > NON_JSON_RESPONSE_BODY_PREVIEW_BYTES { let mut boundary = NON_JSON_RESPONSE_BODY_PREVIEW_BYTES; while !body_preview.is_char_boundary(boundary) { boundary = boundary.saturating_sub(1); } body_preview.truncate(boundary); body_preview.push_str(&format!( "... (truncated {} bytes)", body_len.saturating_sub(boundary) )); } body_preview } fn client_jsonrpc_message_fields( message: &ClientJsonRpcMessage, ) -> (Option, Option) { match message { JsonRpcMessage::Request(request) => ( Some(request.request.method().to_string()), Some(request.id.to_string()), ), JsonRpcMessage::Response(response) => (None, Some(response.id.to_string())), JsonRpcMessage::Notification(_) => (None, None), JsonRpcMessage::Error(error) => (None, error.id.as_ref().map(ToString::to_string)), } } fn log_post_message_http_error( uri: &str, mcp_method: Option<&str>, mcp_request_id: Option<&str>, has_session_id: bool, has_authorization_header: bool, ) { let parsed_url = reqwest::Url::parse(uri).ok(); tracing::warn!( endpoint_scheme = parsed_url .as_ref() .map(reqwest::Url::scheme) .unwrap_or(""), endpoint_host = parsed_url .as_ref() .and_then(reqwest::Url::host_str) .unwrap_or(""), endpoint_path = parsed_url .as_ref() .map(reqwest::Url::path) .unwrap_or(""), endpoint_has_query = parsed_url.as_ref().is_some_and(|url| url.query().is_some()), mcp_method = mcp_method.unwrap_or(""), mcp_request_id = mcp_request_id.unwrap_or(""), has_session_id = has_session_id, has_authorization_header = has_authorization_header, "streamable HTTP post_message failed" ); } fn insert_header( headers: &mut HeaderMap, name: HeaderName, value: String, map_error: impl FnOnce(String) -> Error, ) -> std::result::Result<(), StreamableHttpError> where Error: std::error::Error + Send + Sync + 'static, { let value = reqwest::header::HeaderValue::from_str(&value) .map_err(|error| StreamableHttpError::Client(map_error(error.to_string())))?; headers.insert(name, value); Ok(()) } fn is_streamable_http_content_type(content_type: &str) -> bool { content_type .as_bytes() .starts_with(EVENT_STREAM_MIME_TYPE.as_bytes()) || content_type .as_bytes() .starts_with(JSON_MIME_TYPE.as_bytes()) } fn protocol_headers(headers: &HeaderMap) -> Vec { headers .iter() .filter_map(|(name, value)| { Some(HttpHeader { name: name.as_str().to_string(), value: value.to_str().ok()?.to_string(), }) }) .collect() } fn response_header(headers: &[HttpHeader], name: impl AsRef) -> Option { let name = name.as_ref(); headers .iter() .find(|header| header.name.eq_ignore_ascii_case(name)) .map(|header| header.value.clone()) } fn status_is_success(status: u16) -> bool { StatusCode::from_u16(status).is_ok_and(|status| status.is_success()) } async fn collect_body( body_stream: &mut HttpResponseBodyStream, ) -> std::result::Result, StreamableHttpError> { let mut body = Vec::new(); while let Some(chunk) = body_stream .recv() .await .map_err(StreamableHttpClientAdapterError::from) .map_err(StreamableHttpError::Client)? { body.extend_from_slice(&chunk); } Ok(body) } fn sse_stream_from_body( body_stream: HttpResponseBodyStream, ) -> BoxStream<'static, std::result::Result> { SseStream::from_byte_stream(stream::unfold(body_stream, |mut body_stream| async move { match body_stream.recv().await { Ok(Some(bytes)) => Some((Ok(Bytes::from(bytes)), body_stream)), Ok(None) => None, Err(error) => Some((Err(io::Error::other(error)), body_stream)), } })) .boxed() }