Skip to content

Commit f5aab36

Browse files
authored
Fix SSE event classification to follow spec for missing event field (#913)
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 fd00498 commit f5aab36

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
@@ -114,6 +114,29 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
114114

115115
public static int BAD_REQUEST = 400;
116116

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

119142
private final URI baseUri;
@@ -323,7 +346,7 @@ else if (statusCode == METHOD_NOT_ALLOWED) {
323346
+ statusCode));
324347
}
325348
else if (statusCode >= 200 && statusCode < 300) {
326-
if (MESSAGE_EVENT_TYPE.equals(sseResponseEvent.sseEvent().event())) {
349+
if (isMessageEvent(sseResponseEvent.sseEvent().event())) {
327350
String data = sseResponseEvent.sseEvent().data();
328351
// Per 2025-11-25 spec (SEP-1699), servers may
329352
// 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)