Skip to content

Commit 2934635

Browse files
OrGivatitzolov
andcommitted
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>
1 parent 25f3bad commit 2934635

File tree

5 files changed

+189
-13
lines changed

5 files changed

+189
-13
lines changed

mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java

+94-2
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,14 @@ public class HttpClientSseClientTransport implements McpClientTransport {
6565
private static final String ENDPOINT_EVENT_TYPE = "endpoint";
6666

6767
/** Default SSE endpoint path */
68-
private static final String SSE_ENDPOINT = "/sse";
68+
private static final String DEFAULT_SSE_ENDPOINT = "/sse";
6969

7070
/** Base URI for the MCP server */
7171
private final String baseUri;
7272

73+
/** SSE endpoint path */
74+
private final String sseEndpoint;
75+
7376
/** SSE client for handling server-sent events. Uses the /sse endpoint */
7477
private final FlowSseClient sseClient;
7578

@@ -110,15 +113,104 @@ public HttpClientSseClientTransport(String baseUri) {
110113
* @throws IllegalArgumentException if objectMapper or clientBuilder is null
111114
*/
112115
public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, ObjectMapper objectMapper) {
116+
this(clientBuilder, baseUri, DEFAULT_SSE_ENDPOINT, objectMapper);
117+
}
118+
119+
/**
120+
* Creates a new transport instance with custom HTTP client builder and object mapper.
121+
* @param clientBuilder the HTTP client builder to use
122+
* @param baseUri the base URI of the MCP server
123+
* @param sseEndpoint the SSE endpoint path
124+
* @param objectMapper the object mapper for JSON serialization/deserialization
125+
* @throws IllegalArgumentException if objectMapper or clientBuilder is null
126+
*/
127+
public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, String sseEndpoint,
128+
ObjectMapper objectMapper) {
113129
Assert.notNull(objectMapper, "ObjectMapper must not be null");
114130
Assert.hasText(baseUri, "baseUri must not be empty");
131+
Assert.hasText(sseEndpoint, "sseEndpoint must not be empty");
115132
Assert.notNull(clientBuilder, "clientBuilder must not be null");
116133
this.baseUri = baseUri;
134+
this.sseEndpoint = sseEndpoint;
117135
this.objectMapper = objectMapper;
118136
this.httpClient = clientBuilder.connectTimeout(Duration.ofSeconds(10)).build();
119137
this.sseClient = new FlowSseClient(this.httpClient);
120138
}
121139

140+
/**
141+
* Creates a new builder for {@link HttpClientSseClientTransport}.
142+
* @param baseUri the base URI of the MCP server
143+
* @return a new builder instance
144+
*/
145+
public static Builder builder(String baseUri) {
146+
return new Builder(baseUri);
147+
}
148+
149+
/**
150+
* Builder for {@link HttpClientSseClientTransport}.
151+
*/
152+
public static class Builder {
153+
154+
private final String baseUri;
155+
156+
private String sseEndpoint = DEFAULT_SSE_ENDPOINT;
157+
158+
private HttpClient.Builder clientBuilder = HttpClient.newBuilder();
159+
160+
private ObjectMapper objectMapper = new ObjectMapper();
161+
162+
/**
163+
* Creates a new builder with the specified base URI.
164+
* @param baseUri the base URI of the MCP server
165+
*/
166+
public Builder(String baseUri) {
167+
Assert.hasText(baseUri, "baseUri must not be empty");
168+
this.baseUri = baseUri;
169+
}
170+
171+
/**
172+
* Sets the SSE endpoint path.
173+
* @param sseEndpoint the SSE endpoint path
174+
* @return this builder
175+
*/
176+
public Builder sseEndpoint(String sseEndpoint) {
177+
Assert.hasText(sseEndpoint, "sseEndpoint must not be empty");
178+
this.sseEndpoint = sseEndpoint;
179+
return this;
180+
}
181+
182+
/**
183+
* Sets the HTTP client builder.
184+
* @param clientBuilder the HTTP client builder
185+
* @return this builder
186+
*/
187+
public Builder clientBuilder(HttpClient.Builder clientBuilder) {
188+
Assert.notNull(clientBuilder, "clientBuilder must not be null");
189+
this.clientBuilder = clientBuilder;
190+
return this;
191+
}
192+
193+
/**
194+
* Sets the object mapper for JSON serialization/deserialization.
195+
* @param objectMapper the object mapper
196+
* @return this builder
197+
*/
198+
public Builder objectMapper(ObjectMapper objectMapper) {
199+
Assert.notNull(objectMapper, "objectMapper must not be null");
200+
this.objectMapper = objectMapper;
201+
return this;
202+
}
203+
204+
/**
205+
* Builds a new {@link HttpClientSseClientTransport} instance.
206+
* @return a new transport instance
207+
*/
208+
public HttpClientSseClientTransport build() {
209+
return new HttpClientSseClientTransport(clientBuilder, baseUri, sseEndpoint, objectMapper);
210+
}
211+
212+
}
213+
122214
/**
123215
* Establishes the SSE connection with the server and sets up message handling.
124216
*
@@ -137,7 +229,7 @@ public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> h
137229
CompletableFuture<Void> future = new CompletableFuture<>();
138230
connectionFuture.set(future);
139231

140-
sseClient.subscribe(this.baseUri + SSE_ENDPOINT, new FlowSseClient.SseEventHandler() {
232+
sseClient.subscribe(this.baseUri + this.sseEndpoint, new FlowSseClient.SseEventHandler() {
141233
@Override
142234
public void onEvent(SseEvent event) {
143235
if (isClosing) {

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

+82-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.modelcontextprotocol.spec.McpServerSession;
1919
import io.modelcontextprotocol.spec.McpServerTransport;
2020
import io.modelcontextprotocol.spec.McpServerTransportProvider;
21+
import io.modelcontextprotocol.util.Assert;
2122
import jakarta.servlet.AsyncContext;
2223
import jakarta.servlet.ServletException;
2324
import jakarta.servlet.annotation.WebServlet;
@@ -170,8 +171,8 @@ public Mono<Void> notifyClients(String method, Map<String, Object> params) {
170171
protected void doGet(HttpServletRequest request, HttpServletResponse response)
171172
throws ServletException, IOException {
172173

173-
String pathInfo = request.getPathInfo();
174-
if (!sseEndpoint.equals(pathInfo)) {
174+
String requestURI = request.getRequestURI();
175+
if (!requestURI.endsWith(sseEndpoint)) {
175176
response.sendError(HttpServletResponse.SC_NOT_FOUND);
176177
return;
177178
}
@@ -225,8 +226,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
225226
return;
226227
}
227228

228-
String pathInfo = request.getPathInfo();
229-
if (!messageEndpoint.equals(pathInfo)) {
229+
String requestURI = request.getRequestURI();
230+
if (!requestURI.endsWith(messageEndpoint)) {
230231
response.sendError(HttpServletResponse.SC_NOT_FOUND);
231232
return;
232233
}
@@ -429,4 +430,81 @@ public void close() {
429430

430431
}
431432

433+
/**
434+
* Creates a new Builder instance for configuring and creating instances of
435+
* HttpServletSseServerTransportProvider.
436+
* @return A new Builder instance
437+
*/
438+
public static Builder builder() {
439+
return new Builder();
440+
}
441+
442+
/**
443+
* Builder for creating instances of HttpServletSseServerTransportProvider.
444+
* <p>
445+
* This builder provides a fluent API for configuring and creating instances of
446+
* HttpServletSseServerTransportProvider with custom settings.
447+
*/
448+
public static class Builder {
449+
450+
private ObjectMapper objectMapper = new ObjectMapper();
451+
452+
private String messageEndpoint;
453+
454+
private String sseEndpoint = DEFAULT_SSE_ENDPOINT;
455+
456+
/**
457+
* Sets the JSON object mapper to use for message serialization/deserialization.
458+
* @param objectMapper The object mapper to use
459+
* @return This builder instance for method chaining
460+
*/
461+
public Builder objectMapper(ObjectMapper objectMapper) {
462+
Assert.notNull(objectMapper, "ObjectMapper must not be null");
463+
this.objectMapper = objectMapper;
464+
return this;
465+
}
466+
467+
/**
468+
* Sets the endpoint path where clients will send their messages.
469+
* @param messageEndpoint The message endpoint path
470+
* @return This builder instance for method chaining
471+
*/
472+
public Builder messageEndpoint(String messageEndpoint) {
473+
Assert.hasText(messageEndpoint, "Message endpoint must not be empty");
474+
this.messageEndpoint = messageEndpoint;
475+
return this;
476+
}
477+
478+
/**
479+
* Sets the endpoint path where clients will establish SSE connections.
480+
* <p>
481+
* If not specified, the default value of {@link #DEFAULT_SSE_ENDPOINT} will be
482+
* used.
483+
* @param sseEndpoint The SSE endpoint path
484+
* @return This builder instance for method chaining
485+
*/
486+
public Builder sseEndpoint(String sseEndpoint) {
487+
Assert.hasText(sseEndpoint, "SSE endpoint must not be empty");
488+
this.sseEndpoint = sseEndpoint;
489+
return this;
490+
}
491+
492+
/**
493+
* Builds a new instance of HttpServletSseServerTransportProvider with the
494+
* configured settings.
495+
* @return A new HttpServletSseServerTransportProvider instance
496+
* @throws IllegalStateException if objectMapper or messageEndpoint is not set
497+
*/
498+
public HttpServletSseServerTransportProvider build() {
499+
if (objectMapper == null) {
500+
throw new IllegalStateException("ObjectMapper must be set");
501+
}
502+
if (messageEndpoint == null) {
503+
throw new IllegalStateException("MessageEndpoint must be set");
504+
}
505+
return new HttpServletSseServerTransportProvider(objectMapper, messageEndpoint, sseEndpoint);
506+
}
507+
508+
}
509+
432510
}

mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package io.modelcontextprotocol.server;
66

7-
import com.fasterxml.jackson.databind.ObjectMapper;
87
import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
98
import io.modelcontextprotocol.spec.McpServerTransportProvider;
109
import org.junit.jupiter.api.Timeout;
@@ -19,7 +18,7 @@ class ServletSseMcpAsyncServerTests extends AbstractMcpAsyncServerTests {
1918

2019
@Override
2120
protected McpServerTransportProvider createMcpTransportProvider() {
22-
return new HttpServletSseServerTransportProvider(new ObjectMapper(), "/mcp/message");
21+
return HttpServletSseServerTransportProvider.builder().messageEndpoint("/mcp/message").build();
2322
}
2423

2524
}

mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package io.modelcontextprotocol.server;
66

7-
import com.fasterxml.jackson.databind.ObjectMapper;
87
import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
98
import io.modelcontextprotocol.spec.McpServerTransportProvider;
109
import org.junit.jupiter.api.Timeout;
@@ -19,7 +18,7 @@ class ServletSseMcpSyncServerTests extends AbstractMcpSyncServerTests {
1918

2019
@Override
2120
protected McpServerTransportProvider createMcpTransportProvider() {
22-
return new HttpServletSseServerTransportProvider(new ObjectMapper(), "/mcp/message");
21+
return HttpServletSseServerTransportProvider.builder().messageEndpoint("/mcp/message").build();
2322
}
2423

2524
}

mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
4747

4848
private static final int PORT = 8185;
4949

50-
private static final String MESSAGE_ENDPOINT = "/mcp/message";
50+
private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse";
51+
52+
private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message";
5153

5254
private HttpServletSseServerTransportProvider mcpServerTransportProvider;
5355

@@ -66,7 +68,11 @@ public void before() {
6668
Context context = tomcat.addContext("", baseDir);
6769

6870
// Create and configure the transport provider
69-
mcpServerTransportProvider = new HttpServletSseServerTransportProvider(new ObjectMapper(), MESSAGE_ENDPOINT);
71+
mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
72+
.objectMapper(new ObjectMapper())
73+
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
74+
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
75+
.build();
7076

7177
// Add transport servlet to Tomcat
7278
org.apache.catalina.Wrapper wrapper = context.createWrapper();
@@ -87,7 +93,9 @@ public void before() {
8793
throw new RuntimeException("Failed to start Tomcat", e);
8894
}
8995

90-
this.clientBuilder = McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT));
96+
this.clientBuilder = McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT)
97+
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
98+
.build());
9199
}
92100

93101
@AfterEach

0 commit comments

Comments
 (0)