Skip to content

Commit 7d1ce52

Browse files
committed
fix: improve compatibility with non-compliant MCP servers (#413)
Addresses issues with servers like Shopify that violate MCP/HTTP specs: - Prioritize application/json in Accept headers to fix content-type issues - Handle empty notification bodies ({}) that should be bodiless per spec - Add status code validation and null safety improvements Resolves #406 Signed-off-by: Christian Tzolov <[email protected]>
1 parent 53f7b77 commit 7d1ce52

File tree

4 files changed

+60
-33
lines changed

4 files changed

+60
-33
lines changed

mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.modelcontextprotocol.spec.McpTransportSessionNotFoundException;
3131
import io.modelcontextprotocol.spec.McpTransportStream;
3232
import io.modelcontextprotocol.util.Assert;
33+
import io.modelcontextprotocol.util.Utils;
3334
import reactor.core.Disposable;
3435
import reactor.core.publisher.Flux;
3536
import reactor.core.publisher.Mono;
@@ -244,7 +245,7 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
244245

245246
Disposable connection = webClient.post()
246247
.uri(this.endpoint)
247-
.accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON)
248+
.accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)
248249
.headers(httpHeaders -> {
249250
transportSession.sessionId().ifPresent(id -> httpHeaders.add("mcp-session-id", id));
250251
})
@@ -387,9 +388,14 @@ private static String sessionIdOrPlaceholder(McpTransportSession<?> transportSes
387388
private Flux<McpSchema.JSONRPCMessage> responseFlux(ClientResponse response) {
388389
return response.bodyToMono(String.class).<Iterable<McpSchema.JSONRPCMessage>>handle((responseMessage, s) -> {
389390
try {
390-
McpSchema.JSONRPCMessage jsonRpcResponse = McpSchema.deserializeJsonRpcMessage(objectMapper,
391-
responseMessage);
392-
s.next(List.of(jsonRpcResponse));
391+
if (Utils.hasText(responseMessage) && !responseMessage.trim().equals("{}")) {
392+
McpSchema.JSONRPCMessage jsonRpcResponse = McpSchema.deserializeJsonRpcMessage(objectMapper,
393+
responseMessage);
394+
s.next(List.of(jsonRpcResponse));
395+
}
396+
else {
397+
logger.warn("Received empty response message: {}", responseMessage);
398+
}
393399
}
394400
catch (IOException e) {
395401
s.error(e);

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sendMessage) {
358358
String jsonBody = this.toString(sendMessage);
359359

360360
HttpRequest request = requestBuilder.uri(Utils.resolveUri(this.baseUri, this.endpoint))
361-
.header("Accept", TEXT_EVENT_STREAM + ", " + APPLICATION_JSON)
361+
.header("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
362362
.header("Content-Type", APPLICATION_JSON)
363363
.header("Cache-Control", "no-cache")
364364
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
@@ -436,11 +436,19 @@ else if (contentType.contains(TEXT_EVENT_STREAM)) {
436436
else if (contentType.contains(APPLICATION_JSON)) {
437437
messageSink.success();
438438
String data = ((ResponseSubscribers.AggregateResponseEvent) responseEvent).data();
439-
try {
440-
return Mono.just(McpSchema.deserializeJsonRpcMessage(objectMapper, data));
439+
if (Utils.hasText(data) && !data.trim().equals("{}")) {
440+
441+
try {
442+
return Mono.just(McpSchema.deserializeJsonRpcMessage(objectMapper, data));
443+
}
444+
catch (IOException e) {
445+
return Mono.error(e);
446+
}
441447
}
442-
catch (IOException e) {
443-
return Mono.error(e);
448+
else {
449+
// No content type means no response body
450+
logger.debug("No content type returned for POST in session {}", sessionRepresentation);
451+
return Mono.empty();
444452
}
445453
}
446454
logger.warn("Unknown media type {} returned for POST in session {}", contentType,

mcp/src/main/java/io/modelcontextprotocol/client/transport/ResponseSubscribers.java

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -135,36 +135,46 @@ protected void hookOnSubscribe(Subscription subscription) {
135135

136136
@Override
137137
protected void hookOnNext(String line) {
138-
if (line.isEmpty()) {
139-
// Empty line means end of event
140-
if (this.eventBuilder.length() > 0) {
141-
String eventData = this.eventBuilder.toString();
142-
SseEvent sseEvent = new SseEvent(currentEventId.get(), currentEventType.get(), eventData.trim());
143-
144-
this.sink.next(new SseResponseEvent(responseInfo, sseEvent));
145-
this.eventBuilder.setLength(0);
146-
}
147-
}
148-
else {
149-
if (line.startsWith("data:")) {
150-
var matcher = EVENT_DATA_PATTERN.matcher(line);
151-
if (matcher.find()) {
152-
this.eventBuilder.append(matcher.group(1).trim()).append("\n");
138+
if (this.responseInfo.statusCode() >= 200 && this.responseInfo.statusCode() < 300) {
139+
140+
if (line.isEmpty()) {
141+
// Empty line means end of event
142+
if (this.eventBuilder.length() > 0) {
143+
String eventData = this.eventBuilder.toString();
144+
SseEvent sseEvent = new SseEvent(currentEventId.get(), currentEventType.get(),
145+
eventData.trim());
146+
147+
this.sink.next(new SseResponseEvent(responseInfo, sseEvent));
148+
this.eventBuilder.setLength(0);
153149
}
154150
}
155-
else if (line.startsWith("id:")) {
156-
var matcher = EVENT_ID_PATTERN.matcher(line);
157-
if (matcher.find()) {
158-
this.currentEventId.set(matcher.group(1).trim());
151+
else {
152+
if (line.startsWith("data:")) {
153+
var matcher = EVENT_DATA_PATTERN.matcher(line);
154+
if (matcher.find()) {
155+
this.eventBuilder.append(matcher.group(1).trim()).append("\n");
156+
}
159157
}
160-
}
161-
else if (line.startsWith("event:")) {
162-
var matcher = EVENT_TYPE_PATTERN.matcher(line);
163-
if (matcher.find()) {
164-
this.currentEventType.set(matcher.group(1).trim());
158+
else if (line.startsWith("id:")) {
159+
var matcher = EVENT_ID_PATTERN.matcher(line);
160+
if (matcher.find()) {
161+
this.currentEventId.set(matcher.group(1).trim());
162+
}
163+
}
164+
else if (line.startsWith("event:")) {
165+
var matcher = EVENT_TYPE_PATTERN.matcher(line);
166+
if (matcher.find()) {
167+
this.currentEventType.set(matcher.group(1).trim());
168+
}
165169
}
166170
}
167171
}
172+
else {
173+
// If the response is not successful, emit an error
174+
System.out.println("Received non-successful response: " + this.responseInfo.statusCode());
175+
SseEvent sseEvent = new SseEvent(null, null, null);
176+
this.sink.next(new SseResponseEvent(responseInfo, sseEvent));
177+
}
168178
}
169179

170180
@Override

mcp/src/main/java/io/modelcontextprotocol/util/Utils.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ public static boolean isEmpty(@Nullable Map<?, ?> map) {
6969
* base URL or URI is malformed
7070
*/
7171
public static URI resolveUri(URI baseUrl, String endpointUrl) {
72+
if (!Utils.hasText(endpointUrl)) {
73+
return baseUrl;
74+
}
7275
URI endpointUri = URI.create(endpointUrl);
7376
if (endpointUri.isAbsolute() && !isUnderBaseUri(baseUrl, endpointUri)) {
7477
throw new IllegalArgumentException("Absolute endpoint URL does not match the base URL.");

0 commit comments

Comments
 (0)