Skip to content

Commit 86991c1

Browse files
leehautKehrlann
authored andcommitted
fix the baseUrl is configured with a trailing slash
Signed-off-by: lance <leehaut@gmail.com> Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent 67f8eab commit 86991c1

File tree

4 files changed

+221
-34
lines changed

4 files changed

+221
-34
lines changed

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
import java.util.concurrent.ConcurrentHashMap;
1515
import java.util.concurrent.atomic.AtomicBoolean;
1616

17+
import io.modelcontextprotocol.common.McpTransportContext;
1718
import io.modelcontextprotocol.json.McpJsonMapper;
1819
import io.modelcontextprotocol.json.TypeRef;
19-
import io.modelcontextprotocol.common.McpTransportContext;
2020
import io.modelcontextprotocol.server.McpTransportContextExtractor;
2121
import io.modelcontextprotocol.spec.McpError;
2222
import io.modelcontextprotocol.spec.McpSchema;
@@ -69,7 +69,9 @@
6969
@WebServlet(asyncSupported = true)
7070
public class HttpServletSseServerTransportProvider extends HttpServlet implements McpServerTransportProvider {
7171

72-
/** Logger for this class */
72+
/**
73+
* Logger for this class
74+
*/
7375
private static final Logger logger = LoggerFactory.getLogger(HttpServletSseServerTransportProvider.class);
7476

7577
public static final String UTF_8 = "UTF-8";
@@ -78,38 +80,60 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
7880

7981
public static final String FAILED_TO_SEND_ERROR_RESPONSE = "Failed to send error response: {}";
8082

81-
/** Default endpoint path for SSE connections */
83+
/**
84+
* Default endpoint path for SSE connections
85+
*/
8286
public static final String DEFAULT_SSE_ENDPOINT = "/sse";
8387

84-
/** Event type for regular messages */
88+
/**
89+
* Event type for regular messages
90+
*/
8591
public static final String MESSAGE_EVENT_TYPE = "message";
8692

87-
/** Event type for endpoint information */
93+
/**
94+
* Event type for endpoint information
95+
*/
8896
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
8997

98+
public static final String SESSION_ID = "sessionId";
99+
90100
public static final String DEFAULT_BASE_URL = "";
91101

92-
/** JSON mapper for serialization/deserialization */
102+
/**
103+
* JSON mapper for serialization/deserialization
104+
*/
93105
private final McpJsonMapper jsonMapper;
94106

95-
/** Base URL for the server transport */
107+
/**
108+
* Base URL for the server transport
109+
*/
96110
private final String baseUrl;
97111

98-
/** The endpoint path for handling client messages */
112+
/**
113+
* The endpoint path for handling client messages
114+
*/
99115
private final String messageEndpoint;
100116

101-
/** The endpoint path for handling SSE connections */
117+
/**
118+
* The endpoint path for handling SSE connections
119+
*/
102120
private final String sseEndpoint;
103121

104-
/** Map of active client sessions, keyed by session ID */
122+
/**
123+
* Map of active client sessions, keyed by session ID
124+
*/
105125
private final Map<String, McpServerSession> sessions = new ConcurrentHashMap<>();
106126

107127
private McpTransportContextExtractor<HttpServletRequest> contextExtractor;
108128

109-
/** Flag indicating if the transport is in the process of shutting down */
129+
/**
130+
* Flag indicating if the transport is in the process of shutting down
131+
*/
110132
private final AtomicBoolean isClosing = new AtomicBoolean(false);
111133

112-
/** Session factory for creating new sessions */
134+
/**
135+
* Session factory for creating new sessions
136+
*/
113137
private McpServerSession.Factory sessionFactory;
114138

115139
/**
@@ -243,7 +267,22 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
243267
this.sessions.put(sessionId, session);
244268

245269
// Send initial endpoint event
246-
this.sendEvent(writer, ENDPOINT_EVENT_TYPE, this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
270+
this.sendEvent(writer, ENDPOINT_EVENT_TYPE, buildEndpointUrl(sessionId));
271+
}
272+
273+
/**
274+
* Constructs the full message endpoint URL by combining the base URL, message path,
275+
* and the required session_id query parameter.
276+
* @param sessionId the unique session identifier
277+
* @return the fully qualified endpoint URL as a string
278+
*/
279+
private String buildEndpointUrl(String sessionId) {
280+
// for WebMVC compatibility
281+
if (this.baseUrl.endsWith("/")) {
282+
return this.baseUrl.substring(0, this.baseUrl.length() - 1) + this.messageEndpoint + "?sessionId="
283+
+ sessionId;
284+
}
285+
return this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId;
247286
}
248287

249288
/**
@@ -434,8 +473,8 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
434473
* Converts data from one type to another using the configured JsonMapper.
435474
* @param data The source data object to convert
436475
* @param typeRef The target type reference
437-
* @return The converted object of type T
438476
* @param <T> The target type
477+
* @return The converted object of type T
439478
*/
440479
@Override
441480
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {

mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
import java.util.List;
1010
import java.util.concurrent.ConcurrentHashMap;
1111

12+
import io.modelcontextprotocol.common.McpTransportContext;
1213
import io.modelcontextprotocol.json.McpJsonMapper;
1314
import io.modelcontextprotocol.json.TypeRef;
14-
15-
import io.modelcontextprotocol.common.McpTransportContext;
1615
import io.modelcontextprotocol.server.McpTransportContextExtractor;
1716
import io.modelcontextprotocol.spec.McpError;
1817
import io.modelcontextprotocol.spec.McpSchema;
@@ -22,7 +21,6 @@
2221
import io.modelcontextprotocol.spec.ProtocolVersions;
2322
import io.modelcontextprotocol.util.Assert;
2423
import io.modelcontextprotocol.util.KeepAliveScheduler;
25-
2624
import org.slf4j.Logger;
2725
import org.slf4j.LoggerFactory;
2826
import reactor.core.Exceptions;
@@ -37,6 +35,7 @@
3735
import org.springframework.web.reactive.function.server.RouterFunctions;
3836
import org.springframework.web.reactive.function.server.ServerRequest;
3937
import org.springframework.web.reactive.function.server.ServerResponse;
38+
import org.springframework.web.util.UriComponentsBuilder;
4039

4140
/**
4241
* Server-side implementation of the MCP (Model Context Protocol) HTTP transport using
@@ -95,6 +94,8 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
9594
*/
9695
public static final String DEFAULT_SSE_ENDPOINT = "/sse";
9796

97+
public static final String SESSION_ID = "sessionId";
98+
9899
public static final String DEFAULT_BASE_URL = "";
99100

100101
private final McpJsonMapper jsonMapper;
@@ -224,6 +225,7 @@ public Mono<Void> notifyClients(String method, Object params) {
224225
// FIXME: This javadoc makes claims about using isClosing flag but it's not
225226
// actually
226227
// doing that.
228+
227229
/**
228230
* Initiates a graceful shutdown of all the sessions. This method ensures all active
229231
* sessions are properly closed and cleaned up.
@@ -286,17 +288,30 @@ private Mono<ServerResponse> handleSseConnection(ServerRequest request) {
286288

287289
// Send initial endpoint event
288290
logger.debug("Sending initial endpoint event to session: {}", sessionId);
289-
sink.next(ServerSentEvent.builder()
290-
.event(ENDPOINT_EVENT_TYPE)
291-
.data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId)
292-
.build());
291+
sink.next(
292+
ServerSentEvent.builder().event(ENDPOINT_EVENT_TYPE).data(buildEndpointUrl(sessionId)).build());
293293
sink.onCancel(() -> {
294294
logger.debug("Session {} cancelled", sessionId);
295295
sessions.remove(sessionId);
296296
});
297297
}).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)), ServerSentEvent.class);
298298
}
299299

300+
/**
301+
* Constructs the full message endpoint URL by combining the base URL, message path,
302+
* and the required session_id query parameter.
303+
* @param sessionId the unique session identifier
304+
* @return the fully qualified endpoint URL as a string
305+
*/
306+
private String buildEndpointUrl(String sessionId) {
307+
// for WebMVC compatibility
308+
return UriComponentsBuilder.fromUriString(this.baseUrl)
309+
.path(this.messageEndpoint)
310+
.queryParam(SESSION_ID, sessionId)
311+
.build()
312+
.toUriString();
313+
}
314+
300315
/**
301316
* Handles incoming JSON-RPC messages from clients. Deserializes the message and
302317
* processes it through the configured message handler.

mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,18 @@
1111
import java.util.concurrent.ConcurrentHashMap;
1212
import java.util.concurrent.locks.ReentrantLock;
1313

14+
import io.modelcontextprotocol.common.McpTransportContext;
1415
import io.modelcontextprotocol.json.McpJsonMapper;
1516
import io.modelcontextprotocol.json.TypeRef;
16-
17-
import io.modelcontextprotocol.common.McpTransportContext;
1817
import io.modelcontextprotocol.server.McpTransportContextExtractor;
1918
import io.modelcontextprotocol.spec.McpError;
2019
import io.modelcontextprotocol.spec.McpSchema;
20+
import io.modelcontextprotocol.spec.McpServerSession;
2121
import io.modelcontextprotocol.spec.McpServerTransport;
2222
import io.modelcontextprotocol.spec.McpServerTransportProvider;
2323
import io.modelcontextprotocol.spec.ProtocolVersions;
24-
import io.modelcontextprotocol.spec.McpServerSession;
2524
import io.modelcontextprotocol.util.Assert;
2625
import io.modelcontextprotocol.util.KeepAliveScheduler;
27-
2826
import org.slf4j.Logger;
2927
import org.slf4j.LoggerFactory;
3028
import reactor.core.publisher.Flux;
@@ -36,6 +34,7 @@
3634
import org.springframework.web.servlet.function.ServerRequest;
3735
import org.springframework.web.servlet.function.ServerResponse;
3836
import org.springframework.web.servlet.function.ServerResponse.SseBuilder;
37+
import org.springframework.web.util.UriComponentsBuilder;
3938

4039
/**
4140
* Server-side implementation of the Model Context Protocol (MCP) transport layer using
@@ -87,6 +86,8 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
8786
*/
8887
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
8988

89+
public static final String SESSION_ID = "sessionId";
90+
9091
/**
9192
* Default SSE endpoint path as specified by the MCP transport specification.
9293
*/
@@ -275,9 +276,7 @@ private ServerResponse handleSseConnection(ServerRequest request) {
275276
this.sessions.put(sessionId, session);
276277

277278
try {
278-
sseBuilder.id(sessionId)
279-
.event(ENDPOINT_EVENT_TYPE)
280-
.data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
279+
sseBuilder.id(sessionId).event(ENDPOINT_EVENT_TYPE).data(buildEndpointUrl(sessionId));
281280
}
282281
catch (Exception e) {
283282
logger.error("Failed to send initial endpoint event: {}", e.getMessage());
@@ -292,6 +291,21 @@ private ServerResponse handleSseConnection(ServerRequest request) {
292291
}
293292
}
294293

294+
/**
295+
* Constructs the full message endpoint URL by combining the base URL, message path,
296+
* and the required session_id query parameter.
297+
* @param sessionId the unique session identifier
298+
* @return the fully qualified endpoint URL as a string
299+
*/
300+
private String buildEndpointUrl(String sessionId) {
301+
// for WebMVC compatibility
302+
return UriComponentsBuilder.fromUriString(this.baseUrl)
303+
.path(this.messageEndpoint)
304+
.queryParam(SESSION_ID, sessionId)
305+
.build()
306+
.toUriString();
307+
}
308+
295309
/**
296310
* Handles incoming JSON-RPC messages from clients. This method:
297311
* <ul>
@@ -308,11 +322,11 @@ private ServerResponse handleMessage(ServerRequest request) {
308322
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
309323
}
310324

311-
if (request.param("sessionId").isEmpty()) {
325+
if (request.param(SESSION_ID).isEmpty()) {
312326
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
313327
}
314328

315-
String sessionId = request.param("sessionId").get();
329+
String sessionId = request.param(SESSION_ID).get();
316330
McpServerSession session = sessions.get(sessionId);
317331

318332
if (session == null) {
@@ -327,9 +341,9 @@ private ServerResponse handleMessage(ServerRequest request) {
327341

328342
// Process the message through the session's handle method
329343
session.handle(message).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)).block(); // Block
330-
// for
331-
// WebMVC
332-
// compatibility
344+
// for
345+
// WebMVC
346+
// compatibility
333347

334348
return ServerResponse.ok().build();
335349
}
@@ -398,8 +412,8 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
398412
* Converts data from one type to another using the configured McpJsonMapper.
399413
* @param data The source data object to convert
400414
* @param typeRef The target type reference
401-
* @return The converted object of type T
402415
* @param <T> The target type
416+
* @return The converted object of type T
403417
*/
404418
@Override
405419
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {

0 commit comments

Comments
 (0)