diff --git a/codex-rs/rmcp-client/src/http_client_adapter.rs b/codex-rs/rmcp-client/src/http_client_adapter.rs index 46d8e043ee..189da4290c 100644 --- a/codex-rs/rmcp-client/src/http_client_adapter.rs +++ b/codex-rs/rmcp-client/src/http_client_adapter.rs @@ -45,6 +45,10 @@ mod www_authenticate; use self::www_authenticate::insufficient_scope_challenge; +#[cfg(test)] +#[path = "http_client_adapter_tests.rs"] +mod tests; + const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream"; const JSON_MIME_TYPE: &str = "application/json"; const HEADER_SESSION_ID: &str = "Mcp-Session-Id"; @@ -215,7 +219,14 @@ impl StreamableHttpClient for StreamableHttpClientAdapter { } Some(content_type) if content_type.starts_with(JSON_MIME_TYPE) => { let body = collect_body(&mut body_stream).await?; - if body.is_empty() && matches!(message, JsonRpcMessage::Notification(_)) { + if body.is_empty() + && matches!( + message, + JsonRpcMessage::Response(_) + | JsonRpcMessage::Notification(_) + | JsonRpcMessage::Error(_) + ) + { return Ok(StreamableHttpPostResponse::Accepted); } let message: ServerJsonRpcMessage = diff --git a/codex-rs/rmcp-client/src/http_client_adapter_tests.rs b/codex-rs/rmcp-client/src/http_client_adapter_tests.rs new file mode 100644 index 0000000000..08b01ac055 --- /dev/null +++ b/codex-rs/rmcp-client/src/http_client_adapter_tests.rs @@ -0,0 +1,68 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use codex_exec_server::Environment; +use reqwest::header::HeaderMap; +use rmcp::ErrorData; +use rmcp::model::ClientJsonRpcMessage; +use rmcp::model::ClientResult; +use rmcp::model::RequestId; +use rmcp::transport::streamable_http_client::StreamableHttpClient; +use rmcp::transport::streamable_http_client::StreamableHttpPostResponse; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; + +use super::StreamableHttpClientAdapter; + +#[tokio::test] +async fn accepts_empty_ok_for_one_way_client_messages() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/mcp")) + .respond_with(ResponseTemplate::new(200).insert_header("content-type", "application/json")) + .expect(2) + .mount(&server) + .await; + + let adapter = StreamableHttpClientAdapter::new( + Environment::default_for_tests().get_http_client(), + HeaderMap::new(), + /*auth_provider*/ None, + ); + let messages = [ + ( + "response", + ClientJsonRpcMessage::response( + ClientResult::empty(()), + RequestId::String("response-id".into()), + ), + ), + ( + "error", + ClientJsonRpcMessage::error( + ErrorData::internal_error("test error", /*data*/ None), + Some(RequestId::String("error-id".into())), + ), + ), + ]; + + for (message_kind, message) in messages { + let result = adapter + .post_message( + Arc::from(format!("{}/mcp", server.uri())), + message, + /*session_id*/ None, + /*auth_token*/ None, + HashMap::new(), + ) + .await; + + assert!( + matches!(result, Ok(StreamableHttpPostResponse::Accepted)), + "expected empty response for {message_kind} to be accepted, got {result:?}" + ); + } +}