Add Origin header validation

- Fixes #695
- Does not implement Host header validation yet

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Daniel Garnier-Moiroux 2026-02-04 18:56:23 +01:00 • committed by Daniel Garnier-Moiroux
commit 96c9bbe745
4 changed files with 1804 additions and 30 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright 2024-2024 the original author or authors.
* Copyright 2024-2026 the original author or authors.
*/
package io.modelcontextprotocol.server.transport;
@ -7,6 +7,7 @@ package io.modelcontextprotocol.server.transport;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@ -118,6 +119,11 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
private KeepAliveScheduler keepAliveScheduler;
/**
* Security validator for validating HTTP requests.
*/
private final ServerTransportSecurityValidator securityValidator;
/**
* Constructs a new WebMvcSseServerTransportProvider instance.
* @param jsonMapper The McpJsonMapper to use for JSON serialization/deserialization
@ -131,22 +137,26 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
* @param keepAliveInterval The interval for sending keep-alive messages to clients.
* @param contextExtractor The contextExtractor to fill in a
* {@link McpTransportContext}.
* @param securityValidator The security validator for validating HTTP requests.
* @throws IllegalArgumentException if any parameter is null
*/
private WebMvcSseServerTransportProvider(McpJsonMapper jsonMapper, String baseUrl, String messageEndpoint,
String sseEndpoint, Duration keepAliveInterval,
McpTransportContextExtractor<ServerRequest> contextExtractor) {
McpTransportContextExtractor<ServerRequest> contextExtractor,
ServerTransportSecurityValidator securityValidator) {
Assert.notNull(jsonMapper, "McpJsonMapper must not be null");
Assert.notNull(baseUrl, "Message base URL must not be null");
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
Assert.notNull(contextExtractor, "Context extractor must not be null");
Assert.notNull(securityValidator, "Security validator must not be null");
this.jsonMapper = jsonMapper;
this.baseUrl = baseUrl;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.contextExtractor = contextExtractor;
this.securityValidator = securityValidator;
this.routerFunction = RouterFunctions.route()
.GET(this.sseEndpoint, this::handleSseConnection)
.POST(this.messageEndpoint, this::handleMessage)
@ -255,6 +265,14 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
}
try {
Map<String, List<String>> headers = request.headers().asHttpHeaders();
this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
return ServerResponse.status(e.getStatusCode()).body(e.getMessage());
}
// Send initial endpoint event
return ServerResponse.sse(sseBuilder -> {
WebMvcMcpSessionTransport sessionTransport = new WebMvcMcpSessionTransport(sseBuilder);
@ -313,6 +331,14 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
}
try {
Map<String, List<String>> headers = request.headers().asHttpHeaders();
this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
return ServerResponse.status(e.getStatusCode()).body(e.getMessage());
}
if (request.param(SESSION_ID).isEmpty()) {
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
}
@ -474,6 +500,8 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
private McpTransportContextExtractor<ServerRequest> contextExtractor = (
serverRequest) -> McpTransportContext.EMPTY;
private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
/**
* Sets the JSON object mapper to use for message serialization/deserialization.
* @param jsonMapper The object mapper to use
@ -549,6 +577,18 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
return this;
}
/**
* Sets the security validator for validating HTTP requests.
* @param securityValidator The security validator to use. Must not be null.
* @return this builder instance
* @throws IllegalArgumentException if securityValidator is null
*/
public Builder securityValidator(ServerTransportSecurityValidator securityValidator) {
Assert.notNull(securityValidator, "Security validator must not be null");
this.securityValidator = securityValidator;
return this;
}
/**
* Builds a new instance of WebMvcSseServerTransportProvider with the configured
* settings.
@ -560,7 +600,7 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
throw new IllegalStateException("MessageEndpoint must be set");
}
return new WebMvcSseServerTransportProvider(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, contextExtractor);
baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, contextExtractor, securityValidator);
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2024-2024 the original author or authors.
* Copyright 2024-2026 the original author or authors.
*/
package io.modelcontextprotocol.server.transport;
@ -24,6 +24,7 @@ import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* Implementation of a WebMVC based {@link McpStatelessServerTransport}.
@ -50,15 +51,23 @@ public class WebMvcStatelessServerTransport implements McpStatelessServerTranspo
private volatile boolean isClosing = false;
/**
* Security validator for validating HTTP requests.
*/
private final ServerTransportSecurityValidator securityValidator;
private WebMvcStatelessServerTransport(McpJsonMapper jsonMapper, String mcpEndpoint,
McpTransportContextExtractor<ServerRequest> contextExtractor) {
McpTransportContextExtractor<ServerRequest> contextExtractor,
ServerTransportSecurityValidator securityValidator) {
Assert.notNull(jsonMapper, "jsonMapper must not be null");
Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null");
Assert.notNull(contextExtractor, "contextExtractor must not be null");
Assert.notNull(securityValidator, "Security validator must not be null");
this.jsonMapper = jsonMapper;
this.mcpEndpoint = mcpEndpoint;
this.contextExtractor = contextExtractor;
this.securityValidator = securityValidator;
this.routerFunction = RouterFunctions.route()
.GET(this.mcpEndpoint, this::handleGet)
.POST(this.mcpEndpoint, this::handlePost)
@ -100,6 +109,14 @@ public class WebMvcStatelessServerTransport implements McpStatelessServerTranspo
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
}
try {
Map<String, List<String>> headers = request.headers().asHttpHeaders();
this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
return ServerResponse.status(e.getStatusCode()).body(e.getMessage());
}
McpTransportContext transportContext = this.contextExtractor.extract(request);
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
@ -179,6 +196,8 @@ public class WebMvcStatelessServerTransport implements McpStatelessServerTranspo
private McpTransportContextExtractor<ServerRequest> contextExtractor = (
serverRequest) -> McpTransportContext.EMPTY;
private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
private Builder() {
// used by a static method
}
@ -224,6 +243,18 @@ public class WebMvcStatelessServerTransport implements McpStatelessServerTranspo
return this;
}
/**
* Sets the security validator for validating HTTP requests.
* @param securityValidator The security validator to use. Must not be null.
* @return this builder instance
* @throws IllegalArgumentException if securityValidator is null
*/
public Builder securityValidator(ServerTransportSecurityValidator securityValidator) {
Assert.notNull(securityValidator, "Security validator must not be null");
this.securityValidator = securityValidator;
return this;
}
/**
* Builds a new instance of {@link WebMvcStatelessServerTransport} with the
* configured settings.
@ -233,7 +264,7 @@ public class WebMvcStatelessServerTransport implements McpStatelessServerTranspo
public WebMvcStatelessServerTransport build() {
Assert.notNull(mcpEndpoint, "Message endpoint must be set");
return new WebMvcStatelessServerTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
mcpEndpoint, contextExtractor);
mcpEndpoint, contextExtractor, securityValidator);
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2024-2024 the original author or authors.
* Copyright 2024-2026 the original author or authors.
*/
package io.modelcontextprotocol.server.transport;
@ -7,6 +7,7 @@ package io.modelcontextprotocol.server.transport;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@ -102,6 +103,11 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer
private KeepAliveScheduler keepAliveScheduler;
/**
* Security validator for validating HTTP requests.
*/
private final ServerTransportSecurityValidator securityValidator;
/**
* Constructs a new WebMvcStreamableServerTransportProvider instance.
* @param jsonMapper The McpJsonMapper to use for JSON serialization/deserialization
@ -111,19 +117,26 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer
* @param mcpEndpoint The endpoint URI where clients should send their JSON-RPC
* messages via HTTP. This endpoint will handle GET, POST, and DELETE requests.
* @param disallowDelete Whether to disallow DELETE requests on the endpoint.
* @param contextExtractor The context extractor for transport context from the
* request.
* @param keepAliveInterval The interval for keep-alive pings. If null, no keep-alive
* will be scheduled.
* @param securityValidator The security validator for validating HTTP requests.
* @throws IllegalArgumentException if any parameter is null
*/
private WebMvcStreamableServerTransportProvider(McpJsonMapper jsonMapper, String mcpEndpoint,
boolean disallowDelete, McpTransportContextExtractor<ServerRequest> contextExtractor,
Duration keepAliveInterval) {
Duration keepAliveInterval, ServerTransportSecurityValidator securityValidator) {
Assert.notNull(jsonMapper, "McpJsonMapper must not be null");
Assert.notNull(mcpEndpoint, "MCP endpoint must not be null");
Assert.notNull(contextExtractor, "McpTransportContextExtractor must not be null");
Assert.notNull(securityValidator, "Security validator must not be null");
this.jsonMapper = jsonMapper;
this.mcpEndpoint = mcpEndpoint;
this.disallowDelete = disallowDelete;
this.contextExtractor = contextExtractor;
this.securityValidator = securityValidator;
this.routerFunction = RouterFunctions.route()
.GET(this.mcpEndpoint, this::handleGet)
.POST(this.mcpEndpoint, this::handlePost)
@ -233,6 +246,14 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
}
try {
Map<String, List<String>> headers = request.headers().asHttpHeaders();
this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
return ServerResponse.status(e.getStatusCode()).body(e.getMessage());
}
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!acceptHeaders.contains(MediaType.TEXT_EVENT_STREAM)) {
return ServerResponse.badRequest().body("Invalid Accept header. Expected TEXT_EVENT_STREAM");
@ -315,6 +336,14 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
}
try {
Map<String, List<String>> headers = request.headers().asHttpHeaders();
this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
return ServerResponse.status(e.getStatusCode()).body(e.getMessage());
}
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!acceptHeaders.contains(MediaType.TEXT_EVENT_STREAM)
|| !acceptHeaders.contains(MediaType.APPLICATION_JSON)) {
@ -427,6 +456,14 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
}
try {
Map<String, List<String>> headers = request.headers().asHttpHeaders();
this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
return ServerResponse.status(e.getStatusCode()).body(e.getMessage());
}
if (this.disallowDelete) {
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED).build();
}
@ -609,6 +646,8 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer
private Duration keepAliveInterval;
private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
/**
* Sets the McpJsonMapper to use for JSON serialization/deserialization of MCP
* messages.
@ -672,6 +711,18 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer
return this;
}
/**
* Sets the security validator for validating HTTP requests.
* @param securityValidator The security validator to use. Must not be null.
* @return this builder instance
* @throws IllegalArgumentException if securityValidator is null
*/
public Builder securityValidator(ServerTransportSecurityValidator securityValidator) {
Assert.notNull(securityValidator, "Security validator must not be null");
this.securityValidator = securityValidator;
return this;
}
/**
* Builds a new instance of {@link WebMvcStreamableServerTransportProvider} with
* the configured settings.
@ -682,7 +733,7 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer
Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set");
return new WebMvcStreamableServerTransportProvider(
jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, mcpEndpoint, disallowDelete,
contextExtractor, keepAliveInterval);
contextExtractor, keepAliveInterval, securityValidator);
}
}

View file

@ -0,0 +1,334 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.security;
import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Duration;
import java.util.stream.Stream;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpStatelessSyncServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.TestUtil;
import io.modelcontextprotocol.server.TomcatTestUtil;
import io.modelcontextprotocol.server.TomcatTestUtil.TomcatServer;
import io.modelcontextprotocol.server.transport.DefaultServerTransportSecurityValidator;
import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider;
import io.modelcontextprotocol.server.transport.WebMvcStatelessServerTransport;
import io.modelcontextprotocol.server.transport.WebMvcStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.BeforeParameterizedClassInvocation;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Named.named;
import static org.junit.jupiter.params.provider.Arguments.arguments;
/**
* Test the header security validation for all transport types.
*
* @author Daniel Garnier-Moiroux
*/
@ParameterizedClass
@MethodSource("transports")
public class ServerTransportSecurityIntegrationTests {
private static final String DISALLOWED_ORIGIN = "https://malicious.example.com";
@Parameter
private static Class<?> configClass;
private static TomcatServer tomcatServer;
private static String baseUrl;
@BeforeParameterizedClassInvocation
static void createTransportAndStartTomcat(Class<?> configClass) {
var port = TestUtil.findAvailablePort();
baseUrl = "http://localhost:" + port;
startTomcat(configClass, port);
}
@AfterAll
static void afterAll() {
stopTomcat();
}
private McpSyncClient mcpClient;
private TestRequestCustomizer requestCustomizer;
@BeforeEach
void setUp() {
mcpClient = tomcatServer.appContext().getBean(McpSyncClient.class);
requestCustomizer = tomcatServer.appContext().getBean(TestRequestCustomizer.class);
}
@AfterEach
void tearDown() {
mcpClient.close();
}
@Test
void originAllowed() {
requestCustomizer.setOriginHeader(baseUrl);
var result = mcpClient.initialize();
var tools = mcpClient.listTools();
assertThat(result.protocolVersion()).isNotEmpty();
assertThat(tools.tools()).isEmpty();
}
@Test
void noOrigin() {
requestCustomizer.setOriginHeader(null);
var result = mcpClient.initialize();
var tools = mcpClient.listTools();
assertThat(result.protocolVersion()).isNotEmpty();
assertThat(tools.tools()).isEmpty();
}
@Test
void connectOriginNotAllowed() {
requestCustomizer.setOriginHeader(DISALLOWED_ORIGIN);
assertThatThrownBy(() -> mcpClient.initialize());
}
@Test
void messageOriginNotAllowed() {
requestCustomizer.setOriginHeader(baseUrl);
mcpClient.initialize();
requestCustomizer.setOriginHeader(DISALLOWED_ORIGIN);
assertThatThrownBy(() -> mcpClient.listTools());
}
// ----------------------------------------------------
// Tomcat management
// ----------------------------------------------------
private static void startTomcat(Class<?> componentClass, int port) {
tomcatServer = TomcatTestUtil.createTomcatServer("", port, componentClass);
try {
tomcatServer.tomcat().start();
assertThat(tomcatServer.tomcat().getServer().getState()).isEqualTo(LifecycleState.STARTED);
}
catch (Exception e) {
throw new RuntimeException("Failed to start Tomcat", e);
}
}
private static void stopTomcat() {
if (tomcatServer != null) {
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);
}
}
}
}
// ----------------------------------------------------
// Transport servers to test
// ----------------------------------------------------
/**
* All transport types we want to test. We use a {@link MethodSource} rather than a
* {@link org.junit.jupiter.params.provider.ValueSource} to provide a readable name.
*/
static Stream<Arguments> transports() {
//@formatter:off
return Stream.of(
arguments(named("SSE", SseConfig.class)),
arguments(named("Streamable HTTP", StreamableHttpConfig.class)),
arguments(named("Stateless", StatelessConfig.class))
);
//@formatter:on
}
// ----------------------------------------------------
// Spring Configuration classes
// ----------------------------------------------------
@Configuration
static class CommonConfig {
@Bean
TestRequestCustomizer requestCustomizer() {
return new TestRequestCustomizer();
}
@Bean
DefaultServerTransportSecurityValidator validator() {
return DefaultServerTransportSecurityValidator.builder().allowedOrigin("http://localhost:*").build();
}
}
@Configuration
@EnableWebMvc
@Import(CommonConfig.class)
static class SseConfig {
@Bean
@Scope("prototype")
McpSyncClient createMcpClient(McpSyncHttpClientRequestCustomizer requestCustomizer) {
var transport = HttpClientSseClientTransport.builder(baseUrl)
.httpRequestCustomizer(requestCustomizer)
.jsonMapper(McpJsonMapper.getDefault())
.build();
return McpClient.sync(transport).initializationTimeout(Duration.ofMillis(500)).build();
}
@Bean
public WebMvcSseServerTransportProvider webMvcSseServerTransport(
DefaultServerTransportSecurityValidator validator) {
return WebMvcSseServerTransportProvider.builder()
.messageEndpoint("/mcp/message")
.securityValidator(validator)
.build();
}
@Bean
public RouterFunction<ServerResponse> routerFunction(WebMvcSseServerTransportProvider transportProvider) {
return transportProvider.getRouterFunction();
}
@Bean
public McpSyncServer mcpServer(WebMvcSseServerTransportProvider transportProvider) {
return McpServer.sync(transportProvider)
.serverInfo("test-server", "1.0.0")
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
.build();
}
}
@Configuration
@EnableWebMvc
@Import(CommonConfig.class)
static class StreamableHttpConfig {
@Bean
@Scope("prototype")
McpSyncClient createMcpClient(McpSyncHttpClientRequestCustomizer requestCustomizer) {
var transport = HttpClientStreamableHttpTransport.builder(baseUrl)
.httpRequestCustomizer(requestCustomizer)
.jsonMapper(McpJsonMapper.getDefault())
.openConnectionOnStartup(true)
.build();
return McpClient.sync(transport).initializationTimeout(Duration.ofMillis(500)).build();
}
@Bean
public WebMvcStreamableServerTransportProvider webMvcStreamableServerTransport(
DefaultServerTransportSecurityValidator validator) {
return WebMvcStreamableServerTransportProvider.builder().securityValidator(validator).build();
}
@Bean
public RouterFunction<ServerResponse> routerFunction(
WebMvcStreamableServerTransportProvider transportProvider) {
return transportProvider.getRouterFunction();
}
@Bean
public McpSyncServer mcpServer(WebMvcStreamableServerTransportProvider transportProvider) {
return McpServer.sync(transportProvider)
.serverInfo("test-server", "1.0.0")
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
.build();
}
}
@Configuration
@EnableWebMvc
@Import(CommonConfig.class)
static class StatelessConfig {
@Bean
@Scope("prototype")
McpSyncClient createMcpClient(McpSyncHttpClientRequestCustomizer requestCustomizer) {
var transport = HttpClientStreamableHttpTransport.builder(baseUrl)
.httpRequestCustomizer(requestCustomizer)
.jsonMapper(McpJsonMapper.getDefault())
.openConnectionOnStartup(true)
.build();
return McpClient.sync(transport).initializationTimeout(Duration.ofMillis(500)).build();
}
@Bean
public WebMvcStatelessServerTransport webMvcStatelessServerTransport(
DefaultServerTransportSecurityValidator validator) {
return WebMvcStatelessServerTransport.builder().securityValidator(validator).build();
}
@Bean
public RouterFunction<ServerResponse> routerFunction(WebMvcStatelessServerTransport transportProvider) {
return transportProvider.getRouterFunction();
}
@Bean
public McpStatelessSyncServer mcpStatelessServer(WebMvcStatelessServerTransport transportProvider) {
return McpServer.sync(transportProvider)
.serverInfo("test-server", "1.0.0")
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
.build();
}
}
static class TestRequestCustomizer implements McpSyncHttpClientRequestCustomizer {
private String originHeader = null;
@Override
public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body,
McpTransportContext context) {
if (originHeader != null) {
builder.header("Origin", originHeader);
}
}
public void setOriginHeader(String originHeader) {
this.originHeader = originHeader;
}
}
}