From b800b6f79dd538a8d9bead233a9c8bc4f632d009 Mon Sep 17 00:00:00 2001 From: minguncle <57527858+minguncle@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:53:30 +0800 Subject: [PATCH] feat: Support separate registration and published paths in WebMvcSseServerTransportProvider Adds a new constructor to WebMvcSseServerTransportProvider that accepts separate parameters for server registration paths and client-facing published paths. This enhancement allows the provider to work correctly in environments with servlet context paths or global URL prefixes (like Spring Boot's spring.mvc.servlet.path) by: - Using messageEndpoint for server-side router registration - Using publishedMessageEndpoint when communicating with clients The implementation maintains backward compatibility --- .../WebMvcSseServerTransportProvider.java | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java index 65416b25..65545695 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java @@ -91,6 +91,8 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi private final String sseEndpoint; + private final String publishedMessageEndpoint; + private final RouterFunction routerFunction; private McpServerSession.Factory sessionFactory; @@ -105,6 +107,36 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi */ private volatile boolean isClosing = false; + /** + * Constructs a new WebMvcSseServerTransportProvider instance. + * + * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization + * of messages. + * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC + * messages via HTTP POST. This endpoint will be communicated to clients through the + * SSE connection's initial endpoint event. + * @param publishedMessageEndpoint The published endpoint URI that will be sent to clients + * (might differ from the actual messageEndpoint in case of proxies, load balancers, etc.) + * @param sseEndpoint The endpoint URI where clients establish their SSE connections. + * @throws IllegalArgumentException if any parameter is null + */ + public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint, + String publishedMessageEndpoint, String sseEndpoint) { + Assert.notNull(objectMapper, "ObjectMapper must not be null"); + Assert.notNull(messageEndpoint, "Message endpoint must not be null"); + Assert.notNull(publishedMessageEndpoint, "Published message endpoint must not be null"); + Assert.notNull(sseEndpoint, "SSE endpoint must not be null"); + + this.objectMapper = objectMapper; + this.messageEndpoint = messageEndpoint; + this.publishedMessageEndpoint = publishedMessageEndpoint; + this.sseEndpoint = sseEndpoint; + this.routerFunction = RouterFunctions.route() + .GET(this.sseEndpoint, this::handleSseConnection) + .POST(this.messageEndpoint, this::handleMessage) + .build(); + } + /** * Constructs a new WebMvcSseServerTransportProvider instance. * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization @@ -122,6 +154,7 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messag this.objectMapper = objectMapper; this.messageEndpoint = messageEndpoint; + this.publishedMessageEndpoint = messageEndpoint; this.sseEndpoint = sseEndpoint; this.routerFunction = RouterFunctions.route() .GET(this.sseEndpoint, this::handleSseConnection) @@ -248,7 +281,7 @@ private ServerResponse handleSseConnection(ServerRequest request) { try { sseBuilder.id(sessionId) .event(ENDPOINT_EVENT_TYPE) - .data(messageEndpoint + "?sessionId=" + sessionId); + .data(publishedMessageEndpoint + "?sessionId=" + sessionId); } catch (Exception e) { logger.error("Failed to send initial endpoint event: {}", e.getMessage());