This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 12aa5f9a30f1 CAMEL-26665: camel-mcp-server - optional allow-list for
the Host header on /mcp (Camel Main engine) (#29752)
12aa5f9a30f1 is described below
commit 12aa5f9a30f1c5bb9d9bc49a7c16ff5915e6cf29
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 21:54:16 2026 +0100
CAMEL-26665: camel-mcp-server - optional allow-list for the Host header on
/mcp (Camel Main engine) (#29752)
Follow-up to CAMEL-26359 (#29284), which added the Origin check on /mcp for
the Camel Main engine. This adds the optional Host check the MCP Java SDK
validator also offers.
New option camel.server.mcp-allowed-hosts, a comma-separated list of hosts.
Not set (the default), the Host header is not checked, as before. Set, a
request on /mcp (POST, the SSE GET and DELETE) whose Host is missing or not in
the list gets HTTP 421 with a JSON-RPC error, in the same handler as the Origin
check, before the body is read and before any session is created. This protects
a server on a private address against DNS rebinding. Hosts match ignoring case;
host:* matches any po [...]
Claude Code on behalf of davsclaus.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/main.adoc | 3 +-
.../org/apache/camel/catalog/docs/mcp-server.adoc | 25 +-
.../main/camel-main-configuration-metadata.json | 1 +
.../component/mcp/server/McpServerBridge.java | 8 +-
.../mcp/server/McpServerConfiguration.java | 20 ++
.../camel/component/mcp/server/McpServerHosts.java | 132 ++++++++
.../camel/component/mcp/server/McpServerInfo.java | 46 ++-
.../component/mcp/server/McpServerBridgeTest.java | 41 +++
.../component/mcp/server/McpServerHostsTest.java | 91 ++++++
.../camel-mcp-server/src/main/docs/mcp-server.adoc | 25 +-
.../mcp/server/main/DefaultMcpServerFactory.java | 1 +
.../mcp/server/vertx/VertxMcpServerEngine.java | 6 +-
.../VertxMcpStreamableServerTransportProvider.java | 84 +++++-
.../server/main/McpServerMainPropertiesTest.java | 43 ++-
.../mcp/server/vertx/VertxMcpHostTest.java | 331 +++++++++++++++++++++
...ttpServerConfigurationPropertiesConfigurer.java | 7 +
.../camel-main-configuration-metadata.json | 1 +
core/camel-main/src/main/docs/main.adoc | 3 +-
.../main/HttpServerConfigurationProperties.java | 28 ++
19 files changed, 878 insertions(+), 18 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
index 80c685a6c580..aa3a9ba9a290 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
@@ -197,7 +197,7 @@ The camel.routecontroller supports 12 options, which are
listed below.
=== Camel Embedded HTTP Server (only for standalone; not Spring Boot or
Quarkus) configurations
-The camel.server supports 40 options, which are listed below.
+The camel.server supports 41 options, which are listed below.
[width="100%",cols="2,5,^1,2",options="header"]
|===
@@ -217,6 +217,7 @@ The camel.server supports 40 options, which are listed
below.
| `camel.server.jwtKeystorePath` | Path to the keystore file used for JWT
tokens validation. | | String
| `camel.server.jwtKeystoreType` | Type of the keystore used for JWT tokens
validation (jks, pkcs12, etc.). | | String
| `camel.server.maxBodySize` | Maximum HTTP body size the embedded HTTP server
can accept. | | Long
+| `camel.server.mcpAllowedHosts` | Comma-separated list of hosts allowed in
the Host header of a request to the MCP endpoint on the Vert.x streamable
transport, such as mcp.example.com or localhost:*. Each entry is a host, or a
host and a port, where the port may be an asterisk to match any port (or none).
The host is compared ignoring case. When set, a request whose Host header is
missing or not in the list is rejected with HTTP 421 (Misdirected Request),
before its body is read and bef [...]
| `camel.server.mcpAllowedOrigins` | Comma-separated list of origins allowed
to call the MCP endpoint on the Vert.x streamable transport. Each entry is an
exact origin (scheme, host and port), or ends with a colon and an asterisk to
match any port. Scheme and host are compared ignoring case, and a default port
(80 for http, 443 for https) may be left out. A wildcard for all origins is not
supported: an entry that is not of the form scheme://host or scheme://host:port
fails the startup. A [...]
| `camel.server.mcpEnabled` | Whether to expose ai-tool routes as MCP tools,
ai-resource routes as MCP resources and ai-prompt routes as MCP prompts.
Requires camel-mcp-server on the classpath. Use setMcpTransport(String) to
choose streamable HTTP (default) or stdio for IDE subprocess integration. By
default, the MCP server is not enabled. | false | boolean
| `camel.server.mcpInstructions` | Top-level MCP instructions returned to
clients on initialize. | | String
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
index cdf1ca7600b1..5767742c4f41 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
@@ -522,12 +522,35 @@ path such as `+https://app.example.com/mcp+`, an entry
without a scheme such as
A refused request is logged at `DEBUG` level by the
`org.apache.camel.component.mcp.server.vertx.VertxMcpStreamableServerTransportProvider`
-logger, with the `Origin` it carried.
+logger, with the `Origin` (or the `Host`) it carried.
The check is independent of the platform-http CORS configuration: CORS only
adds response headers for the browser, while the origin check refuses the
request on the server.
+The `Host` header can be checked as well, with
+`camel.server.mcp-allowed-hosts`. It is not set by default, and then the `Host`
+header is not checked. When set, a request whose `Host` header is missing or
not
+in the list gets HTTP 421 (Misdirected Request) with a JSON-RPC error, before
+its body is read and before any session is created. This protects a server on a
+private address against DNS rebinding: a hostile web page whose own host name
+resolves to the address of the server sends its own host name in the `Host`
+header, not one from the list.
+
+[source,properties]
+----
+camel.server.mcp-allowed-hosts = mcp.example.com, localhost:*, 127.0.0.1:*
+----
+
+Each entry is a host, or a host and a port, without a scheme. A port of `+*+`
+matches any port, and also a `Host` header without a port (the default port of
+the scheme), so `localhost:*` allows `localhost`, `localhost:8080` and
+`localhost:9876`. An entry without a port matches only a `Host` header without
+a port: `mcp.example.com` is the host name a client uses on the default port,
+behind a reverse proxy for example. The host is compared ignoring case. Write
an
+IPv6 address in brackets, such as `[::1]:*`. An entry with a scheme or a path,
+and `+*+`, fail the startup with a message saying why.
+
The main HTTP server listens on all network interfaces by default
(`camel.server.host = 0.0.0.0`). When the MCP server is only meant for clients
on the same machine, bind it to the loopback interface:
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
index 0db48e268c8d..a168e12aa0d9 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
@@ -463,6 +463,7 @@
{ "name": "camel.server.jwtKeystorePath", "required": false,
"description": "Path to the keystore file used for JWT tokens validation.",
"sourceType": "org.apache.camel.main.HttpServerConfigurationProperties",
"type": "string", "javaType": "java.lang.String", "secret": false },
{ "name": "camel.server.jwtKeystoreType", "required": false,
"description": "Type of the keystore used for JWT tokens validation (jks,
pkcs12, etc.).", "sourceType":
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "string",
"javaType": "java.lang.String", "secret": false },
{ "name": "camel.server.maxBodySize", "required": false, "description":
"Maximum HTTP body size the embedded HTTP server can accept.", "sourceType":
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "integer",
"javaType": "java.lang.Long", "secret": false },
+ { "name": "camel.server.mcpAllowedHosts", "required": false,
"description": "Comma-separated list of hosts allowed in the Host header of a
request to the MCP endpoint on the Vert.x streamable transport, such as
mcp.example.com or localhost:*. Each entry is a host, or a host and a port,
where the port may be an asterisk to match any port (or none). The host is
compared ignoring case. When set, a request whose Host header is missing or not
in the list is rejected with HTTP 421 (Misdire [...]
{ "name": "camel.server.mcpAllowedOrigins", "required": false,
"description": "Comma-separated list of origins allowed to call the MCP
endpoint on the Vert.x streamable transport. Each entry is an exact origin
(scheme, host and port), or ends with a colon and an asterisk to match any
port. Scheme and host are compared ignoring case, and a default port (80 for
http, 443 for https) may be left out. A wildcard for all origins is not
supported: an entry that is not of the form scheme:\/\ [...]
{ "name": "camel.server.mcpEnabled", "required": false, "description":
"Whether to expose ai-tool routes as MCP tools, ai-resource routes as MCP
resources and ai-prompt routes as MCP prompts. Requires camel-mcp-server on the
classpath. Use setMcpTransport(String) to choose streamable HTTP (default) or
stdio for IDE subprocess integration. By default, the MCP server is not
enabled.", "sourceType":
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "boolean",
"javaType" [...]
{ "name": "camel.server.mcpInstructions", "required": false,
"description": "Top-level MCP instructions returned to clients on initialize.",
"sourceType": "org.apache.camel.main.HttpServerConfigurationProperties",
"type": "string", "javaType": "java.lang.String", "secret": false },
diff --git
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
index d1facd6d9923..e3eab52bbe58 100644
---
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
+++
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
@@ -158,7 +158,8 @@ public class McpServerBridge extends ServiceSupport
implements CamelContextAware
configuration.getServerIcons(),
McpServerOrigins.parseAllowedOrigins(configuration.getAllowedOrigins()),
configuration.getOauthProfile(),
configuration.getRequiredScopes(),
- configuration.getResourceUri()));
+ configuration.getResourceUri(),
+
McpServerHosts.parseAllowedHosts(configuration.getAllowedHosts())));
if (!engine.consumesServingConfiguration()) {
if
(!McpServerConstants.DEFAULT_PATH.equals(configuration.getPath())) {
@@ -171,6 +172,11 @@ public class McpServerBridge extends ServiceSupport
implements CamelContextAware
+ "configuration decides which origins are allowed",
engine.getClass().getSimpleName());
}
+ if (isSet(configuration.getAllowedHosts())) {
+ LOG.warn("The MCP allowed hosts option is ignored by engine
{}: the runtime's native MCP server "
+ + "configuration decides which hosts are allowed",
+ engine.getClass().getSimpleName());
+ }
if (hasServerMetadataConfiguration()) {
LOG.warn("The MCP server metadata options may be ignored by
engine {}: the runtime's native MCP "
+ "server configuration decides the server identity
and initialize metadata",
diff --git
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
index ee8c293b8d3d..50477086a1ad 100644
---
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
+++
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
@@ -45,6 +45,7 @@ public class McpServerConfiguration {
private long sessionKeepAliveInterval =
McpServerConstants.DEFAULT_SESSION_KEEP_ALIVE_INTERVAL;
private long sessionIdleTtl = McpServerConstants.DEFAULT_SESSION_IDLE_TTL;
private String allowedOrigins;
+ private String allowedHosts;
private String oauthProfile;
private String requiredScopes;
private String resourceUri;
@@ -223,6 +224,25 @@ public class McpServerConfiguration {
this.allowedOrigins = allowedOrigins;
}
+ /**
+ * Comma-separated list of hosts allowed in the {@code Host} header of a
request to the MCP endpoint, such as
+ * {@code mcp.example.com} or {@code localhost:*} (any port, or none).
When set, a request whose {@code Host} header
+ * is missing or not in the list is rejected with HTTP 421 (Misdirected
Request), before its body is read and before
+ * any session is created; this protects a server bound to a private
address against DNS rebinding. The host is
+ * compared ignoring case. An entry that is not of the form {@code
host[:port]}, such as {@code *} or an entry with
+ * a scheme, fails the startup (see {@link McpServerHosts}). When not set,
the {@code Host} header is not checked.
+ * Engine-owned: ignored by native engines.
+ *
+ * @since 4.24
+ */
+ public String getAllowedHosts() {
+ return allowedHosts;
+ }
+
+ public void setAllowedHosts(String allowedHosts) {
+ this.allowedHosts = allowedHosts;
+ }
+
/**
* Name of the camel-oauth profile used to authenticate OAuth bearer
tokens. Supported by the Camel Main HTTP MCP
* engine; other engines reject this option unless they explicitly
implement OAuth support.
diff --git
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerHosts.java
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerHosts.java
new file mode 100644
index 000000000000..3e313ef47239
--- /dev/null
+++
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerHosts.java
@@ -0,0 +1,132 @@
+/*
+ * 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.camel.component.mcp.server;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * Parses and normalizes the hosts allowed in the {@code Host} header of a
request to the MCP endpoint, and the
+ * {@code Host} header itself, so that both are compared in the same form: the
host in lower case and the port without
+ * leading zeros.
+ * <p>
+ * An allowed host is {@code host} or {@code host:port}, where the port may be
{@code *} to match any port, and also a
+ * {@code Host} header without a port. An IPv6 address is written in brackets,
such as {@code [::1]:*}. Anything else,
+ * such as {@code *}, an entry with a scheme or a path, is refused with an
{@link IllegalArgumentException}, so a
+ * mistake shows at startup instead of as an error for every request.
+ *
+ * @since 4.24
+ */
+public final class McpServerHosts {
+
+ private static final String ANY_PORT = "*";
+ private static final Pattern HOST =
Pattern.compile("[a-zA-Z0-9._-]+|\\[[0-9a-fA-F:.]+]");
+ private static final Pattern PORT = Pattern.compile("\\d{1,5}");
+
+ private McpServerHosts() {
+ }
+
+ /**
+ * Splits a comma-separated list of allowed hosts, skipping empty entries,
and normalizes each entry with
+ * {@link #normalizeAllowedHost(String)}.
+ *
+ * @param allowedHosts the comma-separated list
+ * @return the normalized hosts, or {@code null}
when not set or without any entry, so the
+ * {@code Host} header is not checked
+ * @throws IllegalArgumentException when an entry is not a valid host
+ */
+ public static List<String> parseAllowedHosts(String allowedHosts) {
+ if (allowedHosts == null || allowedHosts.isBlank()) {
+ return null;
+ }
+ List<String> answer = new ArrayList<>();
+ for (String entry : allowedHosts.split(",")) {
+ if (!entry.isBlank()) {
+ answer.add(normalizeAllowedHost(entry));
+ }
+ }
+ return answer.isEmpty() ? null : answer;
+ }
+
+ /**
+ * Normalizes an allowed host: trims it, lower-cases the host and drops
the leading zeros of the port.
+ *
+ * @param allowedHost a host such as {@code
MCP.Example.com}, {@code localhost:8080} or
+ * {@code localhost:*}
+ * @return the normalized host, such as {@code
mcp.example.com}
+ * @throws IllegalArgumentException when the entry is {@code *} or not of
the form {@code host[:port]}
+ */
+ public static String normalizeAllowedHost(String allowedHost) {
+ String value = allowedHost == null ? "" : allowedHost.trim();
+ if (ANY_PORT.equals(value)) {
+ throw new IllegalArgumentException(
+ "MCP allowed host * is not supported: leave the allowed
hosts option unset to not check the Host"
+ + " header, or list each host,
such as mcp.example.com or localhost:* (any port).");
+ }
+ String answer = normalize(value, true);
+ if (answer == null) {
+ throw new IllegalArgumentException(
+ "Invalid MCP allowed host '" + allowedHost + "': expected
host or host:port, without a scheme,"
+ + " where the port may be * for
any port, such as mcp.example.com or"
+ + " localhost:*");
+ }
+ return answer;
+ }
+
+ /**
+ * Normalizes the value of a {@code Host} header the same way as the
allowed hosts, so that both compare as strings.
+ *
+ * @param host the header value
+ * @return the normalized host, or the value as is when it is not of
the form {@code host[:port]} (such as
+ * {@code null}), so it matches no allowed host
+ */
+ public static String normalizeHost(String host) {
+ if (host == null) {
+ return null;
+ }
+ String answer = normalize(host.trim(), false);
+ return answer != null ? answer : host;
+ }
+
+ private static String normalize(String value, boolean allowAnyPort) {
+ String host = value;
+ String port = null;
+ int colon = value.lastIndexOf(':');
+ if (colon >= 0 && colon > value.lastIndexOf(']')) {
+ host = value.substring(0, colon);
+ port = value.substring(colon + 1);
+ }
+ if (!HOST.matcher(host).matches()) {
+ return null;
+ }
+ host = host.toLowerCase(Locale.ROOT);
+ if (port != null) {
+ if (ANY_PORT.equals(port)) {
+ if (!allowAnyPort) {
+ return null;
+ }
+ } else if (PORT.matcher(port).matches() && Integer.parseInt(port)
<= 65535) {
+ port = String.valueOf(Integer.parseInt(port));
+ } else {
+ return null;
+ }
+ }
+ return port != null ? host + ":" + port : host;
+ }
+}
diff --git
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerInfo.java
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerInfo.java
index b06b81f7af6b..fc40fba1e4b7 100644
---
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerInfo.java
+++
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerInfo.java
@@ -44,6 +44,9 @@ import java.util.List;
* disabled
* @param requiredScopes comma-separated OAuth scopes required by
the MCP HTTP endpoint
* @param resourceUri explicit public HTTPS URI of the
protected MCP resource for RFC 9728 metadata
+ * @param allowedHosts hosts allowed in the {@code Host} header
of a request to the MCP endpoint (exact,
+ * or {@code host:*} for any port or none);
{@code null} does not check the
+ * {@code Host} header. Since 4.24.
*
* @since 4.22
*/
@@ -61,19 +64,20 @@ public record McpServerInfo(
List<String> allowedOrigins,
String oauthProfile,
String requiredScopes,
- String resourceUri) {
+ String resourceUri,
+ List<String> allowedHosts) {
public McpServerInfo(String serverName, String version, String path) {
this(serverName, version, path,
McpServerConstants.DEFAULT_SESSION_KEEP_ALIVE_INTERVAL,
McpServerConstants.DEFAULT_SESSION_IDLE_TTL,
- null, null, null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null, null, null);
}
public McpServerInfo(String serverName, String version, String path,
long sessionKeepAliveIntervalMs, long
sessionIdleTtlMs) {
this(serverName, version, path, sessionKeepAliveIntervalMs,
sessionIdleTtlMs,
- null, null, null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null, null, null);
}
public McpServerInfo(String serverName, String version, String path,
@@ -81,7 +85,7 @@ public record McpServerInfo(
String title, String description, String websiteUrl,
String instructions,
List<McpServerIcon> icons) {
this(serverName, version, path, sessionKeepAliveIntervalMs,
sessionIdleTtlMs,
- title, description, websiteUrl, instructions, icons, null, null,
null, null);
+ title, description, websiteUrl, instructions, icons, null, null,
null, null, null);
}
/**
@@ -105,7 +109,36 @@ public record McpServerInfo(
String title, String description, String websiteUrl,
String instructions,
List<McpServerIcon> icons, List<String>
allowedOrigins) {
this(serverName, version, path, sessionKeepAliveIntervalMs,
sessionIdleTtlMs,
- title, description, websiteUrl, instructions, icons,
allowedOrigins, null, null, null);
+ title, description, websiteUrl, instructions, icons,
allowedOrigins, null, null, null, null);
+ }
+
+ /**
+ * Compatibility constructor preserving the signature from before the
allowed hosts. The {@code Host} header is not
+ * checked when this constructor is used.
+ *
+ * @param serverName the MCP server name
+ * @param version the MCP server version
+ * @param path the HTTP endpoint path
+ * @param sessionKeepAliveIntervalMs keep-alive interval in milliseconds
+ * @param sessionIdleTtlMs session idle TTL in milliseconds
+ * @param title optional display title
+ * @param description optional server description
+ * @param websiteUrl optional server website URL
+ * @param instructions optional server instructions
+ * @param icons optional server icons
+ * @param allowedOrigins allowed HTTP origins, or null for the
default
+ * @param oauthProfile the camel-oauth profile, or null when
disabled
+ * @param requiredScopes comma-separated OAuth scopes required
by the MCP HTTP endpoint
+ * @param resourceUri explicit public HTTPS URI of the
protected MCP resource
+ */
+ public McpServerInfo(String serverName, String version, String path,
+ long sessionKeepAliveIntervalMs, long
sessionIdleTtlMs,
+ String title, String description, String websiteUrl,
String instructions,
+ List<McpServerIcon> icons, List<String>
allowedOrigins,
+ String oauthProfile, String requiredScopes, String
resourceUri) {
+ this(serverName, version, path, sessionKeepAliveIntervalMs,
sessionIdleTtlMs,
+ title, description, websiteUrl, instructions, icons,
allowedOrigins, oauthProfile, requiredScopes,
+ resourceUri, null);
}
public McpServerInfo {
@@ -121,5 +154,8 @@ public record McpServerInfo(
if (allowedOrigins != null) {
allowedOrigins = List.copyOf(allowedOrigins);
}
+ if (allowedHosts != null) {
+ allowedHosts = List.copyOf(allowedHosts);
+ }
}
}
diff --git
a/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgeTest.java
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgeTest.java
index b0411d32aab2..c4908e526813 100644
---
a/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgeTest.java
+++
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgeTest.java
@@ -247,6 +247,47 @@ class McpServerBridgeTest extends CamelTestSupport {
assertThat(engine.info().allowedOrigins()).isNull();
}
+ @Test
+ void testAllowedHostsPassedToEngine() throws Exception {
+ CamelContext hostContext = new DefaultCamelContext();
+ RecordingMcpServerEngine hostEngine = new RecordingMcpServerEngine();
+ hostContext.getRegistry().bind("mcpServerEngine", hostEngine);
+ McpServerConfiguration configuration = new McpServerConfiguration();
+ configuration.setTags("crm");
+ configuration.setAllowedHosts(" MCP.Example.com ,, localhost:* ");
+ McpServerBridge hostBridge = new McpServerBridge(configuration);
+ hostContext.addService(hostBridge);
+ hostContext.start();
+ hostBridge.start();
+
+ try {
+
assertThat(hostEngine.info().allowedHosts()).containsExactly("mcp.example.com",
"localhost:*");
+ } finally {
+ hostBridge.stop();
+ hostContext.stop();
+ }
+ }
+
+ @Test
+ void testAllowedHostsNotSetDoesNotCheckTheHost() {
+ assertThat(engine.info().allowedHosts()).isNull();
+ }
+
+ @Test
+ void testAllowedHostThatCanNeverMatchFailsTheStart() throws Exception {
+ try (DefaultCamelContext hostContext = new DefaultCamelContext()) {
+ hostContext.getRegistry().bind("mcpServerEngine", new
RecordingMcpServerEngine());
+ McpServerConfiguration configuration = new
McpServerConfiguration();
+ configuration.setTags("crm");
+ configuration.setAllowedHosts("http://mcp.example.com");
+ McpServerBridge hostBridge = new McpServerBridge(configuration);
+
+ assertThatThrownBy(() -> hostContext.addService(hostBridge))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageStartingWith("Invalid MCP allowed host
'http://mcp.example.com'");
+ }
+ }
+
@Test
void testAllowedOriginThatCanNeverMatchFailsTheStart() throws Exception {
try (DefaultCamelContext originContext = new DefaultCamelContext()) {
diff --git
a/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerHostsTest.java
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerHostsTest.java
new file mode 100644
index 000000000000..3ad7687f3a3e
--- /dev/null
+++
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerHostsTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.camel.component.mcp.server;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class McpServerHostsTest {
+
+ @Test
+ void notSetTurnsTheCheckOff() {
+ assertThat(McpServerHosts.parseAllowedHosts(null)).isNull();
+ assertThat(McpServerHosts.parseAllowedHosts(" ")).isNull();
+ // a value with no host in it is the same as not set
+ assertThat(McpServerHosts.parseAllowedHosts(",")).isNull();
+ }
+
+ @Test
+ void entriesAreTrimmedAndNormalized() {
+ assertThat(McpServerHosts.parseAllowedHosts(" MCP.Example.com ,,
LocalHost:* , [::1]:08080 "))
+ .containsExactly("mcp.example.com", "localhost:*",
"[::1]:8080");
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "mcp.example.com, mcp.example.com",
+ "MCP.Example.COM, mcp.example.com",
+ "mcp.example.com:443, mcp.example.com:443",
+ "localhost:08080, localhost:8080",
+ "localhost:*, localhost:*",
+ "127.0.0.1:*, 127.0.0.1:*",
+ "[::1]:*, [::1]:*",
+ "[FE80::1]:3000, [fe80::1]:3000" })
+ void allowedHostIsNormalized(String entry, String expected) {
+
assertThat(McpServerHosts.normalizeAllowedHost(entry)).isEqualTo(expected);
+ }
+
+ @Test
+ void wildcardForAllHostsIsRefused() {
+ assertThatThrownBy(() ->
McpServerHosts.parseAllowedHosts("mcp.example.com, *"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageStartingWith("MCP allowed host * is not supported")
+ .hasMessageContaining("localhost:*");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "http://mcp.example.com", "mcp.example.com/mcp",
"[email protected]", "*.example.com", "mcp.example.com:",
+ "mcp.example.com:x", "mcp.example.com:70000", "mcp example.com",
"::1", "[::1" })
+ void entryThatCanNeverMatchIsRefused(String entry) {
+ assertThatThrownBy(() -> McpServerHosts.normalizeAllowedHost(entry))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageStartingWith("Invalid MCP allowed host '" + entry +
"'");
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "mcp.example.com, mcp.example.com",
+ "MCP.Example.com:8080, mcp.example.com:8080",
+ "[::1]:8080, [::1]:8080",
+ "evil..example.com/x, evil..example.com/x" })
+ void hostHeaderIsNormalized(String header, String expected) {
+ assertThat(McpServerHosts.normalizeHost(header)).isEqualTo(expected);
+ }
+
+ @Test
+ void hostHeaderWithAWildcardPortIsKeptAsIs() {
+ // only an allowed host may have a wildcard port; a header with one is
left as is, it is lower-cased only when valid
+
assertThat(McpServerHosts.normalizeHost("LocalHost:*")).isEqualTo("LocalHost:*");
+ assertThat(McpServerHosts.normalizeHost(null)).isNull();
+ }
+}
diff --git a/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
b/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
index cdf1ca7600b1..5767742c4f41 100644
--- a/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
+++ b/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
@@ -522,12 +522,35 @@ path such as `+https://app.example.com/mcp+`, an entry
without a scheme such as
A refused request is logged at `DEBUG` level by the
`org.apache.camel.component.mcp.server.vertx.VertxMcpStreamableServerTransportProvider`
-logger, with the `Origin` it carried.
+logger, with the `Origin` (or the `Host`) it carried.
The check is independent of the platform-http CORS configuration: CORS only
adds response headers for the browser, while the origin check refuses the
request on the server.
+The `Host` header can be checked as well, with
+`camel.server.mcp-allowed-hosts`. It is not set by default, and then the `Host`
+header is not checked. When set, a request whose `Host` header is missing or
not
+in the list gets HTTP 421 (Misdirected Request) with a JSON-RPC error, before
+its body is read and before any session is created. This protects a server on a
+private address against DNS rebinding: a hostile web page whose own host name
+resolves to the address of the server sends its own host name in the `Host`
+header, not one from the list.
+
+[source,properties]
+----
+camel.server.mcp-allowed-hosts = mcp.example.com, localhost:*, 127.0.0.1:*
+----
+
+Each entry is a host, or a host and a port, without a scheme. A port of `+*+`
+matches any port, and also a `Host` header without a port (the default port of
+the scheme), so `localhost:*` allows `localhost`, `localhost:8080` and
+`localhost:9876`. An entry without a port matches only a `Host` header without
+a port: `mcp.example.com` is the host name a client uses on the default port,
+behind a reverse proxy for example. The host is compared ignoring case. Write
an
+IPv6 address in brackets, such as `[::1]:*`. An entry with a scheme or a path,
+and `+*+`, fail the startup with a message saying why.
+
The main HTTP server listens on all network interfaces by default
(`camel.server.host = 0.0.0.0`). When the MCP server is only meant for clients
on the same machine, bind it to the loopback interface:
diff --git
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/main/DefaultMcpServerFactory.java
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/main/DefaultMcpServerFactory.java
index bdc2510a672e..6a5dfed267fc 100644
---
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/main/DefaultMcpServerFactory.java
+++
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/main/DefaultMcpServerFactory.java
@@ -61,6 +61,7 @@ public class DefaultMcpServerFactory implements
McpServerFactory {
mcpConfiguration.setSessionKeepAliveInterval(configuration.getMcpSessionKeepAliveInterval());
mcpConfiguration.setSessionIdleTtl(configuration.getMcpSessionIdleTtl());
mcpConfiguration.setAllowedOrigins(configuration.getMcpAllowedOrigins());
+ mcpConfiguration.setAllowedHosts(configuration.getMcpAllowedHosts());
// OAuth options have no meaning on stdio. Keep the historical
warn-and-ignore behavior here,
// while programmatic bridge configurations fail closed on engines
that do not support OAuth.
if (!isStdioTransport(configuration.getMcpTransport())) {
diff --git
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
index 724438defdfa..d0175cdea2e2 100644
---
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
+++
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
@@ -64,7 +64,9 @@ import org.slf4j.LoggerFactory;
* <p>
* Every request on the MCP endpoint that carries an {@code Origin} header
must come from an allowed origin
* ({@link McpServerInfo#allowedOrigins()}, loopback origins by default), or
it is rejected with HTTP 403 before it
- * reaches the MCP server. Requests without an {@code Origin} header (MCP
clients running as a process) are served.
+ * reaches the MCP server. Requests without an {@code Origin} header (MCP
clients running as a process) are served. When
+ * allowed hosts are configured ({@link McpServerInfo#allowedHosts()}), every
request must also carry a {@code Host}
+ * header from that list, or it is rejected with HTTP 421 in the same way.
*/
@JdkService(McpServerConstants.MCP_SERVER_ENGINE_FACTORY)
public class VertxMcpServerEngine extends ServiceSupport implements
McpServerEngine {
@@ -148,7 +150,7 @@ public class VertxMcpServerEngine extends ServiceSupport
implements McpServerEng
jsonMapper, info.path(),
info.sessionKeepAliveIntervalMs(), info.sessionIdleTtlMs(),
info.allowedOrigins(),
camelContext, oauthSecurity, info.requiredScopes(),
info.resourceUri(),
- authorizationServerIssuer, allowInsecureAuthorizationServer);
+ authorizationServerIssuer, allowInsecureAuthorizationServer,
info.allowedHosts());
McpServer.SyncSpecification<?> specification =
McpServer.sync(transport)
.serverInfo(buildServerInfo(info))
// resources(subscribe, listChanged): per-resource
subscriptions are not offered, but clients are
diff --git
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
index de2df446ac5c..f1ba4a80816c 100644
---
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
+++
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
@@ -43,12 +43,14 @@ import io.vertx.core.Context;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerResponse;
+import io.vertx.core.net.HostAndPort;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.SecurityPolicyHandler;
import org.apache.camel.CamelContext;
+import org.apache.camel.component.mcp.server.McpServerHosts;
import org.apache.camel.component.mcp.server.McpServerOrigins;
import org.apache.camel.component.platform.http.vertx.VertxPlatformHttpRouter;
import org.apache.camel.http.base.OAuthHttpSecuritySupport;
@@ -70,6 +72,10 @@ import reactor.core.publisher.Mono;
* Every request carrying an {@code Origin} header is checked against the
allowed origins before any other handling, as
* the MCP streamable HTTP transport requires; a request from an origin that
is not allowed gets HTTP 403 with a
* JSON-RPC error and never reaches the MCP session. Requests without an
{@code Origin} header are served.
+ * <p>
+ * When allowed hosts are configured, the {@code Host} header is checked in
the same handler: a request whose
+ * {@code Host} is missing or not allowed gets HTTP 421 (Misdirected Request),
which protects a server on a private
+ * address against DNS rebinding. Without allowed hosts the {@code Host}
header is not checked.
*/
public class VertxMcpStreamableServerTransportProvider implements
McpStreamableServerTransportProvider {
@@ -86,6 +92,9 @@ public class VertxMcpStreamableServerTransportProvider
implements McpStreamableS
private static final String APPLICATION_JSON = "application/json";
private static final String TEXT_EVENT_STREAM = "text/event-stream";
private static final String ORIGIN = "Origin";
+ private static final String HOST = "Host";
+ // the status the MCP SDK validator answers a Host that is not allowed with
+ private static final int MISDIRECTED_REQUEST = 421;
/**
* The origins allowed when none are configured: loopback hosts on any
port, so browser-based tools on the same
@@ -158,6 +167,32 @@ public class VertxMcpStreamableServerTransportProvider
implements McpStreamableS
String resourceUri,
String
authorizationServerIssuer,
boolean
allowInsecureAuthorizationServer) {
+ this(jsonMapper, path, sessionKeepAliveIntervalMs, sessionIdleTtlMs,
allowedOrigins, camelContext,
+ oauthSecurity, requiredScopes, resourceUri,
authorizationServerIssuer, allowInsecureAuthorizationServer,
+ null);
+ }
+
+ /**
+ * Creates the transport with allowed hosts.
+ *
+ * @param allowedHosts the hosts allowed in the {@code Host} header, each
exact (e.g. {@code mcp.example.com}) or
+ * with a wildcard port (e.g. {@code localhost:*},
which also allows a {@code Host} without a
+ * port), normalized with {@link
McpServerHosts#normalizeAllowedHost(String)}; {@code null} to
+ * not check the {@code Host} header
+ */
+ public VertxMcpStreamableServerTransportProvider(
+ McpJsonMapper jsonMapper,
+ String path,
+ long
sessionKeepAliveIntervalMs,
+ long sessionIdleTtlMs,
+ List<String>
allowedOrigins,
+ CamelContext camelContext,
+ OAuthHttpSecuritySupport
oauthSecurity,
+ String requiredScopes,
+ String resourceUri,
+ String
authorizationServerIssuer,
+ boolean
allowInsecureAuthorizationServer,
+ List<String>
allowedHosts) {
this.jsonMapper = jsonMapper;
this.path = path;
this.sessionKeepAliveIntervalMs = sessionKeepAliveIntervalMs;
@@ -175,9 +210,26 @@ public class VertxMcpStreamableServerTransportProvider
implements McpStreamableS
.allowedOrigins(allowedOrigins != null
?
allowedOrigins.stream().map(McpServerOrigins::normalizeAllowedOrigin).toList()
: DEFAULT_ALLOWED_ORIGINS)
+ .allowedHosts(allowedHosts != null ?
hostPatterns(allowedHosts) : List.of())
.build();
}
+ /**
+ * The hosts as the MCP SDK validator matches them: a {@code host:*} entry
matches only a {@code Host} with a port
+ * there, so the host without a port (the default port of the scheme) is
added next to it.
+ */
+ private static List<String> hostPatterns(List<String> allowedHosts) {
+ List<String> answer = new ArrayList<>();
+ for (String entry : allowedHosts) {
+ String host = McpServerHosts.normalizeAllowedHost(entry);
+ answer.add(host);
+ if (host.endsWith(":*")) {
+ answer.add(host.substring(0, host.length() - 2));
+ }
+ }
+ return answer;
+ }
+
@Override
public void setSessionFactory(McpStreamableServerSession.Factory
sessionFactory) {
this.sessionFactory = sessionFactory;
@@ -262,19 +314,28 @@ public class VertxMcpStreamableServerTransportProvider
implements McpStreamableS
}
/**
- * Serves a request without an {@code Origin} header or from an allowed
origin; answers any other with HTTP 403 and
- * a JSON-RPC error that has no {@code id}. Runs on the event loop before
any other handler of the route.
+ * Serves a request without an {@code Origin} header or from an allowed
origin, and, when allowed hosts are
+ * configured, with an allowed {@code Host}; answers any other with HTTP
403 (origin) or 421 (host) and a JSON-RPC
+ * error that has no {@code id}. Runs on the event loop before any other
handler of the route.
*/
private void checkOrigin(RoutingContext ctx) {
try {
originValidator.validate(new VertxHeaderAccessor(ctx));
} catch (ServerTransportSecurityException e) {
- LOG.debug("Rejected MCP {} request on {}: origin {} is not
allowed", ctx.request().method(), path,
- ctx.request().getHeader(ORIGIN));
+ String message;
+ if (e.getStatusCode() == MISDIRECTED_REQUEST) {
+ LOG.debug("Rejected MCP {} request on {}: host {} is not
allowed", ctx.request().method(), path,
+ ctx.request().authority());
+ message = "Host not allowed";
+ } else {
+ LOG.debug("Rejected MCP {} request on {}: origin {} is not
allowed", ctx.request().method(), path,
+ ctx.request().getHeader(ORIGIN));
+ message = "Origin not allowed";
+ }
ctx.response()
.setStatusCode(e.getStatusCode())
.putHeader("Content-Type", APPLICATION_JSON)
- .end(errorBody(McpSchema.ErrorCodes.INVALID_REQUEST,
"Origin not allowed"));
+ .end(errorBody(McpSchema.ErrorCodes.INVALID_REQUEST,
message));
return;
}
ctx.next();
@@ -789,6 +850,19 @@ public class VertxMcpStreamableServerTransportProvider
implements McpStreamableS
@Override
public List<String> getHeader(String name) {
+ if (HOST.equalsIgnoreCase(name)) {
+ // the authority of the request: the Host header on HTTP/1.1,
:authority on HTTP/2
+ HostAndPort authority = ctx.request().authority();
+ if (authority == null) {
+ return List.of();
+ }
+ String host = authority.host();
+ if (host.indexOf(':') >= 0 && !host.startsWith("[")) {
+ host = "[" + host + "]";
+ }
+ String value = authority.port() >= 0 ? host + ":" +
authority.port() : host;
+ return List.of(McpServerHosts.normalizeHost(value));
+ }
List<String> values = ctx.request().headers().getAll(name);
if (ORIGIN.equalsIgnoreCase(name)) {
// compare in the form the allowed origins are kept in:
lower-case scheme and host, no default port
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainPropertiesTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainPropertiesTest.java
index c3a2a0d97b04..8af1cbda8324 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainPropertiesTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainPropertiesTest.java
@@ -124,6 +124,43 @@ class McpServerMainPropertiesTest {
}
}
+ @Test
+ void testMcpAllowedHostsFromProperties() throws Exception {
+ int port = AvailablePortFinder.getNextAvailable();
+
+ Main main = new Main();
+ main.addInitialProperty("camel.server.enabled", "true");
+ main.addInitialProperty("camel.server.port", String.valueOf(port));
+ main.addInitialProperty("camel.server.mcp-enabled", "true");
+ main.addInitialProperty("camel.server.mcp-tags", "crm");
+ main.addInitialProperty("camel.server.mcp-allowed-hosts",
"LocalHost:*");
+ main.start();
+
+ try {
+ assertThat(initialize(URI.create("http://localhost:" + port +
"/mcp"), null)).isEqualTo(200);
+ assertThat(initialize(URI.create("http://127.0.0.1:" + port +
"/mcp"), null)).isEqualTo(421);
+ } finally {
+ main.stop();
+ }
+ }
+
+ @Test
+ void testMcpAllowedHostWithSchemeFailsFast() {
+ Main main = new Main();
+ main.addInitialProperty("camel.server.enabled", "true");
+ main.addInitialProperty("camel.server.port",
String.valueOf(AvailablePortFinder.getNextAvailable()));
+ main.addInitialProperty("camel.server.mcp-enabled", "true");
+ main.addInitialProperty("camel.server.mcp-tags", "crm");
+ main.addInitialProperty("camel.server.mcp-allowed-hosts",
"http://localhost:8080");
+
+ try {
+ assertThatThrownBy(main::start)
+ .hasStackTraceContaining("Invalid MCP allowed host
'http://localhost:8080'");
+ } finally {
+ main.stop();
+ }
+ }
+
@Test
void testMcpAllowedOriginWildcardFailsFast() {
Main main = new Main();
@@ -142,8 +179,12 @@ class McpServerMainPropertiesTest {
}
private static int initialize(int port, String origin) throws Exception {
+ return initialize(URI.create("http://localhost:" + port + "/mcp"),
origin);
+ }
+
+ private static int initialize(URI uri, String origin) throws Exception {
HttpRequest.Builder builder = HttpRequest.newBuilder()
- .uri(URI.create("http://localhost:" + port + "/mcp"))
+ .uri(uri)
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.header("Accept", "application/json, text/event-stream")
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpHostTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpHostTest.java
new file mode 100644
index 000000000000..dbdd1f9c5413
--- /dev/null
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpHostTest.java
@@ -0,0 +1,331 @@
+/*
+ * 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.camel.component.mcp.server.vertx;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.ai.tool.AiToolParameterHelper.ParameterDef;
+import org.apache.camel.component.mcp.server.McpServerInfo;
+import org.apache.camel.component.mcp.server.McpServerTool;
+import org.apache.camel.component.mcp.server.McpToolCallHandler;
+import org.apache.camel.component.mcp.server.McpToolCallResult;
+import org.apache.camel.component.platform.http.main.MainHttpServer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that the Vert.x streamable transport checks the {@code Host}
header on every method of the MCP endpoint when
+ * allowed hosts are configured, and not at all otherwise (CAMEL-26665).
+ */
+class VertxMcpHostTest {
+
+ private static final HttpClient HTTP = HttpClient.newHttpClient();
+
+ private static final String INITIALIZE_REQUEST = """
+
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18",
+ "capabilities":{},"clientInfo":{"name":"test","version":"1"}}}""";
+
+ private static final String INITIALIZED_NOTIFICATION = """
+
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}""";
+
+ private static final String CALL_TOOL_REQUEST = """
+
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ping","arguments":{}}}""";
+
+ private CamelContext camelContext;
+ private VertxMcpServerEngine engine;
+ private int port;
+
+ @AfterEach
+ void stop() {
+ if (engine != null) {
+ engine.stop();
+ }
+ if (camelContext != null) {
+ camelContext.stop();
+ }
+ }
+
+ @Test
+ void hostIsNotCheckedByDefault() throws Exception {
+ start(null);
+
+ assertThat(post("localhost", null,
INITIALIZE_REQUEST).statusCode()).isEqualTo(200);
+ assertThat(post("127.0.0.1", null,
INITIALIZE_REQUEST).statusCode()).isEqualTo(200);
+ assertThat(rawPost("Host: attacker.example.com:" +
port)).startsWith("HTTP/1.1 200");
+ assertThat(engine.sessionCount()).isEqualTo(3);
+ }
+
+ @Test
+ void allowedHostIsServed() throws Exception {
+ start(List.of("localhost:*"));
+
+ HttpResponse<String> response = post("localhost", null,
INITIALIZE_REQUEST);
+
+ assertThat(response.statusCode()).isEqualTo(200);
+
assertThat(response.headers().firstValue("mcp-session-id")).isPresent();
+ assertThat(engine.sessionCount()).isEqualTo(1);
+ }
+
+ @Test
+ void otherHostIsRejected() throws Exception {
+ start(List.of("localhost:*"));
+
+ HttpResponse<String> response = post("127.0.0.1", null,
INITIALIZE_REQUEST);
+
+ assertThat(response.statusCode()).isEqualTo(421);
+
assertThat(response.headers().firstValue("Content-Type")).hasValueSatisfying(
+ value -> assertThat(value).startsWith("application/json"));
+ assertThat(response.body())
+ .contains("\"jsonrpc\":\"2.0\"")
+ .contains("\"code\":-32600")
+ .contains("Host not allowed")
+ .doesNotContain("\"id\"");
+ assertThat(response.headers().firstValue("mcp-session-id")).isEmpty();
+ assertThat(engine.sessionCount()).isZero();
+ }
+
+ @Test
+ void reboundHostNameIsRejected() throws Exception {
+ start(List.of("localhost:*", "127.0.0.1:*"));
+
+ // a page on a host name that resolves to this server (DNS rebinding)
sends its own host name
+ assertThat(rawPost("Host: attacker.example.com:" +
port)).startsWith("HTTP/1.1 421");
+ assertThat(rawPost("Host: localhost.attacker.example.com:" +
port)).startsWith("HTTP/1.1 421");
+ assertThat(engine.sessionCount()).isZero();
+ }
+
+ @Test
+ void missingHostIsRejected() throws Exception {
+ start(List.of("localhost:*"));
+
+ // Vert.x refuses an HTTP/1.1 request without a Host header itself;
HTTP/1.0 has no Host header to require
+ assertThat(rawPost("HTTP/1.1", null)).startsWith("HTTP/1.1 400");
+ assertThat(rawPost("HTTP/1.0", null)).contains(" 421 ");
+ assertThat(engine.sessionCount()).isZero();
+ }
+
+ @Test
+ void hostIsComparedIgnoringCase() throws Exception {
+ start(List.of("mcp.example.com:*"));
+
+ assertThat(rawPost("Host: MCP.Example.COM:" +
port)).startsWith("HTTP/1.1 200");
+ }
+
+ @Test
+ void wildcardPortAlsoMatchesAHostWithoutAPort() throws Exception {
+ start(List.of("mcp.example.com:*"));
+
+ assertThat(rawPost("Host: mcp.example.com")).startsWith("HTTP/1.1
200");
+ assertThat(rawPost("Host: mcp.example.com:" +
port)).startsWith("HTTP/1.1 200");
+ }
+
+ @Test
+ void exactPortMatchesThatPortOnly() throws Exception {
+ start(List.of("mcp.example.com:8443", "proxy.example.com"));
+
+ assertThat(rawPost("Host: mcp.example.com:8443")).startsWith("HTTP/1.1
200");
+ assertThat(rawPost("Host: mcp.example.com:" +
port)).startsWith("HTTP/1.1 421");
+ assertThat(rawPost("Host: mcp.example.com")).startsWith("HTTP/1.1
421");
+ assertThat(rawPost("Host: proxy.example.com")).startsWith("HTTP/1.1
200");
+ assertThat(rawPost("Host: proxy.example.com:" +
port)).startsWith("HTTP/1.1 421");
+ }
+
+ @Test
+ void originIsStillCheckedFirst() throws Exception {
+ start(List.of("localhost:*"));
+
+ HttpResponse<String> response = post("localhost",
"http://app.example.com", null, INITIALIZE_REQUEST);
+
+ assertThat(response.statusCode()).isEqualTo(403);
+ assertThat(response.body()).contains("Origin not allowed");
+ }
+
+ @Test
+ void rejectedHostDoesNotCallTheTool() throws Exception {
+ start(List.of("localhost:*"));
+ AtomicInteger calls = new AtomicInteger();
+ engine.toolAdded(tool("ping", arguments -> {
+ calls.incrementAndGet();
+ return new McpToolCallResult("pong", false);
+ }));
+ String sessionId = initializeSession();
+
+ HttpResponse<String> rejected = post("127.0.0.1", sessionId,
CALL_TOOL_REQUEST);
+ assertThat(rejected.statusCode()).isEqualTo(421);
+ assertThat(calls).hasValue(0);
+
+ HttpResponse<String> served = post("localhost", sessionId,
CALL_TOOL_REQUEST);
+ assertThat(served.statusCode()).isEqualTo(200);
+ assertThat(served.body()).contains("pong");
+ assertThat(calls).hasValue(1);
+ }
+
+ @Test
+ void sseChannelChecksTheHost() throws Exception {
+ start(List.of("localhost:*"));
+ String sessionId = initializeSession();
+
+ HttpResponse<InputStream> rejected = HTTP.send(
+ request("127.0.0.1", null, sessionId).header("Accept",
"text/event-stream").GET().build(),
+ HttpResponse.BodyHandlers.ofInputStream());
+ try (InputStream body = rejected.body()) {
+ assertThat(rejected.statusCode()).isEqualTo(421);
+ }
+ }
+
+ @Test
+ void deleteChecksTheHost() throws Exception {
+ start(List.of("localhost:*"));
+ String sessionId = initializeSession();
+
+ assertThat(delete("127.0.0.1", sessionId)).isEqualTo(421);
+ assertThat(engine.sessionCount()).isEqualTo(1);
+
+ assertThat(delete("localhost", sessionId)).isEqualTo(200);
+ assertThat(engine.sessionCount()).isZero();
+ }
+
+ private void start(List<String> allowedHosts) throws Exception {
+ port = AvailablePortFinder.getNextAvailable();
+ camelContext = new DefaultCamelContext();
+ MainHttpServer main = new MainHttpServer();
+ main.setCamelContext(camelContext);
+ main.setHost("0.0.0.0");
+ main.setPort(port);
+ camelContext.addService(main);
+ camelContext.start();
+
+ engine = new VertxMcpServerEngine();
+ engine.setCamelContext(camelContext);
+ engine.initialize(new McpServerInfo(
+ "host", "1.0", "/mcp", 0, 0, null, null, null, null, null,
null, null, null, null, allowedHosts));
+ engine.start();
+ }
+
+ private String initializeSession() throws Exception {
+ HttpResponse<String> response = post("localhost", null,
INITIALIZE_REQUEST);
+ assertThat(response.statusCode()).isEqualTo(200);
+ String sessionId =
response.headers().firstValue("mcp-session-id").orElseThrow();
+ assertThat(post("localhost", sessionId,
INITIALIZED_NOTIFICATION).statusCode()).isEqualTo(202);
+ return sessionId;
+ }
+
+ private HttpResponse<String> post(String host, String origin, String
sessionId, String body) throws Exception {
+ HttpRequest.Builder builder = request(host, origin, sessionId)
+ .header("Content-Type", "application/json")
+ .header("Accept", "application/json, text/event-stream")
+ .POST(HttpRequest.BodyPublishers.ofString(body));
+ return HTTP.send(builder.build(),
HttpResponse.BodyHandlers.ofString());
+ }
+
+ private HttpResponse<String> post(String host, String sessionId, String
body) throws Exception {
+ return post(host, null, sessionId, body);
+ }
+
+ private int delete(String host, String sessionId) throws Exception {
+ HttpRequest.Builder builder = request(host, null, sessionId).DELETE();
+ return HTTP.send(builder.build(),
HttpResponse.BodyHandlers.discarding()).statusCode();
+ }
+
+ /**
+ * A request to {@code host} on the port of the server: the HTTP client
sends the host and port of the URI as the
+ * {@code Host} header, and both {@code localhost} and {@code 127.0.0.1}
reach the server.
+ */
+ private HttpRequest.Builder request(String host, String origin, String
sessionId) {
+ HttpRequest.Builder builder = HttpRequest.newBuilder()
+ .uri(URI.create("http://" + host + ":" + port + "/mcp"))
+ .timeout(Duration.ofSeconds(10));
+ if (origin != null) {
+ builder.header("Origin", origin);
+ }
+ if (sessionId != null) {
+ builder.header("Mcp-Session-Id", sessionId);
+ }
+ return builder;
+ }
+
+ /**
+ * Sends an initialize request over a plain socket, with the given HTTP
version and {@code Host} header line, or
+ * none when {@code null} (the HTTP client does not allow setting it), and
returns the response.
+ */
+ private String rawPost(String hostLine) throws Exception {
+ return rawPost("HTTP/1.1", hostLine);
+ }
+
+ private String rawPost(String version, String hostLine) throws Exception {
+ byte[] body = INITIALIZE_REQUEST.getBytes(StandardCharsets.UTF_8);
+ String head = "POST /mcp " + version + "\r\n"
+ + (hostLine != null ? hostLine + "\r\n" : "")
+ + "Content-Type: application/json\r\n"
+ + "Accept: application/json, text/event-stream\r\n"
+ + "Content-Length: " + body.length + "\r\n"
+ + "Connection: close\r\n\r\n";
+ try (Socket socket = new Socket("127.0.0.1", port)) {
+ socket.setSoTimeout(10_000);
+ OutputStream out = socket.getOutputStream();
+ out.write(head.getBytes(StandardCharsets.US_ASCII));
+ out.write(body);
+ out.flush();
+ return new String(socket.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
+ }
+ }
+
+ private static McpServerTool tool(String name, McpToolCallHandler handler)
{
+ return new McpServerTool() {
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String description() {
+ return "Answers pong";
+ }
+
+ @Override
+ public String inputSchemaJson() {
+ return null;
+ }
+
+ @Override
+ public Map<String, ParameterDef> parameters() {
+ return Map.of();
+ }
+
+ @Override
+ public McpToolCallHandler handler() {
+ return handler;
+ }
+ };
+ }
+}
diff --git
a/core/camel-main/src/generated/java/org/apache/camel/main/HttpServerConfigurationPropertiesConfigurer.java
b/core/camel-main/src/generated/java/org/apache/camel/main/HttpServerConfigurationPropertiesConfigurer.java
index 402a5ffb8f06..47fd5067db27 100644
---
a/core/camel-main/src/generated/java/org/apache/camel/main/HttpServerConfigurationPropertiesConfigurer.java
+++
b/core/camel-main/src/generated/java/org/apache/camel/main/HttpServerConfigurationPropertiesConfigurer.java
@@ -38,6 +38,7 @@ public class HttpServerConfigurationPropertiesConfigurer
extends org.apache.came
map.put("JwtKeystorePath", java.lang.String.class);
map.put("JwtKeystoreType", java.lang.String.class);
map.put("MaxBodySize", java.lang.Long.class);
+ map.put("McpAllowedHosts", java.lang.String.class);
map.put("McpAllowedOrigins", java.lang.String.class);
map.put("McpEnabled", boolean.class);
map.put("McpInstructions", java.lang.String.class);
@@ -98,6 +99,8 @@ public class HttpServerConfigurationPropertiesConfigurer
extends org.apache.came
case "jwtKeystoreType":
target.setJwtKeystoreType(property(camelContext, java.lang.String.class,
value)); return true;
case "maxbodysize":
case "maxBodySize": target.setMaxBodySize(property(camelContext,
java.lang.Long.class, value)); return true;
+ case "mcpallowedhosts":
+ case "mcpAllowedHosts":
target.setMcpAllowedHosts(property(camelContext, java.lang.String.class,
value)); return true;
case "mcpallowedorigins":
case "mcpAllowedOrigins":
target.setMcpAllowedOrigins(property(camelContext, java.lang.String.class,
value)); return true;
case "mcpenabled":
@@ -186,6 +189,8 @@ public class HttpServerConfigurationPropertiesConfigurer
extends org.apache.came
case "jwtKeystoreType": return java.lang.String.class;
case "maxbodysize":
case "maxBodySize": return java.lang.Long.class;
+ case "mcpallowedhosts":
+ case "mcpAllowedHosts": return java.lang.String.class;
case "mcpallowedorigins":
case "mcpAllowedOrigins": return java.lang.String.class;
case "mcpenabled":
@@ -270,6 +275,8 @@ public class HttpServerConfigurationPropertiesConfigurer
extends org.apache.came
case "jwtKeystoreType": return target.getJwtKeystoreType();
case "maxbodysize":
case "maxBodySize": return target.getMaxBodySize();
+ case "mcpallowedhosts":
+ case "mcpAllowedHosts": return target.getMcpAllowedHosts();
case "mcpallowedorigins":
case "mcpAllowedOrigins": return target.getMcpAllowedOrigins();
case "mcpenabled":
diff --git
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
index 0db48e268c8d..a168e12aa0d9 100644
---
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
+++
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
@@ -463,6 +463,7 @@
{ "name": "camel.server.jwtKeystorePath", "required": false,
"description": "Path to the keystore file used for JWT tokens validation.",
"sourceType": "org.apache.camel.main.HttpServerConfigurationProperties",
"type": "string", "javaType": "java.lang.String", "secret": false },
{ "name": "camel.server.jwtKeystoreType", "required": false,
"description": "Type of the keystore used for JWT tokens validation (jks,
pkcs12, etc.).", "sourceType":
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "string",
"javaType": "java.lang.String", "secret": false },
{ "name": "camel.server.maxBodySize", "required": false, "description":
"Maximum HTTP body size the embedded HTTP server can accept.", "sourceType":
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "integer",
"javaType": "java.lang.Long", "secret": false },
+ { "name": "camel.server.mcpAllowedHosts", "required": false,
"description": "Comma-separated list of hosts allowed in the Host header of a
request to the MCP endpoint on the Vert.x streamable transport, such as
mcp.example.com or localhost:*. Each entry is a host, or a host and a port,
where the port may be an asterisk to match any port (or none). The host is
compared ignoring case. When set, a request whose Host header is missing or not
in the list is rejected with HTTP 421 (Misdire [...]
{ "name": "camel.server.mcpAllowedOrigins", "required": false,
"description": "Comma-separated list of origins allowed to call the MCP
endpoint on the Vert.x streamable transport. Each entry is an exact origin
(scheme, host and port), or ends with a colon and an asterisk to match any
port. Scheme and host are compared ignoring case, and a default port (80 for
http, 443 for https) may be left out. A wildcard for all origins is not
supported: an entry that is not of the form scheme:\/\ [...]
{ "name": "camel.server.mcpEnabled", "required": false, "description":
"Whether to expose ai-tool routes as MCP tools, ai-resource routes as MCP
resources and ai-prompt routes as MCP prompts. Requires camel-mcp-server on the
classpath. Use setMcpTransport(String) to choose streamable HTTP (default) or
stdio for IDE subprocess integration. By default, the MCP server is not
enabled.", "sourceType":
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "boolean",
"javaType" [...]
{ "name": "camel.server.mcpInstructions", "required": false,
"description": "Top-level MCP instructions returned to clients on initialize.",
"sourceType": "org.apache.camel.main.HttpServerConfigurationProperties",
"type": "string", "javaType": "java.lang.String", "secret": false },
diff --git a/core/camel-main/src/main/docs/main.adoc
b/core/camel-main/src/main/docs/main.adoc
index 80c685a6c580..aa3a9ba9a290 100644
--- a/core/camel-main/src/main/docs/main.adoc
+++ b/core/camel-main/src/main/docs/main.adoc
@@ -197,7 +197,7 @@ The camel.routecontroller supports 12 options, which are
listed below.
=== Camel Embedded HTTP Server (only for standalone; not Spring Boot or
Quarkus) configurations
-The camel.server supports 40 options, which are listed below.
+The camel.server supports 41 options, which are listed below.
[width="100%",cols="2,5,^1,2",options="header"]
|===
@@ -217,6 +217,7 @@ The camel.server supports 40 options, which are listed
below.
| `camel.server.jwtKeystorePath` | Path to the keystore file used for JWT
tokens validation. | | String
| `camel.server.jwtKeystoreType` | Type of the keystore used for JWT tokens
validation (jks, pkcs12, etc.). | | String
| `camel.server.maxBodySize` | Maximum HTTP body size the embedded HTTP server
can accept. | | Long
+| `camel.server.mcpAllowedHosts` | Comma-separated list of hosts allowed in
the Host header of a request to the MCP endpoint on the Vert.x streamable
transport, such as mcp.example.com or localhost:*. Each entry is a host, or a
host and a port, where the port may be an asterisk to match any port (or none).
The host is compared ignoring case. When set, a request whose Host header is
missing or not in the list is rejected with HTTP 421 (Misdirected Request),
before its body is read and bef [...]
| `camel.server.mcpAllowedOrigins` | Comma-separated list of origins allowed
to call the MCP endpoint on the Vert.x streamable transport. Each entry is an
exact origin (scheme, host and port), or ends with a colon and an asterisk to
match any port. Scheme and host are compared ignoring case, and a default port
(80 for http, 443 for https) may be left out. A wildcard for all origins is not
supported: an entry that is not of the form scheme://host or scheme://host:port
fails the startup. A [...]
| `camel.server.mcpEnabled` | Whether to expose ai-tool routes as MCP tools,
ai-resource routes as MCP resources and ai-prompt routes as MCP prompts.
Requires camel-mcp-server on the classpath. Use setMcpTransport(String) to
choose streamable HTTP (default) or stdio for IDE subprocess integration. By
default, the MCP server is not enabled. | false | boolean
| `camel.server.mcpInstructions` | Top-level MCP instructions returned to
clients on initialize. | | String
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
b/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
index 124cb992cf44..54699cb39dd6 100644
---
a/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
+++
b/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
@@ -91,6 +91,8 @@ public class HttpServerConfigurationProperties implements
BootstrapCloseable {
private long mcpSessionIdleTtl = 300000;
@Metadata
private String mcpAllowedOrigins;
+ @Metadata
+ private String mcpAllowedHosts;
@Metadata(label = "security")
private String mcpOauthProfile;
@Metadata(label = "security")
@@ -560,6 +562,23 @@ public class HttpServerConfigurationProperties implements
BootstrapCloseable {
this.mcpAllowedOrigins = mcpAllowedOrigins;
}
+ public String getMcpAllowedHosts() {
+ return mcpAllowedHosts;
+ }
+
+ /**
+ * Comma-separated list of hosts allowed in the Host header of a request
to the MCP endpoint on the Vert.x
+ * streamable transport, such as mcp.example.com or localhost:*. Each
entry is a host, or a host and a port, where
+ * the port may be an asterisk to match any port (or none). The host is
compared ignoring case. When set, a request
+ * whose Host header is missing or not in the list is rejected with HTTP
421 (Misdirected Request), before its body
+ * is read and before any session is created; this protects a server on a
private address against DNS rebinding. An
+ * entry with a scheme, a path or a wildcard for all hosts fails the
startup. When not set, the Host header is not
+ * checked.
+ */
+ public void setMcpAllowedHosts(String mcpAllowedHosts) {
+ this.mcpAllowedHosts = mcpAllowedHosts;
+ }
+
public String getMcpOauthProfile() {
return mcpOauthProfile;
}
@@ -913,6 +932,15 @@ public class HttpServerConfigurationProperties implements
BootstrapCloseable {
return this;
}
+ /**
+ * Comma-separated list of hosts allowed in the Host header of a request
to the MCP endpoint on the Vert.x
+ * streamable transport. When not set, the Host header is not checked.
+ */
+ public HttpServerConfigurationProperties withMcpAllowedHosts(String
mcpAllowedHosts) {
+ this.mcpAllowedHosts = mcpAllowedHosts;
+ return this;
+ }
+
/**
* Name of the camel-oauth profile used to authenticate OAuth bearer
tokens on the MCP HTTP endpoint.
*/