This is an automated email from the ASF dual-hosted git repository.

sihengyu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 6f82818371 feat: add CORS support with configurable allowed headers in 
MCP server (#6295)
6f82818371 is described below

commit 6f828183710981cd5635b4bb38bc6229df9cfa5e
Author: aias00 <[email protected]>
AuthorDate: Wed Feb 25 18:11:20 2026 +0800

    feat: add CORS support with configurable allowed headers in MCP server 
(#6295)
    
    * feat: add CORS support with configurable allowed headers in MCP server
    
    * Update 
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/McpServerPlugin.java
    
    Co-authored-by: Copilot <[email protected]>
    
    * feat: enhance CORS support by refining allowed methods and headers 
handling
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 .../shenyu/plugin/mcp/server/McpServerPlugin.java  |  93 ++++++++++++++-
 .../mcp/server/manager/ShenyuMcpServerManager.java |  28 ++++-
 ...henyuStreamableHttpServerTransportProvider.java | 132 ++++++++++++++++++---
 .../transport/StreamableHttpProviderBuilder.java   |  15 ++-
 .../plugin/mcp/server/McpServerPluginTest.java     |  52 ++++++++
 ...uStreamableHttpServerTransportProviderTest.java |  87 ++++++++++++++
 .../mcp/server/McpServerPluginConfiguration.java   |  24 +++-
 7 files changed, 400 insertions(+), 31 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/McpServerPlugin.java
 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/McpServerPlugin.java
index ec540683c7..0c268eb361 100644
--- 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/McpServerPlugin.java
+++ 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/McpServerPlugin.java
@@ -36,6 +36,7 @@ import 
org.apache.shenyu.plugin.mcp.server.transport.SseEventFormatter;
 import org.apache.shenyu.plugin.mcp.server.transport.MessageHandlingResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.codec.HttpMessageReader;
 import org.springframework.web.reactive.function.server.ServerRequest;
@@ -43,9 +44,11 @@ import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.nio.charset.StandardCharsets;
 
 /**
@@ -109,10 +112,19 @@ public class McpServerPlugin extends AbstractShenyuPlugin 
{
      */
     private static final String BEARER_PREFIX = "Bearer ";
 
+    private static final String CORS_ALLOW_METHODS = "GET, POST, OPTIONS";
+
+    private static final String CORS_STREAMABLE_ALLOW_METHODS = "POST, 
OPTIONS";
+
+    private static final String CORS_FALLBACK_ALLOW_HEADERS =
+            "Content-Type, Mcp-Session-Id, Authorization, Last-Event-ID, 
Mcp-Protocol-Version, X-Request, XRequest, xrequest";
+
     private final ShenyuMcpServerManager shenyuMcpServerManager;
 
     private final List<HttpMessageReader<?>> messageReaders;
 
+    private final String configuredCorsAllowHeaders;
+
     /**
      * Constructs a new MCP server plugin.
      *
@@ -121,8 +133,22 @@ public class McpServerPlugin extends AbstractShenyuPlugin {
      */
     public McpServerPlugin(final ShenyuMcpServerManager shenyuMcpServerManager,
                            final List<HttpMessageReader<?>> messageReaders) {
+        this(shenyuMcpServerManager, messageReaders, null);
+    }
+
+    /**
+     * Constructs a new MCP server plugin.
+     *
+     * @param shenyuMcpServerManager   the MCP server manager for handling 
transport providers
+     * @param messageReaders           the HTTP message readers for request 
processing
+     * @param configuredCorsAllowHeaders CORS allow headers configured by 
{@code shenyu.cross.allowedHeaders}
+     */
+    public McpServerPlugin(final ShenyuMcpServerManager shenyuMcpServerManager,
+                           final List<HttpMessageReader<?>> messageReaders,
+                           final String configuredCorsAllowHeaders) {
         this.shenyuMcpServerManager = shenyuMcpServerManager;
         this.messageReaders = messageReaders;
+        this.configuredCorsAllowHeaders = configuredCorsAllowHeaders;
     }
 
     @Override
@@ -203,6 +229,10 @@ public class McpServerPlugin extends AbstractShenyuPlugin {
                                        final SelectorData selector,
                                        final String uri) {
 
+        if 
("OPTIONS".equalsIgnoreCase(exchange.getRequest().getMethod().name())) {
+            return handleCorsPreflight(exchange, uri);
+        }
+
         if (isStreamableHttpProtocol(uri)) {
             return handleStreamableHttpRequest(exchange, chain, request, uri);
         } else if (isSseProtocol(uri)) {
@@ -276,6 +306,19 @@ public class McpServerPlugin extends AbstractShenyuPlugin {
         return uri.contains(SSE_PATH) || uri.endsWith(SSE_PATH) || 
uri.endsWith(MESSAGE_ENDPOINT);
     }
 
+    /**
+     * Handles CORS preflight (OPTIONS) requests.
+     *
+     * @param exchange the server web exchange
+     * @return a Mono representing completion
+     */
+    private Mono<Void> handleCorsPreflight(final ServerWebExchange exchange, 
final String uri) {
+        exchange.getResponse().setStatusCode(HttpStatus.OK);
+        setCorsHeaders(exchange, resolveAllowMethods(uri));
+        exchange.getResponse().getHeaders().set("Access-Control-Max-Age", 
"3600");
+        return exchange.getResponse().setComplete();
+    }
+
     /**
      * Handles Streamable HTTP MCP requests with unified endpoint processing.
      *
@@ -579,11 +622,51 @@ public class McpServerPlugin extends AbstractShenyuPlugin 
{
      * @param exchange the server web exchange
      */
     private void setCorsHeaders(final ServerWebExchange exchange) {
-        exchange.getResponse().getHeaders().set("Access-Control-Allow-Origin", 
"*");
-        exchange.getResponse().getHeaders().set("Access-Control-Allow-Headers",
-                "Content-Type, Mcp-Session-Id, Authorization, Last-Event-ID, 
Mcp-Protocol-Version");
-        exchange.getResponse().getHeaders().set("Access-Control-Allow-Methods",
-                "GET, POST, OPTIONS");
+        setCorsHeaders(exchange, 
resolveAllowMethods(exchange.getRequest().getURI().getRawPath()));
+    }
+
+    private void setCorsHeaders(final ServerWebExchange exchange, final String 
allowMethods) {
+        exchange.getResponse().getHeaders().set("Access-Control-Allow-Origin", 
resolveAllowOrigin(exchange));
+        
exchange.getResponse().getHeaders().set("Access-Control-Allow-Headers", 
resolveAllowHeaders(exchange));
+        
exchange.getResponse().getHeaders().set("Access-Control-Allow-Methods", 
allowMethods);
+        mergeVaryHeaders(exchange);
+    }
+
+    private String resolveAllowMethods(final String uri) {
+        return isStreamableHttpProtocol(uri) ? CORS_STREAMABLE_ALLOW_METHODS : 
CORS_ALLOW_METHODS;
+    }
+
+    private String resolveAllowOrigin(final ServerWebExchange exchange) {
+        final String origin = 
exchange.getRequest().getHeaders().getFirst("Origin");
+        return Objects.nonNull(origin) && !origin.isBlank() ? origin : "*";
+    }
+
+    private String resolveAllowHeaders(final ServerWebExchange exchange) {
+        final Set<String> allowedHeaders = new LinkedHashSet<>();
+        final String allowHeaders = 
Objects.nonNull(configuredCorsAllowHeaders) && 
!configuredCorsAllowHeaders.isBlank()
+                ? configuredCorsAllowHeaders : CORS_FALLBACK_ALLOW_HEADERS;
+        for (String header : allowHeaders.split(",")) {
+            final String trimmed = header.trim();
+            if (!trimmed.isEmpty()) {
+                allowedHeaders.add(trimmed);
+            }
+        }
+        return String.join(", ", allowedHeaders);
+    }
+
+    private void mergeVaryHeaders(final ServerWebExchange exchange) {
+        final Set<String> varyValues = new LinkedHashSet<>();
+        for (String varyHeader : 
exchange.getResponse().getHeaders().getOrEmpty(HttpHeaders.VARY)) {
+            for (String varyValue : varyHeader.split(",")) {
+                final String trimmed = varyValue.trim();
+                if (!trimmed.isEmpty()) {
+                    varyValues.add(trimmed);
+                }
+            }
+        }
+        varyValues.add("Origin");
+        varyValues.add("Access-Control-Request-Headers");
+        exchange.getResponse().getHeaders().setVary(List.copyOf(varyValues));
     }
 
     /**
diff --git 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java
 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java
index e31493d587..f60589a91a 100644
--- 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java
+++ 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java
@@ -37,12 +37,12 @@ import org.springframework.util.AntPathMatcher;
 import org.springframework.web.reactive.function.server.HandlerFunction;
 
 import java.time.Duration;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.Set;
-import java.util.HashSet;
-import java.util.Collections;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Enhanced Manager for MCP servers supporting shared server instances across 
multiple transport protocols.
@@ -75,6 +75,11 @@ public class ShenyuMcpServerManager {
      */
     private final ObjectMapper objectMapper = new ObjectMapper();
 
+    /**
+     * CORS allow headers configured by {@code shenyu.cross.allowedHeaders}.
+     */
+    private final String corsAllowedHeaders;
+
     /**
      * Map to store normalized path to shared McpAsyncServer mapping.
      * Key: normalized server path, Value: shared McpAsyncServer instance
@@ -91,6 +96,22 @@ public class ShenyuMcpServerManager {
      */
     private final Map<String, CompositeTransportProvider> 
compositeTransportMap = new ConcurrentHashMap<>();
 
+    /**
+     * Instantiates a new manager with default CORS allow headers handling.
+     */
+    public ShenyuMcpServerManager() {
+        this(null);
+    }
+
+    /**
+     * Instantiates a new manager.
+     *
+     * @param corsAllowedHeaders CORS allow headers configured by {@code 
shenyu.cross.allowedHeaders}
+     */
+    public ShenyuMcpServerManager(final String corsAllowedHeaders) {
+        this.corsAllowedHeaders = corsAllowedHeaders;
+    }
+
     /**
      * Get or create a shared MCP server for the given path, supporting 
multiple transport protocols.
      *
@@ -244,6 +265,7 @@ public class ShenyuMcpServerManager {
         ShenyuStreamableHttpServerTransportProvider transportProvider = 
ShenyuStreamableHttpServerTransportProvider.builder()
                 .objectMapper(objectMapper)
                 .endpoint(originalUri)
+                .allowedHeaders(corsAllowedHeaders)
                 .build();
 
         // Register routes for original URI
diff --git 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
index 31fa494bb3..f67c9c8d51 100644
--- 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
+++ 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
@@ -30,6 +30,7 @@ import io.modelcontextprotocol.util.Assert;
 import org.apache.shenyu.plugin.mcp.server.holder.ShenyuMcpExchangeHolder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.web.reactive.function.server.ServerRequest;
@@ -39,8 +40,12 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -92,10 +97,17 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
 
     private static final String SERVER_VERSION = "1.0.0";
 
+    private static final String CORS_ALLOW_METHODS = "POST, OPTIONS";
+
+    private static final String CORS_FALLBACK_ALLOW_HEADERS =
+            "Content-Type, Mcp-Session-Id, Authorization, Last-Event-ID, 
Mcp-Protocol-Version, X-Request, XRequest, xrequest";
+
     private final ObjectMapper objectMapper;
 
     private final McpJsonMapper jsonMapper;
 
+    private final String configuredCorsAllowHeaders;
+
     private McpServerSession.Factory sessionFactory;
 
     /**
@@ -123,10 +135,25 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
      * @throws IllegalArgumentException if objectMapper or endpoint is null
      */
     public ShenyuStreamableHttpServerTransportProvider(final ObjectMapper 
objectMapper, final String endpoint) {
+        this(objectMapper, endpoint, null);
+    }
+
+    /**
+     * Constructs a new Streamable HTTP server transport provider instance.
+     *
+     * @param objectMapper                The ObjectMapper to use for JSON 
serialization/deserialization
+     * @param endpoint                    The endpoint path for the Streamable 
HTTP MCP transport
+     * @param configuredCorsAllowHeaders  CORS allow headers configured by 
{@code shenyu.cross.allowedHeaders}
+     * @throws IllegalArgumentException if objectMapper or endpoint is null
+     */
+    public ShenyuStreamableHttpServerTransportProvider(final ObjectMapper 
objectMapper,
+                                                       final String endpoint,
+                                                       final String 
configuredCorsAllowHeaders) {
         Assert.notNull(objectMapper, "ObjectMapper must not be null");
         Assert.notNull(endpoint, "Endpoint must not be null");
         this.objectMapper = objectMapper;
         this.jsonMapper = new JacksonMcpJsonMapper(objectMapper);
+        this.configuredCorsAllowHeaders = configuredCorsAllowHeaders;
         LOGGER.debug("Created Streamable HTTP transport provider for endpoint: 
{}", endpoint);
     }
 
@@ -189,24 +216,19 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
         if (isClosing) {
             return 
ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is 
shutting down");
         }
-        if (Objects.isNull(sessionFactory)) {
-            LOGGER.error("SessionFactory is null - MCP server not properly 
initialized");
-            return 
ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).bodyValue("MCP server 
not properly initialized");
-        }
         if ("OPTIONS".equalsIgnoreCase(request.methodName())) {
             // Handle CORS preflight requests
-            return ServerResponse.ok()
-                    .header("Access-Control-Allow-Origin", "*")
-                    .header("Access-Control-Allow-Headers", "Content-Type, 
Mcp-Session-Id, Authorization, Mcp-Protocol-Version")
-                    .header("Access-Control-Allow-Methods", "GET, POST, 
OPTIONS")
+            return applyCorsHeaders(request, ServerResponse.ok(), 
CORS_ALLOW_METHODS)
                     .header("Access-Control-Max-Age", "3600")
                     .build();
-        } else if ("GET".equalsIgnoreCase(request.methodName())) {
+        }
+        if (Objects.isNull(sessionFactory)) {
+            LOGGER.error("SessionFactory is null - MCP server not properly 
initialized");
+            return 
ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).bodyValue("MCP server 
not properly initialized");
+        }
+        if ("GET".equalsIgnoreCase(request.methodName())) {
             // Streamable HTTP protocol does not support GET requests, return 
405 error
-            return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED)
-                    .header("Access-Control-Allow-Origin", "*")
-                    .header("Access-Control-Allow-Headers", "Content-Type, 
Mcp-Session-Id, Authorization, Mcp-Protocol-Version")
-                    .header("Access-Control-Allow-Methods", "POST, OPTIONS")
+            return applyCorsHeaders(request, 
ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED), CORS_ALLOW_METHODS)
                     .header("Allow", "POST, OPTIONS")
                     .contentType(MediaType.APPLICATION_JSON)
                     .bodyValue(new java.util.HashMap<String, Object>() {{
@@ -215,14 +237,13 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
                                     put("message", "Streamable HTTP does not 
support GET requests. Please use POST requests for all MCP operations.");
                                 }});
                         }});
-        } else if ("POST".equalsIgnoreCase(request.methodName())) {
+        }
+        if ("POST".equalsIgnoreCase(request.methodName())) {
             // Extract ServerWebExchange from ServerRequest
             final ServerWebExchange exchange = request.exchange();
             return handleMessageEndpoint(exchange, request).flatMap(result -> {
-                ServerResponse.BodyBuilder builder = 
ServerResponse.status(HttpStatus.valueOf(result.getStatusCode()))
-                        .header("Access-Control-Allow-Origin", "*")
-                        .header("Access-Control-Allow-Headers", "Content-Type, 
Mcp-Session-Id, Authorization, Mcp-Protocol-Version")
-                        .header("Access-Control-Allow-Methods", "GET, POST, 
OPTIONS");
+                ServerResponse.BodyBuilder builder = applyCorsHeaders(request,
+                        
ServerResponse.status(HttpStatus.valueOf(result.getStatusCode())), 
CORS_ALLOW_METHODS);
                 if (Objects.nonNull(result.getSessionId())) {
                     builder.header(SESSION_ID_HEADER, result.getSessionId());
                 }
@@ -643,6 +664,81 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
         return response;
     }
 
+    private ServerResponse.BodyBuilder applyCorsHeaders(final ServerRequest 
request,
+                                                        final 
ServerResponse.BodyBuilder builder,
+                                                        final String 
allowMethods) {
+        return builder.headers(headers -> {
+            headers.set("Access-Control-Allow-Origin", 
resolveAllowOrigin(request));
+            headers.set("Access-Control-Allow-Headers", 
resolveAllowHeaders(request));
+            headers.set("Access-Control-Allow-Methods", allowMethods);
+            mergeVaryHeaders(headers);
+        });
+    }
+
+    private String resolveAllowOrigin(final ServerRequest request) {
+        final String origin = request.headers().firstHeader("Origin");
+        return Objects.nonNull(origin) && !origin.isBlank() ? origin : "*";
+    }
+
+    private String resolveAllowHeaders(final ServerRequest request) {
+        final Set<String> configuredHeaders = 
toHeaderSet(resolveConfiguredAllowHeaders());
+        final String requestedHeaders = 
request.headers().firstHeader("Access-Control-Request-Headers");
+        if (configuredHeaders.contains("*")) {
+            if (Objects.nonNull(requestedHeaders) && 
!requestedHeaders.isBlank()) {
+                return String.join(", ", toHeaderSet(requestedHeaders));
+            }
+            return "*";
+        }
+        if (Objects.isNull(requestedHeaders) || requestedHeaders.isBlank()) {
+            return String.join(", ", configuredHeaders);
+        }
+        final Set<String> configuredLowercaseHeaders = new LinkedHashSet<>();
+        for (String header : configuredHeaders) {
+            configuredLowercaseHeaders.add(header.toLowerCase(Locale.ROOT));
+        }
+        final Set<String> effectiveHeaders = new LinkedHashSet<>();
+        for (String requestedHeader : requestedHeaders.split(",")) {
+            final String header = requestedHeader.trim();
+            if (!header.isEmpty() && 
configuredLowercaseHeaders.contains(header.toLowerCase(Locale.ROOT))) {
+                effectiveHeaders.add(header);
+            }
+        }
+        return effectiveHeaders.isEmpty()
+                ? String.join(", ", configuredHeaders)
+                : String.join(", ", effectiveHeaders);
+    }
+
+    private String resolveConfiguredAllowHeaders() {
+        return Objects.nonNull(configuredCorsAllowHeaders) && 
!configuredCorsAllowHeaders.isBlank()
+                ? configuredCorsAllowHeaders : CORS_FALLBACK_ALLOW_HEADERS;
+    }
+
+    private Set<String> toHeaderSet(final String headers) {
+        final Set<String> headerSet = new LinkedHashSet<>();
+        for (String header : headers.split(",")) {
+            final String trimmed = header.trim();
+            if (!trimmed.isEmpty()) {
+                headerSet.add(trimmed);
+            }
+        }
+        return headerSet;
+    }
+
+    private void mergeVaryHeaders(final HttpHeaders headers) {
+        final Set<String> varyValues = new LinkedHashSet<>();
+        for (String varyHeader : headers.getOrEmpty(HttpHeaders.VARY)) {
+            for (String varyValue : varyHeader.split(",")) {
+                final String trimmed = varyValue.trim();
+                if (!trimmed.isEmpty()) {
+                    varyValues.add(trimmed);
+                }
+            }
+        }
+        varyValues.add("Origin");
+        varyValues.add("Access-Control-Request-Headers");
+        headers.setVary(List.copyOf(varyValues));
+    }
+
     /**
      * Extracts the session ID from the request headers or query parameters.
      *
diff --git 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/StreamableHttpProviderBuilder.java
 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/StreamableHttpProviderBuilder.java
index 749359f80f..8ca200556d 100644
--- 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/StreamableHttpProviderBuilder.java
+++ 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/StreamableHttpProviderBuilder.java
@@ -42,6 +42,8 @@ public class StreamableHttpProviderBuilder {
 
     private String endpoint = DEFAULT_ENDPOINT;
 
+    private String allowedHeaders;
+
     /**
      * Sets the ObjectMapper for JSON serialization/deserialization.
      *
@@ -68,6 +70,17 @@ public class StreamableHttpProviderBuilder {
         return this;
     }
 
+    /**
+     * Sets configured CORS allow headers.
+     *
+     * @param allowedHeaders comma-separated headers from configuration
+     * @return this builder for method chaining
+     */
+    public StreamableHttpProviderBuilder allowedHeaders(final String 
allowedHeaders) {
+        this.allowedHeaders = allowedHeaders;
+        return this;
+    }
+
     /**
      * Builds a new ShenyuStreamableHttpServerTransportProvider instance.
      *
@@ -76,6 +89,6 @@ public class StreamableHttpProviderBuilder {
      */
     public ShenyuStreamableHttpServerTransportProvider build() {
         Assert.notNull(objectMapper, "ObjectMapper must be configured");
-        return new ShenyuStreamableHttpServerTransportProvider(objectMapper, 
endpoint);
+        return new ShenyuStreamableHttpServerTransportProvider(objectMapper, 
endpoint, allowedHeaders);
     }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/McpServerPluginTest.java
 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/McpServerPluginTest.java
index 52fbc89ce4..ea27e72bda 100644
--- 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/McpServerPluginTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/McpServerPluginTest.java
@@ -30,14 +30,19 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.codec.HttpMessageReader;
 import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.reactive.function.server.HandlerStrategies;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
 import java.net.URI;
 import java.util.List;
+import java.util.Locale;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -150,4 +155,51 @@ class McpServerPluginTest {
         String rawPath = mcpServerPlugin.getRawPath(exchange);
         assertEquals("/test/path", rawPath);
     }
+
+    @Test
+    void testPreflightWithConfiguredAllowHeaders() {
+        final McpServerPlugin plugin = new 
McpServerPlugin(shenyuMcpServerManager,
+                HandlerStrategies.withDefaults().messageReaders(), 
"Content-Type, XRequest, Authorization");
+        final MockServerWebExchange webExchange = 
MockServerWebExchange.from(MockServerHttpRequest
+                .options("/mcp/streamablehttp")
+                .header("Origin", "http://localhost:6274";)
+                .header("Access-Control-Request-Headers", "xrequest, 
authorization")
+                .build());
+        webExchange.getAttributes().put(Constants.CONTEXT, new 
ShenyuContext());
+        
webExchange.getResponse().getHeaders().setVary(List.of("Accept-Encoding"));
+        
when(shenyuMcpServerManager.canRoute("/mcp/streamablehttp")).thenReturn(true);
+
+        StepVerifier.create(plugin.doExecute(webExchange, chain, selector, 
rule))
+                .verifyComplete();
+
+        assertEquals(HttpStatus.OK, webExchange.getResponse().getStatusCode());
+        assertEquals("http://localhost:6274";,
+                
webExchange.getResponse().getHeaders().getFirst("Access-Control-Allow-Origin"));
+        assertEquals("POST, OPTIONS",
+                
webExchange.getResponse().getHeaders().getFirst("Access-Control-Allow-Methods"));
+        assertEquals("Content-Type, XRequest, Authorization",
+                
webExchange.getResponse().getHeaders().getFirst("Access-Control-Allow-Headers"));
+        
assertTrue(webExchange.getResponse().getHeaders().getVary().contains("Accept-Encoding"));
+        
assertTrue(webExchange.getResponse().getHeaders().getVary().contains("Origin"));
+        
assertTrue(webExchange.getResponse().getHeaders().getVary().contains("Access-Control-Request-Headers"));
+    }
+
+    @Test
+    void testPreflightWithFallbackAllowHeaders() {
+        final McpServerPlugin plugin = new 
McpServerPlugin(shenyuMcpServerManager,
+                HandlerStrategies.withDefaults().messageReaders());
+        final MockServerWebExchange webExchange = 
MockServerWebExchange.from(MockServerHttpRequest
+                .options("/mcp/streamablehttp")
+                .header("Origin", "http://localhost:6274";)
+                .header("Access-Control-Request-Headers", "xrequest")
+                .build());
+        webExchange.getAttributes().put(Constants.CONTEXT, new 
ShenyuContext());
+        
when(shenyuMcpServerManager.canRoute("/mcp/streamablehttp")).thenReturn(true);
+
+        StepVerifier.create(plugin.doExecute(webExchange, chain, selector, 
rule))
+                .verifyComplete();
+
+        final String allowHeaders = 
webExchange.getResponse().getHeaders().getFirst("Access-Control-Allow-Headers");
+        assertTrue(allowHeaders.toLowerCase(Locale.ROOT).contains("xrequest"));
+    }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
new file mode 100644
index 0000000000..d10af3ccb1
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.plugin.mcp.server.transport;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.reactive.function.server.HandlerStrategies;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.test.StepVerifier;
+
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test case for {@link ShenyuStreamableHttpServerTransportProvider}.
+ */
+class ShenyuStreamableHttpServerTransportProviderTest {
+
+    @Test
+    void testPreflightUsesConfiguredHeadersAndMethods() {
+        ShenyuStreamableHttpServerTransportProvider provider =
+                new ShenyuStreamableHttpServerTransportProvider(new 
ObjectMapper(),
+                        "/mcp/streamablehttp", "Content-Type, XRequest");
+        ServerRequest request = 
createRequest(MockServerHttpRequest.options("/mcp/streamablehttp")
+                .header("Origin", "http://localhost:6274";)
+                .header("Access-Control-Request-Headers", "xrequest, 
authorization")
+                .build());
+
+        StepVerifier.create(provider.handleUnifiedEndpoint(request))
+                .assertNext(response -> {
+                    assertEquals(HttpStatus.OK, response.statusCode());
+                    assertEquals("POST, OPTIONS",
+                            
response.headers().getFirst("Access-Control-Allow-Methods"));
+                    assertEquals("xrequest",
+                            
response.headers().getFirst("Access-Control-Allow-Headers"));
+                    
assertTrue(response.headers().getVary().contains("Origin"));
+                    
assertTrue(response.headers().getVary().contains("Access-Control-Request-Headers"));
+                })
+                .verifyComplete();
+    }
+
+    @Test
+    void testPreflightUsesFallbackHeaders() {
+        ShenyuStreamableHttpServerTransportProvider provider =
+                new ShenyuStreamableHttpServerTransportProvider(new 
ObjectMapper(),
+                        "/mcp/streamablehttp");
+        ServerRequest request = 
createRequest(MockServerHttpRequest.options("/mcp/streamablehttp")
+                .header("Origin", "http://localhost:6274";)
+                .header("Access-Control-Request-Headers", "xrequest")
+                .build());
+
+        ServerResponse response = 
provider.handleUnifiedEndpoint(request).block();
+        assertNotNull(response);
+        assertEquals(HttpStatus.OK, response.statusCode());
+        String allowHeaders = 
response.headers().getFirst("Access-Control-Allow-Headers");
+        assertTrue(allowHeaders.toLowerCase(Locale.ROOT).contains("xrequest"));
+    }
+
+    private ServerRequest createRequest(final MockServerHttpRequest request) {
+        return ServerRequest.create(
+                MockServerWebExchange.from(request),
+                HandlerStrategies.withDefaults().messageReaders()
+        );
+    }
+}
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-mcp-server/src/main/java/org/apache/shenyu/springboot/starter/plugin/mcp/server/McpServerPluginConfiguration.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-mcp-server/src/main/java/org/apache/shenyu/springboot/starter/plugin/mcp/server/McpServerPluginConfiguration.java
index 195be3881e..3d92cfb2d3 100644
--- 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-mcp-server/src/main/java/org/apache/shenyu/springboot/starter/plugin/mcp/server/McpServerPluginConfiguration.java
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-mcp-server/src/main/java/org/apache/shenyu/springboot/starter/plugin/mcp/server/McpServerPluginConfiguration.java
@@ -17,6 +17,7 @@
 
 package org.apache.shenyu.springboot.starter.plugin.mcp.server;
 
+import org.apache.shenyu.common.config.ShenyuConfig;
 import org.apache.shenyu.plugin.api.ShenyuPlugin;
 import org.apache.shenyu.plugin.base.handler.PluginDataHandler;
 import org.apache.shenyu.plugin.mcp.server.McpServerPlugin;
@@ -28,6 +29,8 @@ import 
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.http.codec.ServerCodecConfigurer;
 
+import java.util.Objects;
+
 /**
  * The type Mock plugin configuration.
  */
@@ -55,9 +58,15 @@ public class McpServerPluginConfiguration {
 //        return RouterFunctions.route(RequestPredicates.all(), 
shenyuMcpServerManager::dispatch);
 //    }
     
+    /**
+     * Shenyu mcp server manager.
+     *
+     * @param shenyuConfig the shenyu config
+     * @return the shenyu mcp server manager
+     */
     @Bean
-    public ShenyuMcpServerManager shenyuMcpServerManager() {
-        return new ShenyuMcpServerManager();
+    public ShenyuMcpServerManager shenyuMcpServerManager(final ShenyuConfig 
shenyuConfig) {
+        return new 
ShenyuMcpServerManager(resolveCorsAllowedHeaders(shenyuConfig));
     }
 
     /**
@@ -77,11 +86,18 @@ public class McpServerPluginConfiguration {
      *
      * @param shenyuMcpServerManager the shenyu mcp server manager
      * @param configurer the server codec configurer
+     * @param shenyuConfig the shenyu config
      * @return the shenyu plugin
      */
     @Bean
     public ShenyuPlugin mcpServerPlugin(final ShenyuMcpServerManager 
shenyuMcpServerManager,
-                                        final ServerCodecConfigurer 
configurer) {
-        return new McpServerPlugin(shenyuMcpServerManager, 
configurer.getReaders());
+                                        final ServerCodecConfigurer configurer,
+                                        final ShenyuConfig shenyuConfig) {
+        return new McpServerPlugin(shenyuMcpServerManager, 
configurer.getReaders(), resolveCorsAllowedHeaders(shenyuConfig));
+    }
+
+    private String resolveCorsAllowedHeaders(final ShenyuConfig shenyuConfig) {
+        return Objects.nonNull(shenyuConfig) && 
Objects.nonNull(shenyuConfig.getCross())
+                ? shenyuConfig.getCross().getAllowedHeaders() : null;
     }
 }


Reply via email to