Use single Tomcat instance per test class
- Instead of running one Tomcat per test method, run one per test - Improves the testing time of Servlet-based tests Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
parent
dc952a2856
commit
24fbb119a9
6 changed files with 238 additions and 230 deletions
|
|
@ -27,7 +27,9 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.apache.catalina.LifecycleException;
|
import org.apache.catalina.LifecycleException;
|
||||||
import org.apache.catalina.LifecycleState;
|
import org.apache.catalina.LifecycleState;
|
||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.Timeout;
|
import org.junit.jupiter.api.Timeout;
|
||||||
|
|
@ -44,14 +46,42 @@ class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationT
|
||||||
|
|
||||||
private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message";
|
private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message";
|
||||||
|
|
||||||
private HttpServletSseServerTransportProvider mcpServerTransportProvider;
|
// Tomcat is started once for the whole class; each test swaps in its own transport
|
||||||
|
private static final TomcatTestUtil.DelegatingServlet MCP_SERVLET = new TomcatTestUtil.DelegatingServlet();
|
||||||
|
|
||||||
private Tomcat tomcat;
|
private static Tomcat tomcat;
|
||||||
|
|
||||||
|
private HttpServletSseServerTransportProvider mcpServerTransportProvider;
|
||||||
|
|
||||||
static Stream<Arguments> clientsForTesting() {
|
static Stream<Arguments> clientsForTesting() {
|
||||||
return Stream.of(Arguments.of("httpclient"));
|
return Stream.of(Arguments.of("httpclient"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void beforeAll() {
|
||||||
|
tomcat = TomcatTestUtil.createTomcatServer("", PORT, MCP_SERVLET);
|
||||||
|
try {
|
||||||
|
tomcat.start();
|
||||||
|
assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to start Tomcat", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
public static void afterAll() {
|
||||||
|
if (tomcat != null) {
|
||||||
|
try {
|
||||||
|
tomcat.stop();
|
||||||
|
tomcat.destroy();
|
||||||
|
}
|
||||||
|
catch (LifecycleException e) {
|
||||||
|
throw new RuntimeException("Failed to stop Tomcat", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before() {
|
public void before() {
|
||||||
// Create and configure the transport provider
|
// Create and configure the transport provider
|
||||||
|
|
@ -61,15 +91,7 @@ class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationT
|
||||||
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
|
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
|
||||||
.maxRequestSize(MAX_REQUEST_SIZE)
|
.maxRequestSize(MAX_REQUEST_SIZE)
|
||||||
.build();
|
.build();
|
||||||
|
MCP_SERVLET.setDelegate(mcpServerTransportProvider);
|
||||||
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
|
clientBuilders
|
||||||
.put("httpclient",
|
.put("httpclient",
|
||||||
|
|
@ -93,15 +115,6 @@ class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationT
|
||||||
if (mcpServerTransportProvider != null) {
|
if (mcpServerTransportProvider != null) {
|
||||||
mcpServerTransportProvider.closeGracefully().block();
|
mcpServerTransportProvider.closeGracefully().block();
|
||||||
}
|
}
|
||||||
if (tomcat != null) {
|
|
||||||
try {
|
|
||||||
tomcat.stop();
|
|
||||||
tomcat.destroy();
|
|
||||||
}
|
|
||||||
catch (LifecycleException e) {
|
|
||||||
throw new RuntimeException("Failed to stop Tomcat", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,7 @@ import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import ch.qos.logback.classic.Level;
|
|
||||||
import ch.qos.logback.classic.Logger;
|
|
||||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
|
||||||
import ch.qos.logback.core.read.ListAppender;
|
|
||||||
import ch.qos.logback.classic.Level;
|
import ch.qos.logback.classic.Level;
|
||||||
import ch.qos.logback.classic.Logger;
|
import ch.qos.logback.classic.Logger;
|
||||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||||
|
|
@ -54,21 +49,17 @@ import net.javacrumbs.jsonunit.core.Option;
|
||||||
import org.apache.catalina.LifecycleException;
|
import org.apache.catalina.LifecycleException;
|
||||||
import org.apache.catalina.LifecycleState;
|
import org.apache.catalina.LifecycleState;
|
||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.Timeout;
|
import org.junit.jupiter.api.Timeout;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
import reactor.test.StepVerifier;
|
|
||||||
|
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
import org.springframework.mock.web.MockHttpServletResponse;
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
import org.springframework.web.client.RestClient;
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON;
|
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON;
|
||||||
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM;
|
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM;
|
||||||
import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
|
import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
|
||||||
|
|
@ -89,7 +80,9 @@ class HttpServletStatelessIntegrationTests {
|
||||||
|
|
||||||
private static final int MAX_REQUEST_SIZE = 2048;
|
private static final int MAX_REQUEST_SIZE = 2048;
|
||||||
|
|
||||||
private HttpServletStatelessServerTransport mcpStatelessServerTransport;
|
private static Tomcat tomcat;
|
||||||
|
|
||||||
|
private static HttpServletStatelessServerTransport mcpStatelessServerTransport;
|
||||||
|
|
||||||
private final McpClient.SyncSpec clientBuilder = McpClient
|
private final McpClient.SyncSpec clientBuilder = McpClient
|
||||||
.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
|
.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
|
||||||
|
|
@ -98,11 +91,9 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.initializationTimeout(Duration.ofHours(10))
|
.initializationTimeout(Duration.ofHours(10))
|
||||||
.requestTimeout(Duration.ofHours(10));
|
.requestTimeout(Duration.ofHours(10));
|
||||||
|
|
||||||
private Tomcat tomcat;
|
@BeforeAll
|
||||||
|
public static void beforeAll() {
|
||||||
@BeforeEach
|
mcpStatelessServerTransport = HttpServletStatelessServerTransport.builder()
|
||||||
public void before() {
|
|
||||||
this.mcpStatelessServerTransport = HttpServletStatelessServerTransport.builder()
|
|
||||||
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
|
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
|
||||||
.maxRequestSize(MAX_REQUEST_SIZE)
|
.maxRequestSize(MAX_REQUEST_SIZE)
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -117,8 +108,8 @@ class HttpServletStatelessIntegrationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterAll
|
||||||
public void after() {
|
public static void afterAll() {
|
||||||
if (mcpStatelessServerTransport != null) {
|
if (mcpStatelessServerTransport != null) {
|
||||||
mcpStatelessServerTransport.closeGracefully().block();
|
mcpStatelessServerTransport.closeGracefully().block();
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +146,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
return callResponse;
|
return callResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -173,22 +164,16 @@ class HttpServletStatelessIntegrationTests {
|
||||||
assertThat(response).isNotNull();
|
assertThat(response).isNotNull();
|
||||||
assertThat(response).isEqualTo(callResponse);
|
assertThat(response).isEqualTo(callResponse);
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitialize() {
|
void testInitialize() {
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport).build();
|
McpServer.sync(mcpStatelessServerTransport).build();
|
||||||
|
|
||||||
try (var mcpClient = clientBuilder.build()) {
|
try (var mcpClient = clientBuilder.build()) {
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -208,7 +193,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
return completionResponse;
|
return completionResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.prompts(new McpStatelessServerFeatures.SyncPromptSpecification(Prompt.builder("code_review")
|
.prompts(new McpStatelessServerFeatures.SyncPromptSpecification(Prompt.builder("code_review")
|
||||||
.title("Code review")
|
.title("Code review")
|
||||||
|
|
@ -241,9 +226,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
assertThat(completeRequest.get().argument().value()).isEqualTo("py");
|
assertThat(completeRequest.get().argument().value()).isEqualTo("py");
|
||||||
assertThat(completeRequest.get().ref().type()).isEqualTo(PromptReference.TYPE);
|
assertThat(completeRequest.get().ref().type()).isEqualTo(PromptReference.TYPE);
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -265,7 +247,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.of(PromptArgument.builder("topic").title("Topic").description("string").required(false).build()))
|
.of(PromptArgument.builder("topic").title("Topic").description("string").required(false).build()))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.prompts(
|
.prompts(
|
||||||
new McpStatelessServerFeatures.SyncPromptSpecification(prompt,
|
new McpStatelessServerFeatures.SyncPromptSpecification(prompt,
|
||||||
|
|
@ -291,9 +273,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
assertThat(result.completion().total()).isZero();
|
assertThat(result.completion().total()).isZero();
|
||||||
assertThat(result.completion().hasMore()).isFalse();
|
assertThat(result.completion().hasMore()).isFalse();
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -313,7 +292,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.mimeType("text/plain")
|
.mimeType("text/plain")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.resourceTemplates(
|
.resourceTemplates(
|
||||||
new McpStatelessServerFeatures.SyncResourceTemplateSpecification(template,
|
new McpStatelessServerFeatures.SyncResourceTemplateSpecification(template,
|
||||||
|
|
@ -339,14 +318,11 @@ class HttpServletStatelessIntegrationTests {
|
||||||
assertThat(result.completion().total()).isZero();
|
assertThat(result.completion().total()).isZero();
|
||||||
assertThat(result.completion().hasMore()).isFalse();
|
assertThat(result.completion().hasMore()).isFalse();
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCompletionForNonExistentPromptReturnsInvalidParams() {
|
void testCompletionForNonExistentPromptReturnsInvalidParams() {
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -364,14 +340,11 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.extracting(McpSchema.JSONRPCResponse.JSONRPCError::code)
|
.extracting(McpSchema.JSONRPCResponse.JSONRPCError::code)
|
||||||
.isEqualTo(ErrorCodes.INVALID_PARAMS);
|
.isEqualTo(ErrorCodes.INVALID_PARAMS);
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCompletionForNonExistentResourceReturnsResourceNotFound() {
|
void testCompletionForNonExistentResourceReturnsResourceNotFound() {
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -390,9 +363,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.extracting(McpSchema.JSONRPCResponse.JSONRPCError::code)
|
.extracting(McpSchema.JSONRPCResponse.JSONRPCError::code)
|
||||||
.isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND);
|
.isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -421,7 +391,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.build();
|
.build();
|
||||||
});
|
});
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool)
|
.tools(tool)
|
||||||
|
|
@ -459,9 +429,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.isEqualTo(json("""
|
.isEqualTo(json("""
|
||||||
{"result":5.0,"operation":"2 + 3","timestamp":"2024-01-01T10:00:00Z"}"""));
|
{"result":5.0,"operation":"2 + 3","timestamp":"2024-01-01T10:00:00Z"}"""));
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -492,7 +459,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool)
|
.tools(tool)
|
||||||
|
|
@ -517,9 +484,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
{"name":"John","age":30}"""), json("""
|
{"name":"John","age":30}"""), json("""
|
||||||
{"name":"Peter","age":25}"""));
|
{"name":"Peter","age":25}"""));
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.closeGracefully();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -545,7 +509,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool)
|
.tools(tool)
|
||||||
|
|
@ -572,9 +536,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
McpSchema.TextContent.builder("Error calling tool: Simulated in-handler error").build());
|
McpSchema.TextContent.builder("Error calling tool: Simulated in-handler error").build());
|
||||||
assertThat(response.structuredContent()).isNull();
|
assertThat(response.structuredContent()).isNull();
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.closeGracefully();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -599,7 +560,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.build();
|
.build();
|
||||||
});
|
});
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool)
|
.tools(tool)
|
||||||
|
|
@ -621,9 +582,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
String errorMessage = ((McpSchema.TextContent) response.content().get(0)).text();
|
String errorMessage = ((McpSchema.TextContent) response.content().get(0)).text();
|
||||||
assertThat(errorMessage).contains("Validation failed");
|
assertThat(errorMessage).contains("Validation failed");
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -643,7 +601,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
return CallToolResult.builder().addTextContent("Calculation completed").build();
|
return CallToolResult.builder().addTextContent("Calculation completed").build();
|
||||||
});
|
});
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.instructions("bla")
|
.instructions("bla")
|
||||||
|
|
@ -667,9 +625,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
assertThat(errorMessage).isEqualTo(
|
assertThat(errorMessage).isEqualTo(
|
||||||
"Response missing structured content which is expected when calling tool with non-empty outputSchema");
|
"Response missing structured content which is expected when calling tool with non-empty outputSchema");
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -738,9 +693,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.isEqualTo(json("""
|
.isEqualTo(json("""
|
||||||
{"count":3,"message":"Dynamic execution"}"""));
|
{"count":3,"message":"Dynamic execution"}"""));
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -787,13 +739,11 @@ class HttpServletStatelessIntegrationTests {
|
||||||
assertThat(jsonrpcResponse.error()).isNotNull();
|
assertThat(jsonrpcResponse.error()).isNotNull();
|
||||||
assertThat(jsonrpcResponse.error().code()).isEqualTo(ErrorCodes.INTERNAL_ERROR);
|
assertThat(jsonrpcResponse.error().code()).isEqualTo(ErrorCodes.INTERNAL_ERROR);
|
||||||
assertThat(jsonrpcResponse.error().message()).isEqualTo("testing");
|
assertThat(jsonrpcResponse.error().message()).isEqualTo("testing");
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMissingHandlerReturnsMethodNotFoundError() {
|
void testMissingHandlerReturnsMethodNotFoundError() {
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().build())
|
.capabilities(ServerCapabilities.builder().build())
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -828,9 +778,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
assertThat(response.get().error().code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND);
|
assertThat(response.get().error().code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND);
|
||||||
assertThat(response.get().error().message()).isEqualTo("Method not found: foo/bar");
|
assertThat(response.get().error().message()).isEqualTo("Method not found: foo/bar");
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.closeGracefully();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -841,7 +788,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
handlerLogger.addAppender(logAppender);
|
handlerLogger.addAppender(logAppender);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().build())
|
.capabilities(ServerCapabilities.builder().build())
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -849,9 +796,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
try (var mcpClient = clientBuilder.build()) {
|
try (var mcpClient = clientBuilder.build()) {
|
||||||
mcpClient.initialize(); // automatically sends notifications/initialized
|
mcpClient.initialize(); // automatically sends notifications/initialized
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
handlerLogger.detachAppender(logAppender);
|
handlerLogger.detachAppender(logAppender);
|
||||||
|
|
@ -869,7 +813,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
handlerLogger.addAppender(logAppender);
|
handlerLogger.addAppender(logAppender);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().build())
|
.capabilities(ServerCapabilities.builder().build())
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -878,9 +822,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
mcpClient.initialize();
|
mcpClient.initialize();
|
||||||
mcpClient.rootsListChangedNotification();
|
mcpClient.rootsListChangedNotification();
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
handlerLogger.detachAppender(logAppender);
|
handlerLogger.detachAppender(logAppender);
|
||||||
|
|
@ -913,7 +855,7 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
|
McpServer.sync(mcpStatelessServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().tools(false).build())
|
.capabilities(ServerCapabilities.builder().tools(false).build())
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -929,16 +871,11 @@ class HttpServletStatelessIntegrationTests {
|
||||||
.isInstanceOf(RuntimeException.class)
|
.isInstanceOf(RuntimeException.class)
|
||||||
.hasMessageContaining("413");
|
.hasMessageContaining("413");
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.closeGracefully();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsWhenBodyBytesExceedLimitWithoutContentLengthHeader() throws Exception {
|
void rejectsWhenBodyBytesExceedLimitWithoutContentLengthHeader() throws Exception {
|
||||||
var mcpServer = McpServer.sync(mcpStatelessServerTransport).build();
|
McpServer.sync(mcpStatelessServerTransport).build();
|
||||||
|
|
||||||
try {
|
|
||||||
var httpClient = HttpClient.newHttpClient();
|
var httpClient = HttpClient.newHttpClient();
|
||||||
|
|
||||||
// A publisher with unknown content length forces chunked transfer
|
// A publisher with unknown content length forces chunked transfer
|
||||||
|
|
@ -977,10 +914,6 @@ class HttpServletStatelessIntegrationTests {
|
||||||
var response = httpClient.send(request, HttpResponse.BodyHandlers.discarding());
|
var response = httpClient.send(request, HttpResponse.BodyHandlers.discarding());
|
||||||
assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
|
assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
mcpServer.closeGracefully();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private double evaluateExpression(String expression) {
|
private double evaluateExpression(String expression) {
|
||||||
// Simple expression evaluator for testing
|
// Simple expression evaluator for testing
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.apache.catalina.LifecycleException;
|
import org.apache.catalina.LifecycleException;
|
||||||
import org.apache.catalina.LifecycleState;
|
import org.apache.catalina.LifecycleState;
|
||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Timeout;
|
import org.junit.jupiter.api.Timeout;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
|
@ -40,29 +42,49 @@ class HttpServletStatelessSharedIntegrationTests extends AbstractStatelessIntegr
|
||||||
static McpTransportContextExtractor<HttpServletRequest> TEST_CONTEXT_EXTRACTOR = (request) -> McpTransportContext
|
static McpTransportContextExtractor<HttpServletRequest> TEST_CONTEXT_EXTRACTOR = (request) -> McpTransportContext
|
||||||
.create(Map.of("important", "value"));
|
.create(Map.of("important", "value"));
|
||||||
|
|
||||||
private HttpServletStatelessServerTransport mcpServerTransport;
|
// Tomcat is started once for the whole class; each test swaps in its own transport
|
||||||
|
private static final TomcatTestUtil.DelegatingServlet MCP_SERVLET = new TomcatTestUtil.DelegatingServlet();
|
||||||
|
|
||||||
private Tomcat tomcat;
|
private static Tomcat tomcat;
|
||||||
|
|
||||||
|
private HttpServletStatelessServerTransport mcpServerTransport;
|
||||||
|
|
||||||
static Stream<Arguments> clientsForTesting() {
|
static Stream<Arguments> clientsForTesting() {
|
||||||
return Stream.of(Arguments.of("httpclient"));
|
return Stream.of(Arguments.of("httpclient"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void beforeAll() {
|
||||||
|
tomcat = TomcatTestUtil.createTomcatServer("", PORT, MCP_SERVLET);
|
||||||
|
try {
|
||||||
|
tomcat.start();
|
||||||
|
assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to start Tomcat", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
public static void afterAll() {
|
||||||
|
if (tomcat != null) {
|
||||||
|
try {
|
||||||
|
tomcat.stop();
|
||||||
|
tomcat.destroy();
|
||||||
|
}
|
||||||
|
catch (LifecycleException e) {
|
||||||
|
throw new RuntimeException("Failed to stop Tomcat", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before() {
|
public void before() {
|
||||||
this.mcpServerTransport = HttpServletStatelessServerTransport.builder()
|
this.mcpServerTransport = HttpServletStatelessServerTransport.builder()
|
||||||
.contextExtractor(TEST_CONTEXT_EXTRACTOR)
|
.contextExtractor(TEST_CONTEXT_EXTRACTOR)
|
||||||
.messageEndpoint(MESSAGE_ENDPOINT)
|
.messageEndpoint(MESSAGE_ENDPOINT)
|
||||||
.build();
|
.build();
|
||||||
|
MCP_SERVLET.setDelegate(this.mcpServerTransport);
|
||||||
this.tomcat = TomcatTestUtil.createTomcatServer("", PORT, this.mcpServerTransport);
|
|
||||||
try {
|
|
||||||
this.tomcat.start();
|
|
||||||
assertThat(this.tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new RuntimeException("Failed to start Tomcat", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareClients(PORT, MESSAGE_ENDPOINT);
|
prepareClients(PORT, MESSAGE_ENDPOINT);
|
||||||
}
|
}
|
||||||
|
|
@ -90,15 +112,6 @@ class HttpServletStatelessSharedIntegrationTests extends AbstractStatelessIntegr
|
||||||
if (this.mcpServerTransport != null) {
|
if (this.mcpServerTransport != null) {
|
||||||
this.mcpServerTransport.closeGracefully().block();
|
this.mcpServerTransport.closeGracefully().block();
|
||||||
}
|
}
|
||||||
if (this.tomcat != null) {
|
|
||||||
try {
|
|
||||||
this.tomcat.stop();
|
|
||||||
this.tomcat.destroy();
|
|
||||||
}
|
|
||||||
catch (LifecycleException e) {
|
|
||||||
throw new RuntimeException("Failed to stop Tomcat", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ import org.apache.catalina.LifecycleException;
|
||||||
import org.apache.catalina.LifecycleState;
|
import org.apache.catalina.LifecycleState;
|
||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
import org.awaitility.Awaitility;
|
import org.awaitility.Awaitility;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.Timeout;
|
import org.junit.jupiter.api.Timeout;
|
||||||
|
|
@ -48,14 +50,42 @@ class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerInteg
|
||||||
|
|
||||||
private static final String MESSAGE_ENDPOINT = "/mcp/message";
|
private static final String MESSAGE_ENDPOINT = "/mcp/message";
|
||||||
|
|
||||||
private HttpServletStreamableServerTransportProvider mcpServerTransportProvider;
|
// Tomcat is started once for the whole class; each test swaps in its own transport
|
||||||
|
private static final TomcatTestUtil.DelegatingServlet MCP_SERVLET = new TomcatTestUtil.DelegatingServlet();
|
||||||
|
|
||||||
private Tomcat tomcat;
|
private static Tomcat tomcat;
|
||||||
|
|
||||||
|
private HttpServletStreamableServerTransportProvider mcpServerTransportProvider;
|
||||||
|
|
||||||
static Stream<Arguments> clientsForTesting() {
|
static Stream<Arguments> clientsForTesting() {
|
||||||
return Stream.of(Arguments.of("httpclient"));
|
return Stream.of(Arguments.of("httpclient"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void beforeAll() {
|
||||||
|
tomcat = TomcatTestUtil.createTomcatServer("", PORT, MCP_SERVLET);
|
||||||
|
try {
|
||||||
|
tomcat.start();
|
||||||
|
assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to start Tomcat", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
public static void afterAll() {
|
||||||
|
if (tomcat != null) {
|
||||||
|
try {
|
||||||
|
tomcat.stop();
|
||||||
|
tomcat.destroy();
|
||||||
|
}
|
||||||
|
catch (LifecycleException e) {
|
||||||
|
throw new RuntimeException("Failed to stop Tomcat", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before() {
|
public void before() {
|
||||||
// Create and configure the transport provider
|
// Create and configure the transport provider
|
||||||
|
|
@ -65,15 +95,7 @@ class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerInteg
|
||||||
.keepAliveInterval(Duration.ofSeconds(1))
|
.keepAliveInterval(Duration.ofSeconds(1))
|
||||||
.maxRequestSize(MAX_REQUEST_SIZE)
|
.maxRequestSize(MAX_REQUEST_SIZE)
|
||||||
.build();
|
.build();
|
||||||
|
MCP_SERVLET.setDelegate(mcpServerTransportProvider);
|
||||||
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
|
clientBuilders
|
||||||
.put("httpclient",
|
.put("httpclient",
|
||||||
|
|
@ -97,15 +119,6 @@ class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerInteg
|
||||||
if (mcpServerTransportProvider != null) {
|
if (mcpServerTransportProvider != null) {
|
||||||
mcpServerTransportProvider.closeGracefully().block();
|
mcpServerTransportProvider.closeGracefully().block();
|
||||||
}
|
}
|
||||||
if (tomcat != null) {
|
|
||||||
try {
|
|
||||||
tomcat.stop();
|
|
||||||
tomcat.destroy();
|
|
||||||
}
|
|
||||||
catch (LifecycleException e) {
|
|
||||||
throw new RuntimeException("Failed to stop Tomcat", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import org.apache.catalina.LifecycleException;
|
||||||
import org.apache.catalina.LifecycleState;
|
import org.apache.catalina.LifecycleState;
|
||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.McpClient;
|
import io.modelcontextprotocol.client.McpClient;
|
||||||
|
|
@ -48,19 +48,20 @@ import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
||||||
*/
|
*/
|
||||||
class McpCompletionTests {
|
class McpCompletionTests {
|
||||||
|
|
||||||
private HttpServletSseServerTransportProvider mcpServerTransportProvider;
|
|
||||||
|
|
||||||
private static final int PORT = TomcatTestUtil.findAvailablePort();
|
private static final int PORT = TomcatTestUtil.findAvailablePort();
|
||||||
|
|
||||||
private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message";
|
private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message";
|
||||||
|
|
||||||
McpClient.SyncSpec clientBuilder;
|
private static HttpServletSseServerTransportProvider mcpServerTransportProvider;
|
||||||
|
|
||||||
private Tomcat tomcat;
|
private static Tomcat tomcat;
|
||||||
|
|
||||||
@BeforeEach
|
final McpClient.SyncSpec clientBuilder = McpClient
|
||||||
public void before() {
|
.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT).build());
|
||||||
// Create and con figure the transport provider
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void beforeAll() {
|
||||||
|
// Create and configure the transport provider
|
||||||
mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
|
mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
|
||||||
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
|
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -73,12 +74,10 @@ class McpCompletionTests {
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to start Tomcat", e);
|
throw new RuntimeException("Failed to start Tomcat", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clientBuilder = McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT).build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterAll
|
||||||
public void after() {
|
public static void afterAll() {
|
||||||
if (mcpServerTransportProvider != null) {
|
if (mcpServerTransportProvider != null) {
|
||||||
mcpServerTransportProvider.closeGracefully().block();
|
mcpServerTransportProvider.closeGracefully().block();
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +87,7 @@ class McpCompletionTests {
|
||||||
tomcat.destroy();
|
tomcat.destroy();
|
||||||
}
|
}
|
||||||
catch (LifecycleException e) {
|
catch (LifecycleException e) {
|
||||||
e.printStackTrace();
|
throw new RuntimeException("Failed to stop Tomcat", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +108,7 @@ class McpCompletionTests {
|
||||||
.size(123L)
|
.size(123L)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.resources(new McpServerFeatures.SyncResourceSpecification(resource,
|
.resources(new McpServerFeatures.SyncResourceSpecification(resource,
|
||||||
(exchange, req) -> ReadResourceResult.builder(List.of()).build()))
|
(exchange, req) -> ReadResourceResult.builder(List.of()).build()))
|
||||||
|
|
@ -135,8 +134,6 @@ class McpCompletionTests {
|
||||||
assertThat(receivedRequest.get().context().arguments()).containsEntry("previous", "value");
|
assertThat(receivedRequest.get().context().arguments()).containsEntry("previous", "value");
|
||||||
assertThat(result.completion().values()).containsExactly("test-completion");
|
assertThat(result.completion().values()).containsExactly("test-completion");
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -153,7 +150,7 @@ class McpCompletionTests {
|
||||||
.arguments(List.of(PromptArgument.builder("arg").description("string").required(false).build()))
|
.arguments(List.of(PromptArgument.builder("arg").description("string").required(false).build()))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.prompts(new McpServerFeatures.SyncPromptSpecification(prompt,
|
.prompts(new McpServerFeatures.SyncPromptSpecification(prompt,
|
||||||
(mcpSyncServerExchange, getPromptRequest) -> null))
|
(mcpSyncServerExchange, getPromptRequest) -> null))
|
||||||
|
|
@ -178,8 +175,6 @@ class McpCompletionTests {
|
||||||
assertThat(contextWasNull.get()).isTrue();
|
assertThat(contextWasNull.get()).isTrue();
|
||||||
assertThat(result.completion().values()).containsExactly("no-context-completion");
|
assertThat(result.completion().values()).containsExactly("no-context-completion");
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -197,7 +192,7 @@ class McpCompletionTests {
|
||||||
.arguments(List.of(PromptArgument.builder("topic").description("string").required(false).build()))
|
.arguments(List.of(PromptArgument.builder("topic").description("string").required(false).build()))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.prompts(
|
.prompts(
|
||||||
new McpServerFeatures.SyncPromptSpecification(prompt,
|
new McpServerFeatures.SyncPromptSpecification(prompt,
|
||||||
|
|
@ -224,8 +219,6 @@ class McpCompletionTests {
|
||||||
assertThat(result.completion().total()).isZero();
|
assertThat(result.completion().total()).isZero();
|
||||||
assertThat(result.completion().hasMore()).isFalse();
|
assertThat(result.completion().hasMore()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -243,7 +236,7 @@ class McpCompletionTests {
|
||||||
.mimeType("text/plain")
|
.mimeType("text/plain")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.resourceTemplates(
|
.resourceTemplates(
|
||||||
new McpServerFeatures.SyncResourceTemplateSpecification(template,
|
new McpServerFeatures.SyncResourceTemplateSpecification(template,
|
||||||
|
|
@ -271,13 +264,11 @@ class McpCompletionTests {
|
||||||
assertThat(result.completion().total()).isZero();
|
assertThat(result.completion().total()).isZero();
|
||||||
assertThat(result.completion().hasMore()).isFalse();
|
assertThat(result.completion().hasMore()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCompletionForNonExistentPromptReturnsInvalidParams() {
|
void testCompletionForNonExistentPromptReturnsInvalidParams() {
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -297,13 +288,11 @@ class McpCompletionTests {
|
||||||
.extracting(McpSchema.JSONRPCResponse.JSONRPCError::code)
|
.extracting(McpSchema.JSONRPCResponse.JSONRPCError::code)
|
||||||
.isEqualTo(ErrorCodes.INVALID_PARAMS);
|
.isEqualTo(ErrorCodes.INVALID_PARAMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCompletionForNonExistentResourceReturnsResourceNotFound() {
|
void testCompletionForNonExistentResourceReturnsResourceNotFound() {
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -324,8 +313,6 @@ class McpCompletionTests {
|
||||||
.extracting(McpSchema.JSONRPCResponse.JSONRPCError::code)
|
.extracting(McpSchema.JSONRPCResponse.JSONRPCError::code)
|
||||||
.isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND);
|
.isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -364,7 +351,7 @@ class McpCompletionTests {
|
||||||
.size(456L)
|
.size(456L)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.resources(new McpServerFeatures.SyncResourceSpecification(resource,
|
.resources(new McpServerFeatures.SyncResourceSpecification(resource,
|
||||||
(exchange, req) -> ReadResourceResult.builder(List.of()).build()))
|
(exchange, req) -> ReadResourceResult.builder(List.of()).build()))
|
||||||
|
|
@ -407,8 +394,6 @@ class McpCompletionTests {
|
||||||
CompleteResult tableResult2 = mcpClient.completeCompletion(tableRequest2);
|
CompleteResult tableResult2 = mcpClient.completeCompletion(tableRequest2);
|
||||||
assertThat(tableResult2.completion().values()).containsExactly("products", "categories", "inventory");
|
assertThat(tableResult2.completion().values()).containsExactly("products", "categories", "inventory");
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -443,7 +428,7 @@ class McpCompletionTests {
|
||||||
.size(456L)
|
.size(456L)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.resources(new McpServerFeatures.SyncResourceSpecification(resource,
|
.resources(new McpServerFeatures.SyncResourceSpecification(resource,
|
||||||
(exchange, req) -> ReadResourceResult.builder(List.of()).build()))
|
(exchange, req) -> ReadResourceResult.builder(List.of()).build()))
|
||||||
|
|
@ -477,8 +462,6 @@ class McpCompletionTests {
|
||||||
CompleteResult resultWithContext = mcpClient.completeCompletion(requestWithContext);
|
CompleteResult resultWithContext = mcpClient.completeCompletion(requestWithContext);
|
||||||
assertThat(resultWithContext.completion().values()).containsExactly("users", "orders", "products");
|
assertThat(resultWithContext.completion().values()).containsExactly("users", "orders", "products");
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -488,7 +471,7 @@ class McpCompletionTests {
|
||||||
|
|
||||||
McpSchema.Prompt prompt = Prompt.builder("test-prompt").description("this is a test prompt").build();
|
McpSchema.Prompt prompt = Prompt.builder("test-prompt").description("this is a test prompt").build();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.prompts(new McpServerFeatures.SyncPromptSpecification(prompt,
|
.prompts(new McpServerFeatures.SyncPromptSpecification(prompt,
|
||||||
(mcpSyncServerExchange, getPromptRequest) -> null))
|
(mcpSyncServerExchange, getPromptRequest) -> null))
|
||||||
|
|
@ -510,8 +493,6 @@ class McpCompletionTests {
|
||||||
CompleteResult completeResult = mcpClient.completeCompletion(request);
|
CompleteResult completeResult = mcpClient.completeCompletion(request);
|
||||||
assertThat(completeResult.completion().values()).isEmpty();
|
assertThat(completeResult.completion().values()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ import java.net.ServerSocket;
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
import jakarta.servlet.Filter;
|
||||||
import jakarta.servlet.Servlet;
|
import jakarta.servlet.Servlet;
|
||||||
|
import jakarta.servlet.ServletConfig;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
import org.apache.catalina.Context;
|
import org.apache.catalina.Context;
|
||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
import org.apache.tomcat.util.descriptor.web.FilterDef;
|
import org.apache.tomcat.util.descriptor.web.FilterDef;
|
||||||
|
|
@ -63,6 +67,57 @@ public class TomcatTestUtil {
|
||||||
return tomcat;
|
return tomcat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A servlet forwarding all requests to a delegate that can be swapped between tests.
|
||||||
|
* Register one with {@link #createTomcatServer} to start Tomcat once per test class
|
||||||
|
* while still handing each test a freshly built transport.
|
||||||
|
*/
|
||||||
|
public static class DelegatingServlet implements Servlet {
|
||||||
|
|
||||||
|
private volatile ServletConfig servletConfig;
|
||||||
|
|
||||||
|
private volatile Servlet delegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the servlet handling subsequent requests. The delegate is not
|
||||||
|
* {@link Servlet#init(ServletConfig) initialized}, since the MCP servlet
|
||||||
|
* transports do not rely on their {@link ServletConfig}.
|
||||||
|
*/
|
||||||
|
public void setDelegate(Servlet delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ServletConfig config) {
|
||||||
|
this.servletConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServletConfig getServletConfig() {
|
||||||
|
return this.servletConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
|
||||||
|
var current = this.delegate;
|
||||||
|
if (current == null) {
|
||||||
|
throw new IllegalStateException("No delegate servlet has been set");
|
||||||
|
}
|
||||||
|
current.service(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getServletInfo() {
|
||||||
|
return DelegatingServlet.class.getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
this.delegate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds an available port on the local machine.
|
* Finds an available port on the local machine.
|
||||||
* @return an available port number
|
* @return an available port number
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue