Skip to content

Commit 3c200d3

Browse files
committed
Fix SSE event classification to follow spec for missing event field
Per the SSE specification (WHATWG HTML Living Standard 9.2.6), an event with no explicit event field MUST be dispatched as a message event. HttpClientStreamableHttpTransport previously used strict equality and silently dropped such frames in the reconnect/GET stream path, causing server-initiated notifications to never reach the handler. Extract classification into a package-private isMessageEvent helper and cover with parameterized unit tests. Closes gh-885
1 parent 8fd9903 commit 3c200d3

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,29 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
112112

113113
public static int BAD_REQUEST = 400;
114114

115+
/**
116+
* Determines whether an SSE event should be treated as a "message" event carrying a
117+
* JSON-RPC payload.
118+
*
119+
* <p>
120+
* Per the <a href=
121+
* "https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation">
122+
* SSE specification (WHATWG HTML Living Standard §9.2.6)</a>, an event with no
123+
* explicit {@code event:} field MUST be dispatched as a {@code message} event by
124+
* default. This method applies that rule by treating {@code null} or empty event
125+
* names as equivalent to {@link #MESSAGE_EVENT_TYPE}.
126+
*
127+
* <p>
128+
* This alignment ensures interoperability with MCP servers that emit bare
129+
* {@code data:} frames without an accompanying {@code event:} line, which are valid
130+
* per the SSE spec.
131+
* @param eventName the SSE event name, which may be {@code null} or empty
132+
* @return {@code true} if the event should be parsed as a JSON-RPC message
133+
*/
134+
static boolean isMessageEvent(String eventName) {
135+
return eventName == null || eventName.isEmpty() || MESSAGE_EVENT_TYPE.equals(eventName);
136+
}
137+
115138
private final McpJsonMapper jsonMapper;
116139

117140
private final URI baseUri;
@@ -311,7 +334,7 @@ else if (statusCode == METHOD_NOT_ALLOWED) {
311334
+ statusCode));
312335
}
313336
else if (statusCode >= 200 && statusCode < 300) {
314-
if (MESSAGE_EVENT_TYPE.equals(sseResponseEvent.sseEvent().event())) {
337+
if (isMessageEvent(sseResponseEvent.sseEvent().event())) {
315338
String data = sseResponseEvent.sseEvent().data();
316339
// Per 2025-11-25 spec (SEP-1699), servers may
317340
// send SSE events
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2024-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport;
6+
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.params.ParameterizedTest;
9+
import org.junit.jupiter.params.provider.NullAndEmptySource;
10+
import org.junit.jupiter.params.provider.ValueSource;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
/**
15+
* Unit tests for {@link HttpClientStreamableHttpTransport#isMessageEvent(String)}.
16+
*
17+
* <p>
18+
* Verifies that SSE event classification follows the <a href=
19+
* "https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation">
20+
* WHATWG HTML Living Standard §9.2.6</a>: an event without an explicit {@code event:}
21+
* field must be dispatched as a {@code message} event.
22+
*
23+
* @author jiajingda
24+
* @see <a href="https://github.com/modelcontextprotocol/java-sdk/issues/885">#885</a>
25+
*/
26+
class HttpClientStreamableHttpTransportSseEventTypeTest {
27+
28+
@ParameterizedTest
29+
@NullAndEmptySource
30+
void shouldTreatNullOrEmptyEventAsMessage(String eventName) {
31+
assertThat(HttpClientStreamableHttpTransport.isMessageEvent(eventName))
32+
.as("SSE frame with null/empty event field must be treated as a 'message' event per SSE spec")
33+
.isTrue();
34+
}
35+
36+
@Test
37+
void shouldTreatExplicitMessageEventAsMessage() {
38+
assertThat(HttpClientStreamableHttpTransport.isMessageEvent("message"))
39+
.as("Explicit 'message' event must be parsed as a JSON-RPC message")
40+
.isTrue();
41+
}
42+
43+
@ParameterizedTest
44+
@ValueSource(strings = { "ping", "error", "notification", "MESSAGE", "Message", "custom-event" })
45+
void shouldNotTreatOtherEventsAsMessage(String eventName) {
46+
assertThat(HttpClientStreamableHttpTransport.isMessageEvent(eventName))
47+
.as("Non-'message' SSE event '%s' must not be parsed as a JSON-RPC message", eventName)
48+
.isFalse();
49+
}
50+
51+
}

0 commit comments

Comments
 (0)