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

liuhongyu pushed a commit to branch feat/mcp_server_support_header
in repository https://gitbox.apache.org/repos/asf/shenyu.git

commit 4e6c5ff35260c79f32836f24df6b77f1bc9213ae
Author: liuhy <[email protected]>
AuthorDate: Tue Feb 10 10:19:04 2026 +0800

    feat: add CORS support with configurable allowed headers in MCP server
---
 .../shenyu/plugin/mcp/server/McpServerPlugin.java  | 73 +++++++++++++++++++--
 .../mcp/server/manager/ShenyuMcpServerManager.java | 28 +++++++-
 ...henyuStreamableHttpServerTransportProvider.java | 76 ++++++++++++++++++----
 .../transport/StreamableHttpProviderBuilder.java   | 15 ++++-
 .../mcp/server/McpServerPluginConfiguration.java   | 24 +++++--
 5 files changed, 191 insertions(+), 25 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..60268d34f4 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
@@ -43,9 +43,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 +111,17 @@ 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_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 +130,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 +226,10 @@ public class McpServerPlugin extends AbstractShenyuPlugin {
                                        final SelectorData selector,
                                        final String uri) {
 
+        if 
("OPTIONS".equalsIgnoreCase(exchange.getRequest().getMethod().name())) {
+            return handleCorsPreflight(exchange);
+        }
+
         if (isStreamableHttpProtocol(uri)) {
             return handleStreamableHttpRequest(exchange, chain, request, uri);
         } else if (isSseProtocol(uri)) {
@@ -276,6 +303,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) {
+        exchange.getResponse().setStatusCode(HttpStatus.OK);
+        setCorsHeaders(exchange);
+        exchange.getResponse().getHeaders().set("Access-Control-Max-Age", 
"3600");
+        return exchange.getResponse().setComplete();
+    }
+
     /**
      * Handles Streamable HTTP MCP requests with unified endpoint processing.
      *
@@ -579,11 +619,34 @@ 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");
+        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", 
CORS_ALLOW_METHODS);
+        exchange.getResponse().getHeaders().set("Vary", "Origin, 
Access-Control-Request-Headers");
+    }
+
+    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(",")) {
+            allowedHeaders.add(header.trim());
+        }
+        final String requestedHeaders = 
exchange.getRequest().getHeaders().getFirst("Access-Control-Request-Headers");
+        if (Objects.nonNull(requestedHeaders) && !requestedHeaders.isBlank()) {
+            for (String requestedHeader : requestedHeaders.split(",")) {
+                final String header = requestedHeader.trim();
+                if (!header.isEmpty()) {
+                    allowedHeaders.add(header);
+                }
+            }
+        }
+        return String.join(", ", allowedHeaders);
     }
 
     /**
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..23f1ac99b6 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
@@ -39,8 +39,10 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import java.io.IOException;
+import java.util.LinkedHashSet;
 import java.util.Objects;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -92,10 +94,19 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
 
     private static final String SERVER_VERSION = "1.0.0";
 
+    private static final String CORS_ALLOW_METHODS = "GET, POST, OPTIONS";
+
+    private static final String CORS_ALLOW_POST_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 +134,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);
     }
 
@@ -195,18 +221,12 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
         }
         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())) {
             // 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_POST_METHODS)
                     .header("Allow", "POST, OPTIONS")
                     .contentType(MediaType.APPLICATION_JSON)
                     .bodyValue(new java.util.HashMap<String, Object>() {{
@@ -219,10 +239,8 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
             // 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 +661,40 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
         return response;
     }
 
+    private ServerResponse.BodyBuilder applyCorsHeaders(final ServerRequest 
request,
+                                                        final 
ServerResponse.BodyBuilder builder,
+                                                        final String 
allowMethods) {
+        return builder
+                .header("Access-Control-Allow-Origin", 
resolveAllowOrigin(request))
+                .header("Access-Control-Allow-Headers", 
resolveAllowHeaders(request))
+                .header("Access-Control-Allow-Methods", allowMethods)
+                .header("Vary", "Origin, Access-Control-Request-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> allowedHeaders = new LinkedHashSet<>();
+        final String allowHeaders = 
Objects.nonNull(configuredCorsAllowHeaders) && 
!configuredCorsAllowHeaders.isBlank()
+                ? configuredCorsAllowHeaders : CORS_FALLBACK_ALLOW_HEADERS;
+        for (String header : allowHeaders.split(",")) {
+            allowedHeaders.add(header.trim());
+        }
+        final String requestedHeaders = 
request.headers().firstHeader("Access-Control-Request-Headers");
+        if (Objects.nonNull(requestedHeaders) && !requestedHeaders.isBlank()) {
+            for (String requestedHeader : requestedHeaders.split(",")) {
+                final String header = requestedHeader.trim();
+                if (!header.isEmpty()) {
+                    allowedHeaders.add(header);
+                }
+            }
+        }
+        return String.join(", ", allowedHeaders);
+    }
+
     /**
      * 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-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