Skip to content

Commit 509fa92

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix-url-resolving
2 parents 5d009e6 + 713ee1a commit 509fa92

File tree

148 files changed

+16941
-5653
lines changed

Some content is hidden

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

148 files changed

+16941
-5653
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.11.0-SNAPSHOT</version>
10+
<version>0.12.0-SNAPSHOT</version>
1111
</parent>
1212

1313
<artifactId>mcp-bom</artifactId>

mcp-spring/mcp-spring-webflux/pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
<parent>
77
<groupId>io.modelcontextprotocol.sdk</groupId>
88
<artifactId>mcp-parent</artifactId>
9-
<version>0.11.0-SNAPSHOT</version>
9+
<version>0.12.0-SNAPSHOT</version>
1010
<relativePath>../../pom.xml</relativePath>
1111
</parent>
1212
<artifactId>mcp-spring-webflux</artifactId>
1313
<packaging>jar</packaging>
14-
<name>WebFlux implementation of the Java MCP SSE transport</name>
15-
<description></description>
14+
<name>WebFlux transports</name>
15+
<description>WebFlux implementation for the SSE and Streamable Http Client and Server transports</description>
1616
<url>https://github.com/modelcontextprotocol/java-sdk</url>
1717

1818
<scm>
@@ -25,13 +25,13 @@
2525
<dependency>
2626
<groupId>io.modelcontextprotocol.sdk</groupId>
2727
<artifactId>mcp</artifactId>
28-
<version>0.11.0-SNAPSHOT</version>
28+
<version>0.12.0-SNAPSHOT</version>
2929
</dependency>
3030

3131
<dependency>
3232
<groupId>io.modelcontextprotocol.sdk</groupId>
3333
<artifactId>mcp-test</artifactId>
34-
<version>0.11.0-SNAPSHOT</version>
34+
<version>0.12.0-SNAPSHOT</version>
3535
<scope>test</scope>
3636
</dependency>
3737

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

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
15
package io.modelcontextprotocol.client.transport;
26

37
import java.io.IOException;
@@ -23,13 +27,17 @@
2327

2428
import io.modelcontextprotocol.spec.DefaultMcpTransportSession;
2529
import io.modelcontextprotocol.spec.DefaultMcpTransportStream;
30+
import io.modelcontextprotocol.spec.HttpHeaders;
2631
import io.modelcontextprotocol.spec.McpClientTransport;
2732
import io.modelcontextprotocol.spec.McpError;
2833
import io.modelcontextprotocol.spec.McpSchema;
34+
import io.modelcontextprotocol.spec.McpTransportException;
2935
import io.modelcontextprotocol.spec.McpTransportSession;
3036
import io.modelcontextprotocol.spec.McpTransportSessionNotFoundException;
3137
import io.modelcontextprotocol.spec.McpTransportStream;
38+
import io.modelcontextprotocol.spec.ProtocolVersions;
3239
import io.modelcontextprotocol.util.Assert;
40+
import io.modelcontextprotocol.util.Utils;
3341
import reactor.core.Disposable;
3442
import reactor.core.publisher.Flux;
3543
import reactor.core.publisher.Mono;
@@ -63,8 +71,12 @@
6371
*/
6472
public class WebClientStreamableHttpTransport implements McpClientTransport {
6573

74+
private static final String MISSING_SESSION_ID = "[missing_session_id]";
75+
6676
private static final Logger logger = LoggerFactory.getLogger(WebClientStreamableHttpTransport.class);
6777

78+
private static final String MCP_PROTOCOL_VERSION = ProtocolVersions.MCP_2025_03_26;
79+
6880
private static final String DEFAULT_ENDPOINT = "/mcp";
6981

7082
/**
@@ -102,6 +114,11 @@ private WebClientStreamableHttpTransport(ObjectMapper objectMapper, WebClient.Bu
102114
this.activeSession.set(createTransportSession());
103115
}
104116

117+
@Override
118+
public List<String> protocolVersions() {
119+
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26);
120+
}
121+
105122
/**
106123
* Create a stateful builder for creating {@link WebClientStreamableHttpTransport}
107124
* instances.
@@ -127,12 +144,17 @@ public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchem
127144

128145
private DefaultMcpTransportSession createTransportSession() {
129146
Function<String, Publisher<Void>> onClose = sessionId -> sessionId == null ? Mono.empty()
130-
: webClient.delete().uri(this.endpoint).headers(httpHeaders -> {
131-
httpHeaders.add("mcp-session-id", sessionId);
132-
}).retrieve().toBodilessEntity().onErrorComplete(e -> {
133-
logger.warn("Got error when closing transport", e);
134-
return true;
135-
}).then();
147+
: webClient.delete()
148+
.uri(this.endpoint)
149+
.header(HttpHeaders.MCP_SESSION_ID, sessionId)
150+
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
151+
.retrieve()
152+
.toBodilessEntity()
153+
.onErrorComplete(e -> {
154+
logger.warn("Got error when closing transport", e);
155+
return true;
156+
})
157+
.then();
136158
return new DefaultMcpTransportSession(onClose);
137159
}
138160

@@ -185,10 +207,11 @@ private Mono<Disposable> reconnect(McpTransportStream<Disposable> stream) {
185207
Disposable connection = webClient.get()
186208
.uri(this.endpoint)
187209
.accept(MediaType.TEXT_EVENT_STREAM)
210+
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
188211
.headers(httpHeaders -> {
189-
transportSession.sessionId().ifPresent(id -> httpHeaders.add("mcp-session-id", id));
212+
transportSession.sessionId().ifPresent(id -> httpHeaders.add(HttpHeaders.MCP_SESSION_ID, id));
190213
if (stream != null) {
191-
stream.lastId().ifPresent(id -> httpHeaders.add("last-event-id", id));
214+
stream.lastId().ifPresent(id -> httpHeaders.add(HttpHeaders.LAST_EVENT_ID, id));
192215
}
193216
})
194217
.exchangeToFlux(response -> {
@@ -201,8 +224,13 @@ else if (isNotAllowed(response)) {
201224
return Flux.empty();
202225
}
203226
else if (isNotFound(response)) {
204-
String sessionIdRepresentation = sessionIdOrPlaceholder(transportSession);
205-
return mcpSessionNotFoundError(sessionIdRepresentation);
227+
if (transportSession.sessionId().isPresent()) {
228+
String sessionIdRepresentation = sessionIdOrPlaceholder(transportSession);
229+
return mcpSessionNotFoundError(sessionIdRepresentation);
230+
}
231+
else {
232+
return this.extractError(response, MISSING_SESSION_ID);
233+
}
206234
}
207235
else {
208236
return response.<McpSchema.JSONRPCMessage>createError().doOnError(e -> {
@@ -244,14 +272,15 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
244272

245273
Disposable connection = webClient.post()
246274
.uri(this.endpoint)
247-
.accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON)
275+
.accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)
276+
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
248277
.headers(httpHeaders -> {
249-
transportSession.sessionId().ifPresent(id -> httpHeaders.add("mcp-session-id", id));
278+
transportSession.sessionId().ifPresent(id -> httpHeaders.add(HttpHeaders.MCP_SESSION_ID, id));
250279
})
251280
.bodyValue(message)
252281
.exchangeToFlux(response -> {
253282
if (transportSession
254-
.markInitialized(response.headers().asHttpHeaders().getFirst("mcp-session-id"))) {
283+
.markInitialized(response.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID))) {
255284
// Once we have a session, we try to open an async stream for
256285
// the server to send notifications and requests out-of-band.
257286
reconnect(null).contextWrite(sink.contextView()).subscribe();
@@ -287,7 +316,7 @@ else if (mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
287316
logger.trace("Received response to POST for session {}", sessionRepresentation);
288317
// communicate to caller the message was delivered
289318
sink.success();
290-
return responseFlux(response);
319+
return directResponseFlux(message, response);
291320
}
292321
else {
293322
logger.warn("Unknown media type {} returned for POST in session {}", contentType,
@@ -297,10 +326,10 @@ else if (mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
297326
}
298327
}
299328
else {
300-
if (isNotFound(response)) {
329+
if (isNotFound(response) && !sessionRepresentation.equals(MISSING_SESSION_ID)) {
301330
return mcpSessionNotFoundError(sessionRepresentation);
302331
}
303-
return extractError(response, sessionRepresentation);
332+
return this.extractError(response, sessionRepresentation);
304333
}
305334
})
306335
.flatMap(jsonRpcMessage -> this.handler.get().apply(Mono.just(jsonRpcMessage)))
@@ -340,10 +369,11 @@ private Flux<McpSchema.JSONRPCMessage> extractError(ClientResponse response, Str
340369
McpSchema.JSONRPCResponse jsonRpcResponse = objectMapper.readValue(body,
341370
McpSchema.JSONRPCResponse.class);
342371
jsonRpcError = jsonRpcResponse.error();
343-
toPropagate = new McpError(jsonRpcError);
372+
toPropagate = jsonRpcError != null ? new McpError(jsonRpcError)
373+
: new McpTransportException("Can't parse the jsonResponse " + jsonRpcResponse);
344374
}
345375
catch (IOException ex) {
346-
toPropagate = new RuntimeException("Sending request failed", e);
376+
toPropagate = new McpTransportException("Sending request failed, " + e.getMessage(), e);
347377
logger.debug("Received content together with {} HTTP code response: {}", response.statusCode(), body);
348378
}
349379

@@ -352,7 +382,11 @@ private Flux<McpSchema.JSONRPCMessage> extractError(ClientResponse response, Str
352382
// invalidate the session
353383
// https://github.com/modelcontextprotocol/typescript-sdk/issues/389
354384
if (responseException.getStatusCode().isSameCodeAs(HttpStatus.BAD_REQUEST)) {
355-
return Mono.error(new McpTransportSessionNotFoundException(sessionRepresentation, toPropagate));
385+
if (!sessionRepresentation.equals(MISSING_SESSION_ID)) {
386+
return Mono.error(new McpTransportSessionNotFoundException(sessionRepresentation, toPropagate));
387+
}
388+
return Mono.error(new McpTransportException("Received 400 BAD REQUEST for session "
389+
+ sessionRepresentation + ". " + toPropagate.getMessage(), toPropagate));
356390
}
357391
return Mono.error(toPropagate);
358392
}).flux();
@@ -381,18 +415,25 @@ private static boolean isEventStream(ClientResponse response) {
381415
}
382416

383417
private static String sessionIdOrPlaceholder(McpTransportSession<?> transportSession) {
384-
return transportSession.sessionId().orElse("[missing_session_id]");
418+
return transportSession.sessionId().orElse(MISSING_SESSION_ID);
385419
}
386420

387-
private Flux<McpSchema.JSONRPCMessage> responseFlux(ClientResponse response) {
421+
private Flux<McpSchema.JSONRPCMessage> directResponseFlux(McpSchema.JSONRPCMessage sentMessage,
422+
ClientResponse response) {
388423
return response.bodyToMono(String.class).<Iterable<McpSchema.JSONRPCMessage>>handle((responseMessage, s) -> {
389424
try {
390-
McpSchema.JSONRPCMessage jsonRpcResponse = McpSchema.deserializeJsonRpcMessage(objectMapper,
391-
responseMessage);
392-
s.next(List.of(jsonRpcResponse));
425+
if (sentMessage instanceof McpSchema.JSONRPCNotification && Utils.hasText(responseMessage)) {
426+
logger.warn("Notification: {} received non-compliant response: {}", sentMessage, responseMessage);
427+
s.complete();
428+
}
429+
else {
430+
McpSchema.JSONRPCMessage jsonRpcResponse = McpSchema.deserializeJsonRpcMessage(objectMapper,
431+
responseMessage);
432+
s.next(List.of(jsonRpcResponse));
433+
}
393434
}
394435
catch (IOException e) {
395-
s.error(e);
436+
s.error(new McpTransportException(e));
396437
}
397438
}).flatMapIterable(Function.identity());
398439
}
@@ -419,11 +460,12 @@ private Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>> parse(Serve
419460
return Tuples.of(Optional.ofNullable(event.id()), List.of(message));
420461
}
421462
catch (IOException ioException) {
422-
throw new McpError("Error parsing JSON-RPC message: " + event.data());
463+
throw new McpTransportException("Error parsing JSON-RPC message: " + event.data(), ioException);
423464
}
424465
}
425466
else {
426-
throw new McpError("Received unrecognized SSE event type: " + event.event());
467+
logger.debug("Received SSE event with type: {}", event);
468+
return Tuples.of(Optional.empty(), List.of());
427469
}
428470
}
429471

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
/*
22
* Copyright 2024 - 2024 the original author or authors.
33
*/
4+
45
package io.modelcontextprotocol.client.transport;
56

67
import java.io.IOException;
8+
import java.util.List;
79
import java.util.function.BiConsumer;
810
import java.util.function.Function;
911

1012
import com.fasterxml.jackson.core.type.TypeReference;
1113
import com.fasterxml.jackson.databind.ObjectMapper;
14+
15+
import io.modelcontextprotocol.spec.HttpHeaders;
1216
import io.modelcontextprotocol.spec.McpClientTransport;
13-
import io.modelcontextprotocol.spec.McpError;
1417
import io.modelcontextprotocol.spec.McpSchema;
1518
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
19+
import io.modelcontextprotocol.spec.ProtocolVersions;
1620
import io.modelcontextprotocol.util.Assert;
1721
import org.slf4j.Logger;
1822
import org.slf4j.LoggerFactory;
@@ -62,6 +66,8 @@ public class WebFluxSseClientTransport implements McpClientTransport {
6266

6367
private static final Logger logger = LoggerFactory.getLogger(WebFluxSseClientTransport.class);
6468

69+
private static final String MCP_PROTOCOL_VERSION = ProtocolVersions.MCP_2024_11_05;
70+
6571
/**
6672
* Event type for JSON-RPC messages received through the SSE connection. The server
6773
* sends messages with this event type to transmit JSON-RPC protocol data.
@@ -166,6 +172,11 @@ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMappe
166172
this.sseEndpoint = sseEndpoint;
167173
}
168174

175+
@Override
176+
public List<String> protocolVersions() {
177+
return List.of(MCP_PROTOCOL_VERSION);
178+
}
179+
169180
/**
170181
* Establishes a connection to the MCP server using Server-Sent Events (SSE). This
171182
* method initiates the SSE connection and sets up the message processing pipeline.
@@ -185,8 +196,6 @@ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMappe
185196
* @param handler a function that processes incoming JSON-RPC messages and returns
186197
* responses
187198
* @return a Mono that completes when the connection is fully established
188-
* @throws McpError if there's an error processing SSE events or if an unrecognized
189-
* event type is received
190199
*/
191200
@Override
192201
public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> handler) {
@@ -203,7 +212,7 @@ public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> h
203212
else {
204213
// TODO: clarify with the spec if multiple events can be
205214
// received
206-
s.error(new McpError("Failed to handle SSE endpoint event"));
215+
s.error(new RuntimeException("Failed to handle SSE endpoint event"));
207216
}
208217
}
209218
else if (MESSAGE_EVENT_TYPE.equals(event.event())) {
@@ -216,7 +225,8 @@ else if (MESSAGE_EVENT_TYPE.equals(event.event())) {
216225
}
217226
}
218227
else {
219-
s.error(new McpError("Received unrecognized SSE event type: " + event.event()));
228+
logger.debug("Received unrecognized SSE event type: {}", event);
229+
s.complete();
220230
}
221231
}).transform(handler)).subscribe();
222232

@@ -249,6 +259,7 @@ public Mono<Void> sendMessage(JSONRPCMessage message) {
249259
return webClient.post()
250260
.uri(messageEndpointUri)
251261
.contentType(MediaType.APPLICATION_JSON)
262+
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
252263
.bodyValue(jsonText)
253264
.retrieve()
254265
.toBodilessEntity()
@@ -281,6 +292,7 @@ protected Flux<ServerSentEvent<String>> eventStream() {// @formatter:off
281292
.get()
282293
.uri(this.sseEndpoint)
283294
.accept(MediaType.TEXT_EVENT_STREAM)
295+
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
284296
.retrieve()
285297
.bodyToFlux(SSE_TYPE)
286298
.retryWhen(Retry.from(retrySignal -> retrySignal.handle(inboundRetryHandler)));

0 commit comments

Comments
 (0)