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 <christian.tzolov@broadcom.com> Co-authored-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
25f3bad68d
commit
2934635d99
5 changed files with 189 additions and 13 deletions
|
|
@ -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<Void> 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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue