Skip to content

Commit cfa72e4

Browse files
iaJingdaKehrlann
authored andcommitted
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 fd00498 commit cfa72e4

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)