feat: add CORS support with configurable allowed headers in MCP server#6295
feat: add CORS support with configurable allowed headers in MCP server#6295
Conversation
There was a problem hiding this comment.
Pull request overview
Adds CORS handling to the MCP server plugin/transport so cross-origin clients can call MCP endpoints with configurable Access-Control-Allow-Headers sourced from shenyu.cross.allowedHeaders.
Changes:
- Wires
shenyu.cross.allowedHeadersfrom Spring BootShenyuConfiginto the MCP plugin and MCP server manager. - Extends the Streamable HTTP transport provider/builder to accept configured allowed headers and applies CORS headers to responses (including OPTIONS preflight).
- Updates MCP plugin request handling to respond to OPTIONS preflight and to emit CORS headers using configurable allowed headers.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| shenyu-spring-boot-starter-plugin-mcp-server/McpServerPluginConfiguration.java | Injects ShenyuConfig and passes configured CORS allowed headers into MCP manager/plugin beans. |
| shenyu-plugin-mcp-server/transport/StreamableHttpProviderBuilder.java | Adds allowedHeaders to builder and passes it into the transport provider constructor. |
| shenyu-plugin-mcp-server/transport/ShenyuStreamableHttpServerTransportProvider.java | Adds configurable CORS allow-headers and centralizes CORS header application for Streamable HTTP responses. |
| shenyu-plugin-mcp-server/manager/ShenyuMcpServerManager.java | Stores configured CORS allowed headers and passes them into Streamable HTTP transport creation. |
| shenyu-plugin-mcp-server/McpServerPlugin.java | Adds OPTIONS preflight handling and resolves CORS headers (origin/allowed headers/methods) per request. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...nyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/McpServerPlugin.java
Outdated
Show resolved
Hide resolved
| 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"); |
There was a problem hiding this comment.
set("Vary", ...) overwrites any existing Vary header that may already be set by the framework (e.g., Accept-Encoding), which can break caching semantics. Prefer adding/merging the Vary values instead of replacing the header entirely.
| exchange.getResponse().getHeaders().set("Vary", "Origin, Access-Control-Request-Headers"); | |
| // Merge CORS-related Vary values with any existing Vary header entries | |
| final List<String> existingVary = exchange.getResponse().getHeaders().getVary(); | |
| final Set<String> varyValues = new LinkedHashSet<>(existingVary); | |
| varyValues.add("Origin"); | |
| varyValues.add("Access-Control-Request-Headers"); | |
| exchange.getResponse().getHeaders().setVary(new java.util.ArrayList<>(varyValues)); |
| 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(); |
There was a problem hiding this comment.
This transport returns 405 for GET, but the CORS preflight response advertises Access-Control-Allow-Methods: GET, POST, OPTIONS. Align the advertised methods with what the endpoint actually supports (likely POST, OPTIONS) to avoid misleading clients and incorrect CORS behavior.
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
This logic unions the configured allow-list with all client-supplied Access-Control-Request-Headers, which effectively allows any requested header and makes the configuration non-enforcing. Consider returning only the configured allow-list (or intersecting configured vs requested) so disallowed requested headers fail preflight.
| 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); | |
| } | |
| } | |
| final String trimmed = header.trim(); | |
| if (!trimmed.isEmpty()) { | |
| allowedHeaders.add(trimmed); | |
| } | |
| } | |
| final String requestedHeaders = request.headers().firstHeader("Access-Control-Request-Headers"); | |
| if (Objects.nonNull(requestedHeaders) && !requestedHeaders.isBlank()) { | |
| final Set<String> effectiveHeaders = new LinkedHashSet<>(); | |
| for (String requestedHeader : requestedHeaders.split(",")) { | |
| final String header = requestedHeader.trim(); | |
| if (!header.isEmpty() && allowedHeaders.contains(header)) { | |
| effectiveHeaders.add(header); | |
| } | |
| } | |
| if (!effectiveHeaders.isEmpty()) { | |
| return String.join(", ", effectiveHeaders); | |
| } |
| 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(); |
There was a problem hiding this comment.
CORS behavior was added/changed (preflight handling + configurable allowed headers), but there are no corresponding tests covering OPTIONS requests and the resulting Access-Control-* headers. Please add unit/integration tests that assert the preflight response headers for both configured and default allowed-headers scenarios.
…e/shenyu/plugin/mcp/server/McpServerPlugin.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
add CORS support with configurable allowed headers in MCP server
Make sure that:
./mvnw clean install -Dmaven.javadoc.skip=true.