Repository navigation
Conversation
davsclaus
left a comment
There was a problem hiding this comment.
Thanks, this is a solid step towards parity. The engine follows the Vert.x engine almost line for line: resources, blobs, templates with the explicit remove notification, prompts, structured output and annotations. The Origin check reuses McpServerOrigins with the same loopback defaults as Camel Main. The identity is taken only from the servlet principal, and with no principal it fails closed. The Origin tests for POST, GET and DELETE are well done, including the one that shows a rejected request does not run the route.
Please address these before merging:
-
Generated files.
docs/spring-boot/modules/ROOT/pages/starters/mcp-server.adocis generated fromintro.adoc,usage.adocandsrc/main/docs/mcp-server.json, but the committed page does not match them:- It has no Resources, Prompts or custom-provider sections.
- It still has the old OAuth example that builds
resource_metadatafrom the request URL (UrlUtils.buildFullRequestUrl(request), page line 84). The newintro.adoc:124-129warns against exactly that. - The
enabledrow is the old one.
mcp-server.jsonis generated from the Javadoc. Its newenableddescription does not matchMcpServerConfigurationProperties.java:34-37, and itsallowed-originstext differs from that option's Javadoc. Could you update the Javadoc, rebuild the starter and commit the regenerated JSON and page? Otherwise the sync bot overwrites them after the merge. -
camel.mcp-server.enabled=false.CamelMcpServerTransportAutoConfiguration(lines 49-66) does not check this option, so the Camel transport still replaces the Spring AI one.@ConditionalOnProperty(value = "camel.mcp-server.enabled", matchIfMissing = true)fixes that. A few back-off tests withWebApplicationContextRunnerwould help too:- with
enabled=false, the Spring AI provider is the one used; - a
WebMvcStreamableServerTransportProviderthe user provides is the one actually used; allowed-origins=*fails the startup.
SpringBootMcpServerCustomTransportTestwould pass with the Camel bean too, since both install the same extractor. - with
-
Upgrade guide. The Spring AI default validator is
NOOP, so Spring Boot apps had no Origin check until now. After this PR, a browser client that is not on loopback gets a 403. The 4.24 upgrade guide in apache/camel (the entry on the Origin header check on/mcp) currently says the Spring Boot engine is not affected. Could you open a small companion PR there that mentionscamel.mcp-server.allowed-origins? -
Clashes with Spring-defined resources and prompts. The MCP SDK's
addResource,addResourceTemplateandaddPrompt(engine:166, 226, 255) silently replace an existing entry with the same key; they only log "Replace existing ...". When the Camel route stops, the Spring-defined@McpResourceor prompt is then removed for good. The engine SPI expectsMcpServerRefusedExceptionfor this case. Checkingserver.listResources(),listResourceTemplates()andlistPrompts()first, and throwing it, lets the bridge log a clear refusal. A check onlistTools()would close the same, older gap for tools.
Suggestions:
- Role format (
SpringAiMcpTransportContext.java:92-130):CamelMcpSecurityPrincipalRolesholds the raw Spring authorities. That meansROLE_*,SCOPE_*for a JWT, and with Spring Security 7.1 alsoFACTOR_*. On Camel Main, the roles are the unprefixedroles/realm_access.roles/groupsclaims, andoauthSubject/oauthScopes/oauthClaimsare filled in. So anAuthorizationPolicywritten for Main does not match on Spring Boot. At least document the format, with an example such asROLE_MCP_ADMIN(intro.adoc:150-155). Better still: stripROLE_, dropFACTOR_*, mapSCOPE_*intooauthScopes, and fill the OAuth fields for aJwtAuthenticationToken. The reflective lookups could also be cached instead of repeated on every request. - Notification test (
SpringBootMcpServerTest.java:682-719): the client opens its SSE stream asynchronously afterinitialize(), so the firststopRoutenotification can be dropped. Sending the notification again inside the await (for exampleMcpSyncServer.notifyToolsListChanged()on each poll) makes the test deterministic. Stoppingcustomer-template-routewould also cover the explicit notification inresourceTemplateRemoved. supportsResources()/supportsResourceTemplates()/supportsPrompts()always return true. They could returnserver.getServerCapabilities().resources()(orprompts())!= null. With a capability turned off, the bridge then logs its single "not served" warning instead of oneIllegalStateExceptionper route.- Host allow-list: Main has an opt-in
camel.server.mcp-allowed-hostsas a DNS-rebinding defence. A matchingcamel.mcp-server.allowed-hosts, built onMcpServerHostsand the SDK'sallowedHosts, would complete the parity. Fine as a follow-up. - Nits:
- Add a comment on why the deprecated
ServerTransportSecurityValidatoris needed: the Spring AI 2.0.2 builder accepts nothing else. intro.adoc:25-27says resources and prompts are "registered separately", butusage.adoc:83-85says they use the same tags.- Mark the
mcp.security.*names atintro.adoc:54-56as application properties, so they don't read as options of the starter. - The test-scoped
camel-ai-resource/camel-ai-promptentries in the pom are redundant:camel-mcp-server-apibrings them in. - The commit title needs a space after
CAMEL-26360:.
- Add a comment on why the deprecated
Claude Code on behalf of Claus Ibsen (davsclaus). This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.
|
Thanks @davsclaus for the review. I’ve addressed the feedback in commit 98e9611. The updates include:
Validation: the focused test suite passes (35 tests, 0 failures or errors), and the starter package build succeeds. I’ll raise a separate PR against the main Camel repository for the Camel 4.24 upgrade-guide update covering the Spring Boot MCP Streamable HTTP Origin validation behavior. |
Croway
left a comment
There was a problem hiding this comment.
Review of the Spring Boot MCP parity changes, checked against Camel main (camel-mcp-server-api), Spring AI 2.0.2 and MCP Java SDK 2.1.0. The findings come from reading the code; I did not run the tests.
The inline comments are roughly in priority order. The main concern is that raw JWT claims are merged into the caller's roles in SpringAiMcpTransportContext. Next come the Origin default change for apps with only Spring-defined MCP endpoints, and the startup failure caused by an invalid outputSchema.
Claude Code (Opus 5.5) on behalf of Federico Mariani
|
|
||
| addScopeClaim(scopes, claims.get("scope")); | ||
| addScopeClaim(scopes, claims.get("scp")); | ||
| addClaimRoles(claims, roles); |
There was a problem hiding this comment.
Raw JWT claims are merged into the caller's roles. addClaimRoles adds the roles / realm_access.roles / groups claims to the principal roles, on top of the authorities produced by the application's own JwtAuthenticationConverter. In a Spring app the converter is where the application decides which token claims become authorities, so this bypasses that decision.
Example: an app maps only client roles (resource_access.myapp.roles) or only scopes to authorities. A Keycloak token whose realm_access.roles contains admin (granted for a different application) still ends up with admin in McpToolCallContext.SECURITY_PRINCIPAL_ROLES_PROPERTY. A route AuthorizationPolicy that requires admin then lets the call through, although Spring Security never granted ROLE_admin.
Suggestion: on Spring Boot, derive roles only from Authentication.getAuthorities() and keep the raw claims available through the OAuth claims for routes that want them. If claim-based roles are needed for parity with Camel Main, make them opt-in and document the behaviour.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for highlighting this security concern. Fixed by deriving Camel roles and OAuth scopes exclusively from the authorities granted by Spring Security's configured authentication provider or JWT authority converter.
Raw token claims, including roles, realm_access.roles, and groups, are still propagated through McpToolCallContext as OAuth claims, but they are no longer promoted to Camel authorization roles.
This preserves the application's Spring Security authority-mapping policy and prevents claims that were not converted into granted authorities from bypassing Spring Security authorization decisions.
Added regression tests verifying that realm_access.roles, groups, and raw roles claims do not independently grant Camel roles.
|
|
||
| @Bean | ||
| @ConditionalOnMissingBean(WebMvcStreamableServerTransportProvider.class) | ||
| WebMvcStreamableServerTransportProvider webMvcStreamableServerTransportProvider( |
There was a problem hiding this comment.
This changes Origin handling for apps that don't expose any Camel capability. With the starter on the classpath and spring.ai.mcp.server.protocol=STREAMABLE, this bean replaces Spring AI's default transport, which in 2.0.2 (McpServerStreamableHttpWebMvcAutoConfiguration) sets no security validator. The replacement is active even when camel.mcp-server.tags is unset and Camel exposes nothing, so Spring-defined @McpTool / @McpResource endpoints suddenly accept only loopback origins.
Example: a 4.23 app serving Spring @McpTool methods to a browser client at https://app.example.com gets HTTP 403 Invalid Origin header after upgrading. The only ways out are camel.mcp-server.allowed-origins, or camel.mcp-server.enabled=false, which also turns off the Camel bridge.
Suggestion: add an upgrade-guide entry for this, and consider a dedicated switch for replacing the transport.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for raising this. Addressed the transport-replacement concern by adding the separate camel.mcp-server.transport-enabled property.
When unset, Camel replaces Spring AI's default transport only when Camel MCP tags are configured. Setting it to true enables Camel's transport even without Camel routes, while setting it to false preserves Spring AI's provider without disabling the Camel bridge.
I also documented the behavior in the starter documentation. I'm clarifying the Camel 4.24 upgrade-guide entry to explain the default activation condition and make clear that Spring-only MCP applications are not affected by transport replacement when no Camel tags are configured.
| .description(tool.description()); | ||
|
|
||
| if (tool.outputSchemaJson() != null && !tool.outputSchemaJson().isBlank()) { | ||
| builder.outputSchema(jsonMapper, tool.outputSchemaJson()); |
There was a problem hiding this comment.
An invalid outputSchema fails startup instead of refusing this one tool. server.addTool validates both schemas with jsonSchemaValidator.assertConforms (MCP Java SDK 2.1.0, McpAsyncServer#addTool) and fails with IllegalArgumentException. McpServerBridge only catches McpServerRefusedException around engine.toolAdded, so the exception propagates.
Example: ai-tool:x?outputSchema={"type":"obj"} passes the ai-tool check (it is a JSON object), then fails CamelContext / route startup instead of logging an error and skipping the tool.
Suggestion: catch IllegalArgumentException from server.addTool and rethrow it as McpServerRefusedException, plus a test with an invalid output schema.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for catching this. Fixed by catching IllegalArgumentException during tool registration, including the SDK's server.addTool() validation, and wrapping it in McpServerRefusedException with the tool name and schema error details.
This allows the MCP bridge to refuse the invalid tool registration without propagating the exception and failing Camel startup.
Added a regression test verifying that an invalid output-schema registration error is translated into McpServerRefusedException, preserving the original cause.
| @Override | ||
| public void resourceTemplateAdded(McpServerResourceTemplate resourceTemplate) { | ||
| if (server.listResourceTemplates().stream() | ||
| .anyMatch(existing -> existing.uriTemplate().equals(resourceTemplate.uriTemplate()))) { |
There was a problem hiding this comment.
Only identical template strings are refused, not overlapping ones. The engine contract (McpServerEngine#resourceTemplateAdded: "throws McpServerRefusedException to refuse an overlapping or otherwise unsupported template") asks for overlap detection. McpServerBridge checks overlap only among Camel's own templates, so a clash with a Spring-defined template gets through.
Example: Spring defines camel:///customers/{id} and a Camel route defines camel:///customers/{customerId}. Both are registered. The SDK keeps templates in a ConcurrentHashMap and findResourceTemplateSpecification returns the first match, so which handler serves resources/read camel:///customers/42 is not defined.
Suggestion: refuse the template when it overlaps any template already registered on the server, using the same overlap rule the bridge applies to Camel templates.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for pointing this out. Fixed by checking each incoming resource template against all templates already registered with the Spring AI server, rather than checking only for identical template strings.
The implementation uses the same underlying AiResourceUriTemplate.overlaps() matching logic as Camel's McpServerBridge. Overlapping templates are rejected with McpServerRefusedException. It also fails closed if an existing Spring-defined template cannot be parsed safely.
Added a regression test covering the exact scenario from the review: camel:///customers/{id} already registered by Spring and camel:///customers/{customerId} being added by a Camel route. The test verifies that registration is refused and the SDK server is not modified.
| @ConditionalOnProperty( | ||
| prefix = "camel.mcp-server", | ||
| name = "enabled", | ||
| havingValue = "true", |
There was a problem hiding this comment.
havingValue = "true" doesn't match how CamelMcpServerAutoConfiguration reads the same property. That class uses @ConditionalOnProperty(value = "camel.mcp-server.enabled", matchIfMissing = true), which matches any value other than false. Here only true matches.
Example: with camel.mcp-server.enabled=on (which Spring binds to true), the bridge is active but this transport is not installed. There is no Origin validation, and every tool call gets McpToolCallContext.EMPTY, so role-based AuthorizationPolicy routes deny every caller. Nothing is logged.
Suggestion: drop havingValue so both auto-configurations use the same condition.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for catching this. Fixed by removing the restrictive havingValue = "true" condition so the transport auto-configuration uses the same matchIfMissing = true semantics as the MCP bridge.
I also added a regression test using camel.mcp-server.enabled=on to verify that both the bridge and transport are activated and that the authenticated caller context is propagated correctly.
| "org.springframework.security.core.Authentication"; | ||
| private static final String GRANTED_AUTHORITY_CLASS = | ||
| "org.springframework.security.core.GrantedAuthority"; | ||
| private static final String JWT_AUTHENTICATION_CLASS = |
There was a problem hiding this comment.
Opaque-token callers get no subject or claims. Claims are read only for JwtAuthenticationToken, but getTokenAttributes() is declared on AbstractOAuth2TokenAuthenticationToken, which BearerTokenAuthentication (token introspection, oauth2ResourceServer(o -> o.opaqueToken(...))) also extends. With opaque tokens the OAuth subject and claims are never set, so a route policy based on them denies every caller.
Suggestion: look up getTokenAttributes on AbstractOAuth2TokenAuthenticationToken instead.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for catching this. Fixed by resolving getTokenAttributes() from AbstractOAuth2TokenAuthenticationToken rather than relying on JwtAuthenticationToken specifically. This supports both JWT and opaque bearer-token authentication.
I also added a regression test using BearerTokenAuthentication to verify that the OAuth subject, token attributes, roles, and scopes are correctly propagated into the Camel tool call context.
| // Spring AI 2.0.2 exposes only this deprecated validator type on the | ||
| // WebMVC transport builder. | ||
| @SuppressWarnings("deprecation") | ||
| private static ServerTransportSecurityValidator createOriginValidator(String configuredAllowedOrigins) { |
There was a problem hiding this comment.
Apps with a custom transport can't reuse Camel's Origin policy. The docs ask apps that define their own WebMvcStreamableServerTransportProvider to install "an Origin validator equivalent to, or stricter than" this one, but createOriginValidator and DEFAULT_ALLOWED_ORIGINS are private, so they have to re-implement the normalization and the loopback defaults. SpringBootMcpServerCustomTransportTest already differs: it uses DefaultServerTransportSecurityValidator directly, without the McpServerOrigins normalization.
Similarly, SpringAiMcpTransportContext.CALL_CONTEXT_KEY is package-private, so an app that needs its own transport-context entries can't combine them with Camel's identity entry.
Suggestion: expose the validator factory and defaults as public API, and offer a way to compose the context extractor with an application's own entries.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for pointing this out. Addressed this by exposing the reusable Origin validation API and the default allowed-origin configuration. Both the current MCP SDK header validator and the Spring AI 2.0.2 compatibility adapter use Camel's shared Origin parsing and normalization.
I also made SpringAiMcpTransportContext.CALL_CONTEXT_KEY public and added an overload of extract() that composes Camel's authenticated caller context with application-defined transport-context entries, while preventing applications from overriding Camel's reserved entry.
Updated the custom transport integration test to use these public helpers and added coverage for context composition. The custom transport documentation now demonstrates how to reuse both helpers.
| } | ||
| } | ||
|
|
||
| private record AuthenticationAccessors( |
There was a problem hiding this comment.
Minor: the Conventions section of the repository CLAUDE.md says "Do NOT use Records or Lombok (unless already present in the file)". This file is new, so AuthenticationAccessors could be a small final class instead.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for noting the repository convention. Replaced the AuthenticationAccessors record with a private static final class, keeping the same accessor behavior without introducing a record into the new file.
| * Whether to expose ai-tool routes as MCP tools through the Spring AI MCP server. Enabled by default when the | ||
| * starter is on the classpath. | ||
| * Whether to enable Camel MCP integration for ai-tool, ai-resource, and ai-prompt routes through the Spring AI | ||
| * MCP server. Enabled by default when the starter is on the classpath. |
There was a problem hiding this comment.
The enabled description was updated, but the tags javadoc just below (lines 40-41) still says "Comma-separated list of ai-tool tags to expose as MCP tools ... When not set, no tools are exposed." Tags now also select ai-resource and ai-prompt routes. Updating that javadoc and regenerating also fixes mcp-server.json and the options table in mcp-server.adoc.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for pointing this out. Updated the tags Javadoc to clarify that tags select ai-tool, ai-resource, and ai-prompt routes for exposure through the Spring AI MCP server.
I also regenerated the MCP configuration metadata and starter documentation so the JSON metadata and options table reflect the updated description.
| ---- | ||
| from("ai-resource:app_config?resourceUri=camel:///config/app.json" | ||
| + "&tags=crm&description=Application configuration&mimeType=application/json") | ||
| .setBody(constant("{\\"env\\":\\"production\\"}")); |
There was a problem hiding this comment.
AsciiDoc listing blocks show \\" literally, so this example (and the one at line 264) doesn't compile when copied: the Java string ends right after the first \\. These should be single-backslash escapes, for example constant("{\"env\":\"production\"}").
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Thanks for catching this. Fixed both the resource and resource-template examples to use normal Java string escaping (a single backslash before embedded quotes).
I corrected the source usage document and regenerated the starter documentation page. I also verified that both examples in the generated AsciiDoc are copy-pasteable Java code.
The focused MCP test suite passes (45 tests, zero failures or errors), and the starter package build succeeds.
|
Thanks @Croway for the detailed review. I've addressed the outstanding feedback in the latest update. The changes include Spring Security authority-only role mapping, explicit transport activation controls, invalid schema handling, resource-template overlap detection, notification capability guards, resource and prompt timeout configuration, opaque-token context extraction, and reusable Origin/context helpers. I've also corrected the documentation examples, updated generated starter metadata, and addressed the remaining code convention feedback. Validation: 45 focused tests pass with zero failures or errors, and the starter package build succeeds. |
Croway
left a comment
There was a problem hiding this comment.
Follow-up on the latest commit: the earlier comments look addressed. One new issue on the resource-template overlap check, inline.
Claude Code (Opus 5.5) on behalf of Federico Mariani
| try { | ||
| existingMatcher = new AiResourceUriTemplate(existing.uriTemplate()); | ||
| } catch (IllegalArgumentException e) { | ||
| // Fail closed: Camel cannot prove that an unsupported Spring template does not overlap. |
There was a problem hiding this comment.
A single Spring-defined template that Camel can't parse blocks every Camel resource template. The overlap check fails closed whenever an existing template is rejected by AiResourceUriTemplate. That parser intentionally accepts path-segment variables only, so it rejects the style Spring AI documents for @McpResource, where the variable follows the scheme directly: document://{id}, async-data://{id} and static://{path} in the Spring AI 2.0.2 mcp-annotations-examples.adoc all fail with "Resource uriTemplate must contain a path". Templates with query expressions such as {?q} are rejected too.
Example: an app defines @McpResource(uri = "document://{id}") and a Camel route defines ai-resource:customer?uriTemplate=camel:///customers/{customerId}. The two can never match the same URI because the schemes differ, but the Camel template is refused. So is every other Camel resource template in the app, as long as that one Spring template is registered.
Suggestion: only fail closed when the unparseable template has the same scheme as the candidate (case-insensitive). Templates with a different scheme can't overlap, so log a WARN and continue. A test with a Spring document://{id} template next to a Camel camel:///... template would cover it.
Claude Code (Opus 5.5) on behalf of Federico Mariani
There was a problem hiding this comment.
Addressed in commit d75d65d. Added URI scheme comparison, WARN logging for different schemes, and regression tests for different and matching schemes. All 6 SpringAiMcpServerEngineTest tests pass. git diff --check passes.
CAMEL-26360: Add MCP Server parity to Spring Boot
Description
This PR brings the Spring Boot MCP Server engine closer to feature parity with the Camel Main MCP Server engine by integrating Camel MCP server capabilities with Spring AI's MCP Streamable HTTP transport.
It enables Camel MCP server routes to participate in the Spring Boot MCP server alongside Spring-defined MCP tools, while preserving caller identity and supporting Spring Security integration.
Key changes
1. Spring Boot MCP transport integration
STREAMABLEprotocol.2. Caller identity and Spring Security integration
McpToolCallContext.SpringAiMcpTransportContextutility for custom transport providers.AuthorizationPolicyusing the propagated caller roles.3. MCP server capabilities
4. Origin validation and transport security
5. Documentation
ai-tool,ai-resource, andai-prompt.Testing
The following focused test suite completed successfully:
mvn -pl components-starter/camel-mcp-server-starter -am \ -Dtest=SpringBootMcpServerTest,SpringBootMcpServerOriginConfigurationTest,SpringBootMcpServerCustomTransportTest \ -Dsurefire.failIfNoSpecifiedTests=false \ testAdditional focused verification:
testAuthorizationPolicyAllowsOnlyMcpAdminRole— passed.testResourceAndPromptChangeNotificationsAreDelivered— passed independently.Scope
This PR focuses on the Spring Boot MCP Server engine.
Quarkus parity is intentionally deferred until the Spring Boot integration and common API changes have been reviewed.