Skip to content

Commit 080098e

Browse files
committed
Client: handle server responses with Content-Length: 0
- When the client sends `notification/initalized`, servers must respond with HTTP 202 and an empty body. We checked for the absence of a Content-Type header to verify whether the body was empty. - However, some servers will send an empty body with a Content-Type header, and that header may have an unsupported, default type such as `text/html` or `text/plain`. - Now we we also use the Content-Length header to check for an empty body. This header is optional in HTTP/2, so we do not make it our primary mechanism for detecting empty bodies. - As part of this PR, we also move hard-coded HTTP header names to the HttpHeaders interface. While they are not defined by the MCP spec, they are used by it and are core to implementing the protocol. Therefore, they have their place in a core interface. - Fixes #582 Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent 12292ab commit 080098e

File tree

4 files changed

+48
-17
lines changed

4 files changed

+48
-17
lines changed

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@
1818

1919
import org.slf4j.Logger;
2020
import org.slf4j.LoggerFactory;
21-
22-
import io.modelcontextprotocol.json.McpJsonMapper;
23-
import io.modelcontextprotocol.json.TypeRef;
24-
21+
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
2522
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
2623
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
27-
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
2824
import io.modelcontextprotocol.common.McpTransportContext;
25+
import io.modelcontextprotocol.json.McpJsonMapper;
26+
import io.modelcontextprotocol.json.TypeRef;
27+
import io.modelcontextprotocol.spec.HttpHeaders;
2928
import io.modelcontextprotocol.spec.McpClientTransport;
3029
import io.modelcontextprotocol.spec.McpSchema;
3130
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
@@ -469,7 +468,7 @@ private Mono<HttpResponse<String>> sendHttpPost(final String endpoint, final Str
469468
return Mono.deferContextual(ctx -> {
470469
var builder = this.requestBuilder.copy()
471470
.uri(requestUri)
472-
.header("Content-Type", "application/json")
471+
.header(HttpHeaders.CONTENT_TYPE, "application/json")
473472
.header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION)
474473
.POST(HttpRequest.BodyPublishers.ofString(body));
475474
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ private Mono<Disposable> reconnect(McpTransportStream<Disposable> stream) {
246246
}
247247

248248
var builder = requestBuilder.uri(uri)
249-
.header("Accept", TEXT_EVENT_STREAM)
249+
.header(HttpHeaders.ACCEPT, TEXT_EVENT_STREAM)
250250
.header("Cache-Control", "no-cache")
251251
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
252252
.GET();
@@ -371,7 +371,7 @@ private BodyHandler<Void> toSendMessageBodySubscriber(FluxSink<ResponseEvent> si
371371

372372
BodyHandler<Void> responseBodyHandler = responseInfo -> {
373373

374-
String contentType = responseInfo.headers().firstValue("Content-Type").orElse("").toLowerCase();
374+
String contentType = responseInfo.headers().firstValue(HttpHeaders.CONTENT_TYPE).orElse("").toLowerCase();
375375

376376
if (contentType.contains(TEXT_EVENT_STREAM)) {
377377
// For SSE streams, use line subscriber that returns Void
@@ -420,9 +420,9 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
420420
}
421421

422422
var builder = requestBuilder.uri(uri)
423-
.header("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
424-
.header("Content-Type", APPLICATION_JSON)
425-
.header("Cache-Control", "no-cache")
423+
.header(HttpHeaders.ACCEPT, APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
424+
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
425+
.header(HttpHeaders.CACHE_CONTROL, "no-cache")
426426
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
427427
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
428428
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
@@ -459,15 +459,19 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
459459

460460
String contentType = responseEvent.responseInfo()
461461
.headers()
462-
.firstValue("Content-Type")
462+
.firstValue(HttpHeaders.CONTENT_TYPE)
463463
.orElse("")
464464
.toLowerCase();
465465

466-
if (contentType.isBlank()) {
467-
logger.debug("No content type returned for POST in session {}", sessionRepresentation);
466+
String contentLength = responseEvent.responseInfo()
467+
.headers()
468+
.firstValue(HttpHeaders.CONTENT_LENGTH)
469+
.orElse(null);
470+
471+
if (contentType.isBlank() || "0".equals(contentLength)) {
472+
logger.debug("No body returned for POST in session {}", sessionRepresentation);
468473
// No content type means no response body, so we can just
469-
// return
470-
// an empty stream
474+
// return an empty stream
471475
deliveredSink.success();
472476
return Flux.empty();
473477
}

mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,31 @@ public interface HttpHeaders {
2626
*/
2727
String PROTOCOL_VERSION = "MCP-Protocol-Version";
2828

29+
/**
30+
* The HTTP Content-Length header.
31+
* @see <a href=
32+
* "https://httpwg.org/specs/rfc9110.html#field.content-length">RFC9110</a>
33+
*/
34+
String CONTENT_LENGTH = "Content-Length";
35+
36+
/**
37+
* The HTTP Content-Type header.
38+
* @see <a href=
39+
* "https://httpwg.org/specs/rfc9110.html#field.content-type">RFC9110</a>
40+
*/
41+
String CONTENT_TYPE = "Content-Type";
42+
43+
/**
44+
* The HTTP Accept header.
45+
* @see <a href= "https://httpwg.org/specs/rfc9110.html#field.accept">RFC9110</a>
46+
*/
47+
String ACCEPT = "Accept";
48+
49+
/**
50+
* The HTTP Cache-Control header.
51+
* @see <a href=
52+
* "https://httpwg.org/specs/rfc9111.html#field.cache-control">RFC9111</a>
53+
*/
54+
String CACHE_CONTROL = "Cache-Control";
55+
2956
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,10 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
293293
// 200 OK for notifications
294294
if (response.statusCode().is2xxSuccessful()) {
295295
Optional<MediaType> contentType = response.headers().contentType();
296+
long contentLength = response.headers().contentLength().orElse(-1);
296297
// Existing SDKs consume notifications with no response body nor
297298
// content type
298-
if (contentType.isEmpty()) {
299+
if (contentType.isEmpty() || contentLength == 0) {
299300
logger.trace("Message was successfully sent via POST for session {}",
300301
sessionRepresentation);
301302
// signal the caller that the message was successfully

0 commit comments

Comments
 (0)