diff --git a/components-starter/camel-mcp-server-starter/src/main/doc/intro.adoc b/components-starter/camel-mcp-server-starter/src/main/doc/intro.adoc index 4af6d66884c..1c94a3143f4 100644 --- a/components-starter/camel-mcp-server-starter/src/main/doc/intro.adoc +++ b/components-starter/camel-mcp-server-starter/src/main/doc/intro.adoc @@ -9,6 +9,36 @@ Tool semantics — tag-based opt-in (the untagged default pool is never exposed), flat-namespace collision refusal, per-call timeout and error sanitization — are owned by the runtime-agnostic `camel-mcp-server-api` bridge and are identical on every Camel runtime. Serving concerns (endpoint -path, protocol, server identity, authentication) are owned by the Spring AI -MCP server and configured via `spring.ai.mcp.server.*`; use +path, protocol, server identity) are owned by the Spring AI MCP server and +configured via `spring.ai.mcp.server.*`; use `spring.ai.mcp.server.protocol=STREAMABLE` for the streamable HTTP transport. + +== Securing the MCP endpoint + +`spring.ai.mcp.server.*` provides **no authentication**. The MCP endpoint is +served on the application's own HTTP port, and anything that can reach it can +list and call every exposed tool. Protecting it is the application's +responsibility. + +Nothing is exposed until `camel.mcp-server.tags` is set — the untagged default +pool is never served — so the surface is opt-in. Once tags are configured, +secure the endpoint path, for example with a Spring Security filter chain: + +[source,java] +---- +@Bean +SecurityFilterChain mcpSecurity(HttpSecurity http) throws Exception { + return http.securityMatcher("/mcp/**") + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) + .build(); +} +---- + +Adjust the matcher to whatever `spring.ai.mcp.server` is configured to serve on. +A network policy that keeps the port off untrusted networks is an alternative +where the deployment allows it. + +See the camel-mcp-server component documentation for the trust boundary this +sits in: external MCP clients are untrusted senders under the Camel security +model. diff --git a/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/CamelMcpServerAutoConfiguration.java b/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/CamelMcpServerAutoConfiguration.java index 4c9c1bac7f2..f2f3b3c00f9 100644 --- a/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/CamelMcpServerAutoConfiguration.java +++ b/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/CamelMcpServerAutoConfiguration.java @@ -22,6 +22,8 @@ import org.apache.camel.component.mcp.server.McpServerConfiguration; import org.apache.camel.component.mcp.server.McpServerEngine; import org.apache.camel.spring.boot.CamelAutoConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -40,6 +42,8 @@ @EnableConfigurationProperties(McpServerConfigurationProperties.class) public class CamelMcpServerAutoConfiguration { + private static final Logger LOG = LoggerFactory.getLogger(CamelMcpServerAutoConfiguration.class); + @Bean(initMethod = "", destroyMethod = "") // Camel handles the lifecycle of this bean @ConditionalOnMissingBean(McpServerEngine.class) @@ -58,6 +62,14 @@ McpServerBridge camelMcpServerBridge( configuration.setToolTimeout(properties.getToolTimeout()); McpServerBridge bridge = new McpServerBridge(configuration); camelContext.addService(bridge); + if (properties.getTags() != null && !properties.getTags().isBlank()) { + // exposing tools is opt-in, but once opted in the endpoint is reachable by anything that can reach + // the application HTTP port - spring.ai.mcp.server.* has no authentication of its own + LOG.info("Exposing ai-tool routes tagged [{}] as MCP tools. The MCP endpoint is served on the" + + " application HTTP port and is not authenticated by the Spring AI MCP server; secure it in" + + " the application, for example with a Spring Security filter chain on its path.", + properties.getTags()); + } return bridge; } } diff --git a/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/McpServerConfigurationProperties.java b/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/McpServerConfigurationProperties.java index b5023fe62b4..2a35b7e6472 100644 --- a/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/McpServerConfigurationProperties.java +++ b/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/McpServerConfigurationProperties.java @@ -19,8 +19,12 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** - * Bridge-owned configuration of the Camel MCP server. Serving concerns (endpoint path, protocol, server identity, - * authentication) are owned by the Spring AI MCP server and configured via {@code spring.ai.mcp.server.*}. + * Bridge-owned configuration of the Camel MCP server. Serving concerns (endpoint path, protocol, server identity) + * are owned by the Spring AI MCP server and configured via {@code spring.ai.mcp.server.*}. + *

+ * Note that {@code spring.ai.mcp.server.*} provides no authentication: the MCP endpoint is served on the + * application's own HTTP port and must be protected by the application, for example with a Spring Security filter + * chain matching the endpoint path. */ @ConfigurationProperties(prefix = "camel.mcp-server") public class McpServerConfigurationProperties { diff --git a/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/SpringAiMcpServerEngine.java b/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/SpringAiMcpServerEngine.java index 1728fd4fd9a..1f8939af346 100644 --- a/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/SpringAiMcpServerEngine.java +++ b/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/SpringAiMcpServerEngine.java @@ -37,7 +37,8 @@ * {@link McpServerEngine} publishing tools into the Spring AI MCP server: {@code toolAdded}/{@code toolRemoved} map to * the auto-configured {@link McpSyncServer}'s {@code addTool}/{@code removeTool}, which emit * {@code notifications/tools/list_changed} to connected clients. Serving concerns (endpoint path, protocol, server - * identity, authentication) are owned by the Spring AI MCP server configuration ({@code spring.ai.mcp.server.*}). + * identity) are owned by the Spring AI MCP server configuration ({@code spring.ai.mcp.server.*}), which provides + * no authentication - the endpoint must be protected by the application. */ public class SpringAiMcpServerEngine extends ServiceSupport implements McpServerEngine { diff --git a/docs/spring-boot/modules/ROOT/pages/starters/mcp-server.adoc b/docs/spring-boot/modules/ROOT/pages/starters/mcp-server.adoc index bd465cb2b0b..73f5c698eb2 100644 --- a/docs/spring-boot/modules/ROOT/pages/starters/mcp-server.adoc +++ b/docs/spring-boot/modules/ROOT/pages/starters/mcp-server.adoc @@ -14,10 +14,40 @@ Tool semantics — tag-based opt-in (the untagged default pool is never exposed), flat-namespace collision refusal, per-call timeout and error sanitization — are owned by the runtime-agnostic `camel-mcp-server-api` bridge and are identical on every Camel runtime. Serving concerns (endpoint -path, protocol, server identity, authentication) are owned by the Spring AI -MCP server and configured via `spring.ai.mcp.server.*`; use +path, protocol, server identity) are owned by the Spring AI MCP server and +configured via `spring.ai.mcp.server.*`; use `spring.ai.mcp.server.protocol=STREAMABLE` for the streamable HTTP transport. +== Securing the MCP endpoint + +`spring.ai.mcp.server.*` provides **no authentication**. The MCP endpoint is +served on the application's own HTTP port, and anything that can reach it can +list and call every exposed tool. Protecting it is the application's +responsibility. + +Nothing is exposed until `camel.mcp-server.tags` is set — the untagged default +pool is never served — so the surface is opt-in. Once tags are configured, +secure the endpoint path, for example with a Spring Security filter chain: + +[source,java] +---- +@Bean +SecurityFilterChain mcpSecurity(HttpSecurity http) throws Exception { + return http.securityMatcher("/mcp/**") + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) + .build(); +} +---- + +Adjust the matcher to whatever `spring.ai.mcp.server` is configured to serve on. +A network policy that keeps the port off untrusted networks is an alternative +where the deployment allows it. + +See the camel-mcp-server component documentation for the trust boundary this +sits in: external MCP clients are untrusted senders under the Camel security +model. + == Maven coordinates [source,xml]