Skip to content

Commit d35032f

Browse files
committed
fix: preserve negotiated progress responses
1 parent 9df629e commit d35032f

2 files changed

Lines changed: 80 additions & 1 deletion

File tree

crates/rmcp/src/transport/streamable_http_server/tower.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,13 @@ where
869869
))
870870
})?;
871871

872-
if self.config.json_response || jsonrpc_http_status(&first) != http::StatusCode::OK {
872+
let terminal = matches!(
873+
&first,
874+
ServerJsonRpcMessage::Response(_) | ServerJsonRpcMessage::Error(_)
875+
);
876+
if jsonrpc_http_status(&first) != http::StatusCode::OK
877+
|| (self.config.json_response && terminal)
878+
{
873879
return jsonrpc_message_response(first, true);
874880
}
875881

crates/rmcp/tests/test_streamable_http_json_response.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,24 @@ use common::calculator::Calculator;
1717

1818
const INIT_BODY: &str = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}"#;
1919
const CALL_WITH_PROGRESS_BODY: &str = r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"progress","arguments":{},"_meta":{"progressToken":"progress-test-1"}}}"#;
20+
const NEGOTIATED_CALL_WITH_PROGRESS_BODY: &str = r#"{
21+
"jsonrpc": "2.0",
22+
"id": 2,
23+
"method": "tools/call",
24+
"params": {
25+
"name": "progress",
26+
"arguments": {},
27+
"_meta": {
28+
"progressToken": "progress-test-1",
29+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
30+
"io.modelcontextprotocol/clientInfo": {
31+
"name": "test",
32+
"version": "1.0"
33+
},
34+
"io.modelcontextprotocol/clientCapabilities": {}
35+
}
36+
}
37+
}"#;
2038

2139
#[derive(Clone)]
2240
struct ProgressServer;
@@ -192,6 +210,61 @@ async fn stateless_json_response_falls_back_to_sse_for_progress() -> anyhow::Res
192210
Ok(())
193211
}
194212

213+
#[tokio::test]
214+
async fn stateless_negotiated_json_response_falls_back_to_sse_for_progress() -> anyhow::Result<()> {
215+
let ct = CancellationToken::new();
216+
let (client, url, ct) = spawn_progress_server(
217+
StreamableHttpServerConfig::default()
218+
.with_stateful_mode(false)
219+
.with_json_response(true)
220+
.with_sse_keep_alive(None)
221+
.with_cancellation_token(ct.child_token()),
222+
)
223+
.await;
224+
225+
let response = client
226+
.post(&url)
227+
.header("Content-Type", "application/json")
228+
.header("Accept", "application/json, text/event-stream")
229+
.header("MCP-Protocol-Version", "2026-07-28")
230+
.header("Mcp-Method", "tools/call")
231+
.header("Mcp-Name", "progress")
232+
.body(NEGOTIATED_CALL_WITH_PROGRESS_BODY)
233+
.send()
234+
.await?;
235+
236+
assert_eq!(response.status(), 200);
237+
238+
let content_type = response
239+
.headers()
240+
.get("content-type")
241+
.and_then(|value| value.to_str().ok())
242+
.unwrap_or("");
243+
assert!(
244+
content_type.contains("text/event-stream"),
245+
"Expected SSE fallback, got: {content_type}"
246+
);
247+
248+
let body = response.text().await?;
249+
let messages: Vec<serde_json::Value> = body
250+
.lines()
251+
.filter_map(|line| line.strip_prefix("data:"))
252+
.map(str::trim)
253+
.filter(|data| !data.is_empty())
254+
.map(serde_json::from_str)
255+
.collect::<Result<_, _>>()?;
256+
assert_eq!(messages.len(), 2, "Expected progress and result: {body}");
257+
assert_eq!(messages[0]["method"], "notifications/progress");
258+
assert_eq!(messages[1]["id"], 2);
259+
assert!(
260+
messages[1]["result"].is_object(),
261+
"Expected result object: {body}"
262+
);
263+
264+
ct.cancel();
265+
Ok(())
266+
}
267+
195268
#[tokio::test]
196269
async fn stateless_sse_mode_default_unchanged() -> anyhow::Result<()> {
197270
let ct = CancellationToken::new();

0 commit comments

Comments
 (0)