feat: HttpServlet Streamable HTTP server implementation (#290)

Implementation of the Streamable HTTP transport provider for the Servlet API.

---
Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Co-authored-by: Christian Tzolov <christian.tzolov@broadcom.com>
Co-authored-by: Zachary German <zdgerman@amazon.com>
This commit is contained in:
Zachary German 2025-07-30 08:58:28 -07:00 • committed by GitHub
commit bde1b6beec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 2234 additions and 1 deletions

View file

@ -0,0 +1,35 @@
/*
* Copyright 2024-2024 the original author or authors.
*/
package io.modelcontextprotocol.server;
import org.junit.jupiter.api.Timeout;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
/**
* Tests for {@link McpAsyncServer} using
* {@link HttpServletStreamableServerTransportProvider}.
*
* @author Christian Tzolov
*/
@Timeout(15)
class HttpServletStreamableAsyncServerTests extends AbstractMcpAsyncServerTests {
protected McpStreamableServerTransportProvider createMcpTransportProvider() {
return HttpServletStreamableServerTransportProvider.builder()
.objectMapper(new ObjectMapper())
.mcpEndpoint("/mcp/message")
.build();
}
@Override
protected McpServer.AsyncSpecification<?> prepareAsyncServerBuilder() {
return McpServer.async(createMcpTransportProvider());
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*/
package io.modelcontextprotocol.server;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.startup.Tomcat;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
import io.modelcontextprotocol.server.McpServer.SyncSpecification;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerIntegrationTests {
private static final int PORT = TomcatTestUtil.findAvailablePort();
private static final String MESSAGE_ENDPOINT = "/mcp/message";
private HttpServletStreamableServerTransportProvider mcpServerTransportProvider;
private Tomcat tomcat;
@BeforeEach
public void before() {
// Create and configure the transport provider
mcpServerTransportProvider = HttpServletStreamableServerTransportProvider.builder()
.objectMapper(new ObjectMapper())
.mcpEndpoint(MESSAGE_ENDPOINT)
.build();
tomcat = TomcatTestUtil.createTomcatServer("", PORT, mcpServerTransportProvider);
try {
tomcat.start();
assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
}
catch (Exception e) {
throw new RuntimeException("Failed to start Tomcat", e);
}
clientBuilders
.put("httpclient",
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
.endpoint(MESSAGE_ENDPOINT)
.build()).initializationTimeout(Duration.ofHours(10)).requestTimeout(Duration.ofHours(10)));
}
@Override
protected AsyncSpecification<?> prepareAsyncServerBuilder() {
return McpServer.async(this.mcpServerTransportProvider);
}
@Override
protected SyncSpecification<?> prepareSyncServerBuilder() {
return McpServer.sync(this.mcpServerTransportProvider);
}
@AfterEach
public void after() {
if (mcpServerTransportProvider != null) {
mcpServerTransportProvider.closeGracefully().block();
}
if (tomcat != null) {
try {
tomcat.stop();
tomcat.destroy();
}
catch (LifecycleException e) {
throw new RuntimeException("Failed to stop Tomcat", e);
}
}
}
@Override
protected void prepareClients(int port, String mcpEndpoint) {
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2024-2024 the original author or authors.
*/
package io.modelcontextprotocol.server;
import org.junit.jupiter.api.Timeout;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
/**
* Tests for {@link McpSyncServer} using
* {@link HttpServletStreamableServerTransportProvider}.
*
* @author Christian Tzolov
*/
@Timeout(15)
class HttpServletStreamableSyncServerTests extends AbstractMcpSyncServerTests {
protected McpStreamableServerTransportProvider createMcpTransportProvider() {
return HttpServletStreamableServerTransportProvider.builder()
.objectMapper(new ObjectMapper())
.mcpEndpoint("/mcp/message")
.build();
}
@Override
protected McpServer.SyncSpecification<?> prepareSyncServerBuilder() {
return McpServer.sync(createMcpTransportProvider());
}
}