Skip to content

Commit 8fc72ae

Browse files
committed
feat(mcp): Add support for custom context paths in HTTP Servlet SSE server transport
Enhance HttpServletSseServerTransportProvider to support deployment under non-root context paths by: - Adding baseUrl field and DEFAULT_BASE_URL constant - Creating new constructor that accepts a baseUrl parameter - Extending Builder with baseUrl configuration method - Prepending baseUrl to message endpoint in SSE events - Add HttpServletSseServerCustomContextPathTests to verify custom context path functionality - Extract common Tomcat server setup code to TomcatTestUtil for test reuse Related to #79 Signed-off-by: Christian Tzolov <[email protected]>
1 parent b21cfab commit 8fc72ae

File tree

4 files changed

+167
-22
lines changed

4 files changed

+167
-22
lines changed

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

+35-2
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,14 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
8080
/** Event type for endpoint information */
8181
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
8282

83+
public static final String DEFAULT_BASE_URL = "";
84+
8385
/** JSON object mapper for serialization/deserialization */
8486
private final ObjectMapper objectMapper;
8587

88+
/** Base URL for the server transport */
89+
private final String baseUrl;
90+
8691
/** The endpoint path for handling client messages */
8792
private final String messageEndpoint;
8893

@@ -108,7 +113,22 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
108113
*/
109114
public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint,
110115
String sseEndpoint) {
116+
this(objectMapper, DEFAULT_BASE_URL, messageEndpoint, sseEndpoint);
117+
}
118+
119+
/**
120+
* Creates a new HttpServletSseServerTransportProvider instance with a custom SSE
121+
* endpoint.
122+
* @param objectMapper The JSON object mapper to use for message
123+
* serialization/deserialization
124+
* @param baseUrl The base URL for the server transport
125+
* @param messageEndpoint The endpoint path where clients will send their messages
126+
* @param sseEndpoint The endpoint path where clients will establish SSE connections
127+
*/
128+
public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
129+
String sseEndpoint) {
111130
this.objectMapper = objectMapper;
131+
this.baseUrl = baseUrl;
112132
this.messageEndpoint = messageEndpoint;
113133
this.sseEndpoint = sseEndpoint;
114134
}
@@ -203,7 +223,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
203223
this.sessions.put(sessionId, session);
204224

205225
// Send initial endpoint event
206-
this.sendEvent(writer, ENDPOINT_EVENT_TYPE, messageEndpoint + "?sessionId=" + sessionId);
226+
this.sendEvent(writer, ENDPOINT_EVENT_TYPE, this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
207227
}
208228

209229
/**
@@ -449,6 +469,8 @@ public static class Builder {
449469

450470
private ObjectMapper objectMapper = new ObjectMapper();
451471

472+
private String baseUrl = DEFAULT_BASE_URL;
473+
452474
private String messageEndpoint;
453475

454476
private String sseEndpoint = DEFAULT_SSE_ENDPOINT;
@@ -464,6 +486,17 @@ public Builder objectMapper(ObjectMapper objectMapper) {
464486
return this;
465487
}
466488

489+
/**
490+
* Sets the base URL for the server transport.
491+
* @param baseUrl The base URL to use
492+
* @return This builder instance for method chaining
493+
*/
494+
public Builder baseUrl(String baseUrl) {
495+
Assert.notNull(baseUrl, "Base URL must not be null");
496+
this.baseUrl = baseUrl;
497+
return this;
498+
}
499+
467500
/**
468501
* Sets the endpoint path where clients will send their messages.
469502
* @param messageEndpoint The message endpoint path
@@ -502,7 +535,7 @@ public HttpServletSseServerTransportProvider build() {
502535
if (messageEndpoint == null) {
503536
throw new IllegalStateException("MessageEndpoint must be set");
504537
}
505-
return new HttpServletSseServerTransportProvider(objectMapper, messageEndpoint, sseEndpoint);
538+
return new HttpServletSseServerTransportProvider(objectMapper, baseUrl, messageEndpoint, sseEndpoint);
506539
}
507540

508541
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2024 - 2024 the original author or authors.
3+
*/
4+
package io.modelcontextprotocol.server.transport;
5+
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import io.modelcontextprotocol.client.McpClient;
8+
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
9+
import io.modelcontextprotocol.server.McpServer;
10+
import io.modelcontextprotocol.spec.McpSchema;
11+
import org.apache.catalina.Context;
12+
import org.apache.catalina.LifecycleException;
13+
import org.apache.catalina.LifecycleState;
14+
import org.apache.catalina.startup.Tomcat;
15+
import org.junit.jupiter.api.AfterEach;
16+
import org.junit.jupiter.api.BeforeEach;
17+
import org.junit.jupiter.api.Test;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
public class HttpServletSseServerCustomContextPathTests {
22+
23+
private static final int PORT = 8195;
24+
25+
private static final String CUSTOM_CONTEXT_PATH = "/api/v1";
26+
27+
private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse";
28+
29+
private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message";
30+
31+
private HttpServletSseServerTransportProvider mcpServerTransportProvider;
32+
33+
McpClient.SyncSpec clientBuilder;
34+
35+
private Tomcat tomcat;
36+
37+
@BeforeEach
38+
public void before() {
39+
40+
// Create and configure the transport provider
41+
mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
42+
.objectMapper(new ObjectMapper())
43+
.baseUrl(CUSTOM_CONTEXT_PATH)
44+
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
45+
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
46+
.build();
47+
48+
tomcat = TomcatTestUtil.createTomcatServer(CUSTOM_CONTEXT_PATH, PORT, mcpServerTransportProvider);
49+
50+
try {
51+
tomcat.start();
52+
assertThat(tomcat.getServer().getState() == LifecycleState.STARTED);
53+
}
54+
catch (Exception e) {
55+
throw new RuntimeException("Failed to start Tomcat", e);
56+
}
57+
58+
this.clientBuilder = McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT)
59+
.sseEndpoint(CUSTOM_CONTEXT_PATH + CUSTOM_SSE_ENDPOINT)
60+
.build());
61+
}
62+
63+
@AfterEach
64+
public void after() {
65+
if (mcpServerTransportProvider != null) {
66+
mcpServerTransportProvider.closeGracefully().block();
67+
}
68+
if (tomcat != null) {
69+
try {
70+
tomcat.stop();
71+
tomcat.destroy();
72+
}
73+
catch (LifecycleException e) {
74+
throw new RuntimeException("Failed to stop Tomcat", e);
75+
}
76+
}
77+
}
78+
79+
@Test
80+
void testCustomContextPath() {
81+
McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build();
82+
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build();
83+
assertThat(client.initialize()).isNotNull();
84+
}
85+
86+
}

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

+1-20
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import io.modelcontextprotocol.spec.McpSchema.Root;
2727
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
2828
import io.modelcontextprotocol.spec.McpSchema.Tool;
29-
import org.apache.catalina.Context;
3029
import org.apache.catalina.LifecycleException;
3130
import org.apache.catalina.LifecycleState;
3231
import org.apache.catalina.startup.Tomcat;
@@ -59,33 +58,15 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
5958

6059
@BeforeEach
6160
public void before() {
62-
tomcat = new Tomcat();
63-
tomcat.setPort(PORT);
64-
65-
String baseDir = System.getProperty("java.io.tmpdir");
66-
tomcat.setBaseDir(baseDir);
67-
68-
Context context = tomcat.addContext("", baseDir);
69-
7061
// Create and configure the transport provider
7162
mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
7263
.objectMapper(new ObjectMapper())
7364
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
7465
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
7566
.build();
7667

77-
// Add transport servlet to Tomcat
78-
org.apache.catalina.Wrapper wrapper = context.createWrapper();
79-
wrapper.setName("mcpServlet");
80-
wrapper.setServlet(mcpServerTransportProvider);
81-
wrapper.setLoadOnStartup(1);
82-
wrapper.setAsyncSupported(true);
83-
context.addChild(wrapper);
84-
context.addServletMappingDecoded("/*", "mcpServlet");
85-
68+
tomcat = TomcatTestUtil.createTomcatServer("", PORT, mcpServerTransportProvider);
8669
try {
87-
var connector = tomcat.getConnector();
88-
connector.setAsyncTimeout(3000);
8970
tomcat.start();
9071
assertThat(tomcat.getServer().getState() == LifecycleState.STARTED);
9172
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
package io.modelcontextprotocol.server.transport;
5+
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import jakarta.servlet.Servlet;
8+
import org.apache.catalina.Context;
9+
import org.apache.catalina.LifecycleState;
10+
import org.apache.catalina.startup.Tomcat;
11+
12+
import static org.junit.Assert.assertThat;
13+
14+
/**
15+
* @author Christian Tzolov
16+
*/
17+
public class TomcatTestUtil {
18+
19+
public static Tomcat createTomcatServer(String contextPath, int port, Servlet servlet) {
20+
21+
var tomcat = new Tomcat();
22+
tomcat.setPort(port);
23+
24+
String baseDir = System.getProperty("java.io.tmpdir");
25+
tomcat.setBaseDir(baseDir);
26+
27+
// Context context = tomcat.addContext("", baseDir);
28+
Context context = tomcat.addContext(contextPath, baseDir);
29+
30+
// Add transport servlet to Tomcat
31+
org.apache.catalina.Wrapper wrapper = context.createWrapper();
32+
wrapper.setName("mcpServlet");
33+
wrapper.setServlet(servlet);
34+
wrapper.setLoadOnStartup(1);
35+
wrapper.setAsyncSupported(true);
36+
context.addChild(wrapper);
37+
context.addServletMappingDecoded("/*", "mcpServlet");
38+
39+
var connector = tomcat.getConnector();
40+
connector.setAsyncTimeout(3000);
41+
42+
return tomcat;
43+
}
44+
45+
}

0 commit comments

Comments
 (0)