- Add McpTransportContextExtractor to WebFlux/WebMVC SSE and Streamable transport providers - Enable extraction of HTTP transport metadata (headers, etc.) for use during request processing - Pass transport context through reactive context chain using McpTransportContext.KEY - Add contextExtractor() builder methods for configuring custom extractors - Update HttpServlet transport providers with same context extraction capability - Modify McpServerSession to properly propagate transport context to handlers - Add test coverage with TEST_CONTEXT_EXTRACTOR in integration tests This allows MCP feature implementations to access HTTP transport level metadata that was present at request time, enabling use cases like authentication, request tracing, and custom header processing. Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com> Co-authored-by: Christian Tzolov <christian.tzolov@broadcom.com> Co-authored-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
133 lines
4.2 KiB
Java
133 lines
4.2 KiB
Java
/*
|
|
* 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.junit.jupiter.api.AfterEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Timeout;
|
|
import org.springframework.context.annotation.Bean;
|
|
import org.springframework.context.annotation.Configuration;
|
|
import org.springframework.web.reactive.function.client.WebClient;
|
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
|
import org.springframework.web.servlet.function.RouterFunction;
|
|
import org.springframework.web.servlet.function.ServerRequest;
|
|
import org.springframework.web.servlet.function.ServerResponse;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
import io.modelcontextprotocol.AbstractMcpClientServerIntegrationTests;
|
|
import io.modelcontextprotocol.client.McpClient;
|
|
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
|
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
|
|
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
|
|
import io.modelcontextprotocol.server.McpServer.SingleSessionSyncSpecification;
|
|
import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider;
|
|
import reactor.core.scheduler.Schedulers;
|
|
|
|
@Timeout(15)
|
|
class WebMvcSseIntegrationTests extends AbstractMcpClientServerIntegrationTests {
|
|
|
|
private static final int PORT = TestUtil.findAvailablePort();
|
|
|
|
private static final String MESSAGE_ENDPOINT = "/mcp/message";
|
|
|
|
private WebMvcSseServerTransportProvider mcpServerTransportProvider;
|
|
|
|
static McpTransportContextExtractor<ServerRequest> TEST_CONTEXT_EXTRACTOR = (r, tc) -> {
|
|
tc.put("important", "value");
|
|
return tc;
|
|
};
|
|
|
|
@Override
|
|
protected void prepareClients(int port, String mcpEndpoint) {
|
|
|
|
clientBuilders.put("httpclient",
|
|
McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + port).build())
|
|
.requestTimeout(Duration.ofHours(10)));
|
|
|
|
clientBuilders.put("webflux", McpClient
|
|
.sync(WebFluxSseClientTransport.builder(WebClient.builder().baseUrl("http://localhost:" + port)).build())
|
|
.requestTimeout(Duration.ofHours(10)));
|
|
}
|
|
|
|
@Configuration
|
|
@EnableWebMvc
|
|
static class TestConfig {
|
|
|
|
@Bean
|
|
public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider() {
|
|
return WebMvcSseServerTransportProvider.builder()
|
|
.objectMapper(new ObjectMapper())
|
|
.messageEndpoint(MESSAGE_ENDPOINT)
|
|
.contextExtractor(TEST_CONTEXT_EXTRACTOR)
|
|
.build();
|
|
}
|
|
|
|
@Bean
|
|
public RouterFunction<ServerResponse> routerFunction(WebMvcSseServerTransportProvider transportProvider) {
|
|
return transportProvider.getRouterFunction();
|
|
}
|
|
|
|
}
|
|
|
|
private TomcatTestUtil.TomcatServer tomcatServer;
|
|
|
|
@BeforeEach
|
|
public void before() {
|
|
|
|
tomcatServer = TomcatTestUtil.createTomcatServer("", PORT, TestConfig.class);
|
|
|
|
try {
|
|
tomcatServer.tomcat().start();
|
|
assertThat(tomcatServer.tomcat().getServer().getState()).isEqualTo(LifecycleState.STARTED);
|
|
}
|
|
catch (Exception e) {
|
|
throw new RuntimeException("Failed to start Tomcat", e);
|
|
}
|
|
|
|
prepareClients(PORT, MESSAGE_ENDPOINT);
|
|
|
|
// Get the transport from Spring context
|
|
mcpServerTransportProvider = tomcatServer.appContext().getBean(WebMvcSseServerTransportProvider.class);
|
|
|
|
}
|
|
|
|
@AfterEach
|
|
public void after() {
|
|
reactor.netty.http.HttpResources.disposeLoopsAndConnections();
|
|
if (mcpServerTransportProvider != null) {
|
|
mcpServerTransportProvider.closeGracefully().block();
|
|
}
|
|
Schedulers.shutdownNow();
|
|
if (tomcatServer.appContext() != null) {
|
|
tomcatServer.appContext().close();
|
|
}
|
|
if (tomcatServer.tomcat() != null) {
|
|
try {
|
|
tomcatServer.tomcat().stop();
|
|
tomcatServer.tomcat().destroy();
|
|
}
|
|
catch (LifecycleException e) {
|
|
throw new RuntimeException("Failed to stop Tomcat", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected AsyncSpecification<?> prepareAsyncServerBuilder() {
|
|
return McpServer.async(mcpServerTransportProvider);
|
|
}
|
|
|
|
@Override
|
|
protected SingleSessionSyncSpecification prepareSyncServerBuilder() {
|
|
return McpServer.sync(mcpServerTransportProvider);
|
|
}
|
|
|
|
}
|