Skip to content

Commit 2f91945

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feature/fix-httpclient-resource-management
# Conflicts: # mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
2 parents 150b758 + 06fdc71 commit 2f91945

File tree

50 files changed

+1592
-511
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1592
-511
lines changed

mcp-bom/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<parent>
88
<groupId>io.modelcontextprotocol.sdk</groupId>
99
<artifactId>mcp-parent</artifactId>
10-
<version>0.15.0-SNAPSHOT</version>
10+
<version>0.17.0-SNAPSHOT</version>
1111
</parent>
1212

1313
<artifactId>mcp-bom</artifactId>

mcp-core/pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>io.modelcontextprotocol.sdk</groupId>
88
<artifactId>mcp-parent</artifactId>
9-
<version>0.15.0-SNAPSHOT</version>
9+
<version>0.17.0-SNAPSHOT</version>
1010
</parent>
1111
<artifactId>mcp-core</artifactId>
1212
<packaging>jar</packaging>
@@ -68,7 +68,7 @@
6868
<dependency>
6969
<groupId>io.modelcontextprotocol.sdk</groupId>
7070
<artifactId>mcp-json</artifactId>
71-
<version>0.15.0-SNAPSHOT</version>
71+
<version>0.17.0-SNAPSHOT</version>
7272
</dependency>
7373

7474
<dependency>
@@ -101,7 +101,7 @@
101101
<dependency>
102102
<groupId>io.modelcontextprotocol.sdk</groupId>
103103
<artifactId>mcp-json-jackson2</artifactId>
104-
<version>0.15.0-SNAPSHOT</version>
104+
<version>0.17.0-SNAPSHOT</version>
105105
<scope>test</scope>
106106
</dependency>
107107

mcp-core/src/main/java/io/modelcontextprotocol/client/LifecycleInitializer.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,9 @@ public <T> Mono<T> withInitialization(String actionName, Function<Initialization
287287
this.initializationRef.compareAndSet(newInit, null);
288288
return Mono.error(new RuntimeException("Client failed to initialize " + actionName, ex));
289289
})
290-
.flatMap(operation);
290+
.flatMap(res -> operation.apply(res)
291+
.contextWrite(c -> c.put(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
292+
res.initializeResult().protocolVersion())));
291293
});
292294
}
293295

@@ -319,6 +321,8 @@ private Mono<McpSchema.InitializeResult> doInitialize(DefaultInitialization init
319321
}
320322

321323
return mcpClientSession.sendNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED, null)
324+
.contextWrite(
325+
c -> c.put(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION, initializeResult.protocolVersion()))
322326
.thenReturn(initializeResult);
323327
}).flatMap(initializeResult -> {
324328
initialization.cacheResult(initializeResult);

mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ public class McpAsyncClient {
106106
public static final TypeRef<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() {
107107
};
108108

109+
public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version";
110+
109111
/**
110112
* Client capabilities.
111113
*/
@@ -402,6 +404,7 @@ public Mono<Void> closeGracefully() {
402404
// --------------------------
403405
// Initialization
404406
// --------------------------
407+
405408
/**
406409
* The initialization phase should be the first interaction between client and server.
407410
* The client will ensure it happens in case it has not been explicitly called and in
@@ -448,6 +451,7 @@ public Mono<Object> ping() {
448451
// --------------------------
449452
// Roots
450453
// --------------------------
454+
451455
/**
452456
* Adds a new root to the client's root list.
453457
* @param root The root to add.
@@ -625,13 +629,13 @@ private McpSchema.CallToolResult validateToolResult(String toolName, McpSchema.C
625629
* @return A Mono that emits the list of all tools result
626630
*/
627631
public Mono<McpSchema.ListToolsResult> listTools() {
628-
return this.listTools(McpSchema.FIRST_PAGE)
629-
.expand(result -> (result.nextCursor() != null) ? this.listTools(result.nextCursor()) : Mono.empty())
630-
.reduce(new McpSchema.ListToolsResult(new ArrayList<>(), null), (allToolsResult, result) -> {
631-
allToolsResult.tools().addAll(result.tools());
632-
return allToolsResult;
633-
})
634-
.map(result -> new McpSchema.ListToolsResult(Collections.unmodifiableList(result.tools()), null));
632+
return this.listTools(McpSchema.FIRST_PAGE).expand(result -> {
633+
String next = result.nextCursor();
634+
return (next != null && !next.isEmpty()) ? this.listTools(next) : Mono.empty();
635+
}).reduce(new McpSchema.ListToolsResult(new ArrayList<>(), null), (allToolsResult, result) -> {
636+
allToolsResult.tools().addAll(result.tools());
637+
return allToolsResult;
638+
}).map(result -> new McpSchema.ListToolsResult(Collections.unmodifiableList(result.tools()), null));
635639
}
636640

637641
/**

mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ class SyncSpec {
167167

168168
private ClientCapabilities capabilities;
169169

170-
private Implementation clientInfo = new Implementation("Java SDK MCP Client", "1.0.0");
170+
private Implementation clientInfo = new Implementation("Java SDK MCP Client", "0.15.0");
171171

172172
private final Map<String, Root> roots = new HashMap<>();
173173

@@ -507,7 +507,7 @@ class AsyncSpec {
507507

508508
private ClientCapabilities capabilities;
509509

510-
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
510+
private Implementation clientInfo = new Implementation("Java SDK MCP Client", "0.15.0");
511511

512512
private final Map<String, Root> roots = new HashMap<>();
513513

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

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import java.net.http.HttpResponse;
1212
import java.net.http.HttpResponse.BodyHandler;
1313
import java.time.Duration;
14+
import java.util.Collections;
15+
import java.util.Comparator;
1416
import java.util.List;
1517
import java.util.Optional;
1618
import java.util.concurrent.CompletionException;
@@ -21,17 +23,13 @@
2123
import java.util.function.Consumer;
2224
import java.util.function.Function;
2325

24-
import org.reactivestreams.Publisher;
25-
import org.slf4j.Logger;
26-
import org.slf4j.LoggerFactory;
27-
28-
import io.modelcontextprotocol.json.TypeRef;
29-
import io.modelcontextprotocol.json.McpJsonMapper;
30-
26+
import io.modelcontextprotocol.client.McpAsyncClient;
27+
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
3128
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
3229
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
33-
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
3430
import io.modelcontextprotocol.common.McpTransportContext;
31+
import io.modelcontextprotocol.json.McpJsonMapper;
32+
import io.modelcontextprotocol.json.TypeRef;
3533
import io.modelcontextprotocol.spec.ClosedMcpTransportSession;
3634
import io.modelcontextprotocol.spec.DefaultMcpTransportSession;
3735
import io.modelcontextprotocol.spec.DefaultMcpTransportStream;
@@ -45,6 +43,9 @@
4543
import io.modelcontextprotocol.spec.ProtocolVersions;
4644
import io.modelcontextprotocol.util.Assert;
4745
import io.modelcontextprotocol.util.Utils;
46+
import org.reactivestreams.Publisher;
47+
import org.slf4j.Logger;
48+
import org.slf4j.LoggerFactory;
4849
import reactor.core.Disposable;
4950
import reactor.core.publisher.Flux;
5051
import reactor.core.publisher.FluxSink;
@@ -81,8 +82,6 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
8182

8283
private static final Logger logger = LoggerFactory.getLogger(HttpClientStreamableHttpTransport.class);
8384

84-
private static final String MCP_PROTOCOL_VERSION = ProtocolVersions.MCP_2025_06_18;
85-
8685
private static final String DEFAULT_ENDPOINT = "/mcp";
8786

8887
/**
@@ -128,6 +127,10 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
128127

129128
private final AtomicReference<Consumer<Throwable>> exceptionHandler = new AtomicReference<>();
130129

130+
private final List<String> supportedProtocolVersions;
131+
132+
private final String latestSupportedProtocolVersion;
133+
131134
/**
132135
* Consumer to handle HttpClient closure. If null, no cleanup is performed (external
133136
* HttpClient).
@@ -137,7 +140,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
137140
private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient httpClient,
138141
HttpRequest.Builder requestBuilder, String baseUri, String endpoint, boolean resumableStreams,
139142
boolean openConnectionOnStartup, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer,
140-
Consumer<HttpClient> onCloseClient) {
143+
List<String> supportedProtocolVersions, Consumer<HttpClient> onCloseClient) {
141144
Assert.notNull(onCloseClient, "onCloseClient must not be null");
142145
this.jsonMapper = jsonMapper;
143146
this.httpClient = httpClient;
@@ -149,12 +152,16 @@ private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient h
149152
this.activeSession.set(createTransportSession());
150153
this.httpRequestCustomizer = httpRequestCustomizer;
151154
this.onCloseClient = onCloseClient;
155+
this.supportedProtocolVersions = Collections.unmodifiableList(supportedProtocolVersions);
156+
this.latestSupportedProtocolVersion = this.supportedProtocolVersions.stream()
157+
.sorted(Comparator.reverseOrder())
158+
.findFirst()
159+
.get();
152160
}
153161

154162
@Override
155163
public List<String> protocolVersions() {
156-
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26,
157-
ProtocolVersions.MCP_2025_06_18);
164+
return supportedProtocolVersions;
158165
}
159166

160167
public static Builder builder(String baseUri) {
@@ -198,7 +205,9 @@ private Publisher<Void> createDelete(String sessionId) {
198205
.uri(uri)
199206
.header("Cache-Control", "no-cache")
200207
.header(HttpHeaders.MCP_SESSION_ID, sessionId)
201-
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
208+
.header(HttpHeaders.PROTOCOL_VERSION,
209+
ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
210+
this.latestSupportedProtocolVersion))
202211
.DELETE();
203212
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
204213
return Mono.from(this.httpRequestCustomizer.customize(builder, "DELETE", uri, null, transportContext));
@@ -314,7 +323,9 @@ private Mono<Disposable> reconnect(McpTransportStream<Disposable> stream) {
314323
var builder = requestBuilder.uri(uri)
315324
.header(HttpHeaders.ACCEPT, TEXT_EVENT_STREAM)
316325
.header("Cache-Control", "no-cache")
317-
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
326+
.header(HttpHeaders.PROTOCOL_VERSION,
327+
connectionCtx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
328+
this.latestSupportedProtocolVersion))
318329
.GET();
319330
var transportContext = connectionCtx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
320331
return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null, transportContext));
@@ -489,7 +500,9 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
489500
.header(HttpHeaders.ACCEPT, APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
490501
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
491502
.header(HttpHeaders.CACHE_CONTROL, "no-cache")
492-
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
503+
.header(HttpHeaders.PROTOCOL_VERSION,
504+
ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
505+
this.latestSupportedProtocolVersion))
493506
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
494507
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
495508
return Mono
@@ -684,6 +697,9 @@ public static class Builder {
684697
private Consumer<HttpClient> onCloseClient = (HttpClient client) -> {
685698
};
686699

700+
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
701+
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18);
702+
687703
/**
688704
* Creates a new builder with the specified base URI.
689705
* @param baseUri the base URI of the MCP server
@@ -831,6 +847,30 @@ public Builder onHttpClientClose(Consumer<HttpClient> onCloseClient) {
831847
return this;
832848
}
833849

850+
/**
851+
* Sets the list of supported protocol versions used in version negotiation. By
852+
* default, the client will send the latest of those versions in the
853+
* {@code MCP-Protocol-Version} header.
854+
* <p>
855+
* Setting this value only updates the values used in version negotiation, and
856+
* does NOT impact the actual capabilities of the transport. It should only be
857+
* used for compatibility with servers having strict requirements around the
858+
* {@code MCP-Protocol-Version} header.
859+
* @param supportedProtocolVersions protocol versions supported by this transport
860+
* @return this builder
861+
* @see <a href=
862+
* "https://modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle#version-negotiation">version
863+
* negotiation specification</a>
864+
* @see <a href=
865+
* "https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header">Protocol
866+
* Version Header</a>
867+
*/
868+
public Builder supportedProtocolVersions(List<String> supportedProtocolVersions) {
869+
Assert.notEmpty(supportedProtocolVersions, "supportedProtocolVersions must not be empty");
870+
this.supportedProtocolVersions = Collections.unmodifiableList(supportedProtocolVersions);
871+
return this;
872+
}
873+
834874
/**
835875
* Construct a fresh instance of {@link HttpClientStreamableHttpTransport} using
836876
* the current builder configuration.
@@ -869,7 +909,7 @@ public HttpClientStreamableHttpTransport build() {
869909

870910
return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
871911
httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup,
872-
httpRequestCustomizer, closeHandler);
912+
httpRequestCustomizer, supportedProtocolVersions, closeHandler);
873913
}
874914

875915
}

mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
2525
import io.modelcontextprotocol.spec.McpSchema.CompleteResult.CompleteCompletion;
2626
import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
27-
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse;
2827
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
2928
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
3029
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
@@ -398,19 +397,23 @@ public Mono<CallToolResult> apply(McpAsyncServerExchange exchange, McpSchema.Cal
398397
// results that conform to this schema.
399398
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema
400399
if (result.structuredContent() == null) {
401-
logger.warn(
402-
"Response missing structured content which is expected when calling tool with non-empty outputSchema");
403-
return new CallToolResult(
404-
"Response missing structured content which is expected when calling tool with non-empty outputSchema",
405-
true);
400+
String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
401+
logger.warn(content);
402+
return CallToolResult.builder()
403+
.content(List.of(new McpSchema.TextContent(content)))
404+
.isError(true)
405+
.build();
406406
}
407407

408408
// Validate the result against the output schema
409409
var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent());
410410

411411
if (!validation.valid()) {
412412
logger.warn("Tool call result validation failed: {}", validation.errorMessage());
413-
return new CallToolResult(validation.errorMessage(), true);
413+
return CallToolResult.builder()
414+
.content(List.of(new McpSchema.TextContent(validation.errorMessage())))
415+
.isError(true)
416+
.build();
414417
}
415418

416419
if (Utils.isEmpty(result.content())) {

0 commit comments

Comments
 (0)