From 2934635d99cd83bcafe26051e1a231bb6681cfbd Mon Sep 17 00:00:00 2001 From: or-givati Date: Mon, 24 Mar 2025 14:41:07 +0200 Subject: [PATCH] feat(mcp): customize transport endpoints and improve URI handling (#69) - Add support for customizable SSE endpoints in HttpClientSseClientTransport - Replace pathInfo with requestURI in HttpServletSseServerTransportProvider for more reliable endpoint matching - Implement builder pattern to support the customization options Related to #40 Signed-off-by: Christian Tzolov Co-authored-by: Christian Tzolov --- .../HttpClientSseClientTransport.java | 96 ++++++++++++++++++- ...HttpServletSseServerTransportProvider.java | 86 ++++++++++++++++- .../server/ServletSseMcpAsyncServerTests.java | 3 +- .../server/ServletSseMcpSyncServerTests.java | 3 +- ...rverTransportProviderIntegrationTests.java | 14 ++- 5 files changed, 189 insertions(+), 13 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index ca1b0e8..696efdf 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -65,11 +65,14 @@ public class HttpClientSseClientTransport implements McpClientTransport { private static final String ENDPOINT_EVENT_TYPE = "endpoint"; /** Default SSE endpoint path */ - private static final String SSE_ENDPOINT = "/sse"; + private static final String DEFAULT_SSE_ENDPOINT = "/sse"; /** Base URI for the MCP server */ private final String baseUri; + /** SSE endpoint path */ + private final String sseEndpoint; + /** SSE client for handling server-sent events. Uses the /sse endpoint */ private final FlowSseClient sseClient; @@ -110,15 +113,104 @@ public class HttpClientSseClientTransport implements McpClientTransport { * @throws IllegalArgumentException if objectMapper or clientBuilder is null */ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, ObjectMapper objectMapper) { + this(clientBuilder, baseUri, DEFAULT_SSE_ENDPOINT, objectMapper); + } + + /** + * Creates a new transport instance with custom HTTP client builder and object mapper. + * @param clientBuilder the HTTP client builder to use + * @param baseUri the base URI of the MCP server + * @param sseEndpoint the SSE endpoint path + * @param objectMapper the object mapper for JSON serialization/deserialization + * @throws IllegalArgumentException if objectMapper or clientBuilder is null + */ + public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, String sseEndpoint, + ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); Assert.hasText(baseUri, "baseUri must not be empty"); + Assert.hasText(sseEndpoint, "sseEndpoint must not be empty"); Assert.notNull(clientBuilder, "clientBuilder must not be null"); this.baseUri = baseUri; + this.sseEndpoint = sseEndpoint; this.objectMapper = objectMapper; this.httpClient = clientBuilder.connectTimeout(Duration.ofSeconds(10)).build(); this.sseClient = new FlowSseClient(this.httpClient); } + /** + * Creates a new builder for {@link HttpClientSseClientTransport}. + * @param baseUri the base URI of the MCP server + * @return a new builder instance + */ + public static Builder builder(String baseUri) { + return new Builder(baseUri); + } + + /** + * Builder for {@link HttpClientSseClientTransport}. + */ + public static class Builder { + + private final String baseUri; + + private String sseEndpoint = DEFAULT_SSE_ENDPOINT; + + private HttpClient.Builder clientBuilder = HttpClient.newBuilder(); + + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Creates a new builder with the specified base URI. + * @param baseUri the base URI of the MCP server + */ + public Builder(String baseUri) { + Assert.hasText(baseUri, "baseUri must not be empty"); + this.baseUri = baseUri; + } + + /** + * Sets the SSE endpoint path. + * @param sseEndpoint the SSE endpoint path + * @return this builder + */ + public Builder sseEndpoint(String sseEndpoint) { + Assert.hasText(sseEndpoint, "sseEndpoint must not be empty"); + this.sseEndpoint = sseEndpoint; + return this; + } + + /** + * Sets the HTTP client builder. + * @param clientBuilder the HTTP client builder + * @return this builder + */ + public Builder clientBuilder(HttpClient.Builder clientBuilder) { + Assert.notNull(clientBuilder, "clientBuilder must not be null"); + this.clientBuilder = clientBuilder; + return this; + } + + /** + * Sets the object mapper for JSON serialization/deserialization. + * @param objectMapper the object mapper + * @return this builder + */ + public Builder objectMapper(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "objectMapper must not be null"); + this.objectMapper = objectMapper; + return this; + } + + /** + * Builds a new {@link HttpClientSseClientTransport} instance. + * @return a new transport instance + */ + public HttpClientSseClientTransport build() { + return new HttpClientSseClientTransport(clientBuilder, baseUri, sseEndpoint, objectMapper); + } + + } + /** * Establishes the SSE connection with the server and sets up message handling. * @@ -137,7 +229,7 @@ public class HttpClientSseClientTransport implements McpClientTransport { CompletableFuture future = new CompletableFuture<>(); connectionFuture.set(future); - sseClient.subscribe(this.baseUri + SSE_ENDPOINT, new FlowSseClient.SseEventHandler() { + sseClient.subscribe(this.baseUri + this.sseEndpoint, new FlowSseClient.SseEventHandler() { @Override public void onEvent(SseEvent event) { if (isClosing) { diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index 152462b..a64b4a3 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -18,6 +18,7 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; +import io.modelcontextprotocol.util.Assert; import jakarta.servlet.AsyncContext; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; @@ -170,8 +171,8 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String pathInfo = request.getPathInfo(); - if (!sseEndpoint.equals(pathInfo)) { + String requestURI = request.getRequestURI(); + if (!requestURI.endsWith(sseEndpoint)) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } @@ -225,8 +226,8 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement return; } - String pathInfo = request.getPathInfo(); - if (!messageEndpoint.equals(pathInfo)) { + String requestURI = request.getRequestURI(); + if (!requestURI.endsWith(messageEndpoint)) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } @@ -429,4 +430,81 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement } + /** + * Creates a new Builder instance for configuring and creating instances of + * HttpServletSseServerTransportProvider. + * @return A new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for creating instances of HttpServletSseServerTransportProvider. + *

+ * This builder provides a fluent API for configuring and creating instances of + * HttpServletSseServerTransportProvider with custom settings. + */ + public static class Builder { + + private ObjectMapper objectMapper = new ObjectMapper(); + + private String messageEndpoint; + + private String sseEndpoint = DEFAULT_SSE_ENDPOINT; + + /** + * Sets the JSON object mapper to use for message serialization/deserialization. + * @param objectMapper The object mapper to use + * @return This builder instance for method chaining + */ + public Builder objectMapper(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this.objectMapper = objectMapper; + return this; + } + + /** + * Sets the endpoint path where clients will send their messages. + * @param messageEndpoint The message endpoint path + * @return This builder instance for method chaining + */ + public Builder messageEndpoint(String messageEndpoint) { + Assert.hasText(messageEndpoint, "Message endpoint must not be empty"); + this.messageEndpoint = messageEndpoint; + return this; + } + + /** + * Sets the endpoint path where clients will establish SSE connections. + *

+ * If not specified, the default value of {@link #DEFAULT_SSE_ENDPOINT} will be + * used. + * @param sseEndpoint The SSE endpoint path + * @return This builder instance for method chaining + */ + public Builder sseEndpoint(String sseEndpoint) { + Assert.hasText(sseEndpoint, "SSE endpoint must not be empty"); + this.sseEndpoint = sseEndpoint; + return this; + } + + /** + * Builds a new instance of HttpServletSseServerTransportProvider with the + * configured settings. + * @return A new HttpServletSseServerTransportProvider instance + * @throws IllegalStateException if objectMapper or messageEndpoint is not set + */ + public HttpServletSseServerTransportProvider build() { + if (objectMapper == null) { + throw new IllegalStateException("ObjectMapper must be set"); + } + if (messageEndpoint == null) { + throw new IllegalStateException("MessageEndpoint must be set"); + } + return new HttpServletSseServerTransportProvider(objectMapper, messageEndpoint, sseEndpoint); + } + + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java index 9de186b..81d9042 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java @@ -4,7 +4,6 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.junit.jupiter.api.Timeout; @@ -19,7 +18,7 @@ class ServletSseMcpAsyncServerTests extends AbstractMcpAsyncServerTests { @Override protected McpServerTransportProvider createMcpTransportProvider() { - return new HttpServletSseServerTransportProvider(new ObjectMapper(), "/mcp/message"); + return HttpServletSseServerTransportProvider.builder().messageEndpoint("/mcp/message").build(); } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java index 60dc53a..154cf3a 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java @@ -4,7 +4,6 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.junit.jupiter.api.Timeout; @@ -19,7 +18,7 @@ class ServletSseMcpSyncServerTests extends AbstractMcpSyncServerTests { @Override protected McpServerTransportProvider createMcpTransportProvider() { - return new HttpServletSseServerTransportProvider(new ObjectMapper(), "/mcp/message"); + return HttpServletSseServerTransportProvider.builder().messageEndpoint("/mcp/message").build(); } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java index fd8a4e9..1cd395e 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java @@ -47,7 +47,9 @@ public class HttpServletSseServerTransportProviderIntegrationTests { private static final int PORT = 8185; - private static final String MESSAGE_ENDPOINT = "/mcp/message"; + private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse"; + + private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message"; private HttpServletSseServerTransportProvider mcpServerTransportProvider; @@ -66,7 +68,11 @@ public class HttpServletSseServerTransportProviderIntegrationTests { Context context = tomcat.addContext("", baseDir); // Create and configure the transport provider - mcpServerTransportProvider = new HttpServletSseServerTransportProvider(new ObjectMapper(), MESSAGE_ENDPOINT); + mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder() + .objectMapper(new ObjectMapper()) + .messageEndpoint(CUSTOM_MESSAGE_ENDPOINT) + .sseEndpoint(CUSTOM_SSE_ENDPOINT) + .build(); // Add transport servlet to Tomcat org.apache.catalina.Wrapper wrapper = context.createWrapper(); @@ -87,7 +93,9 @@ public class HttpServletSseServerTransportProviderIntegrationTests { throw new RuntimeException("Failed to start Tomcat", e); } - this.clientBuilder = McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT)); + this.clientBuilder = McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT) + .sseEndpoint(CUSTOM_SSE_ENDPOINT) + .build()); } @AfterEach