CAMEL-24218: Add Security-First execution layer to Camel JBang MCP Server - #25020
Conversation
…rver Adds an opt-in security execution layer for the MCP server with: - Tool-level authorization (read-only/read-write/admin access levels) derived from existing MCP tool annotations (readOnlyHint/destructiveHint) - Structured audit logging of tool invocations via dedicated logger category - Input sanitization (argument length limits, control character stripping) - Secret redaction in tool responses (passwords, API keys, tokens, AWS keys, connection strings) - Configuration via Quarkus properties (camel.mcp.security.*) Uses Quarkus MCP Server 1.13.1 guardrail/filter APIs: - McpAccessFilter (ToolFilter) hides tools from tools/list - McpAccessGuardrail (ToolInputGuardrail) enforces authorization - McpOutputGuardrail (ToolOutputGuardrail) redacts secrets All features disabled by default for backward compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
gnodet
left a comment
There was a problem hiding this comment.
Well-designed security layer — defense-in-depth with both filter (hides tools from discovery) and guardrail (blocks execution even if filter bypassed), all disabled by default for backward compatibility, clean CDI architecture using Quarkus MCP Server guardrail APIs.
What's good:
- Three-tier access model (read-only / read-write / admin) derived from existing MCP tool annotations — no separate ACL needed
- Input sanitization (control char stripping, argument length limits) and output secret redaction are orthogonal concerns properly separated into distinct CDI beans
- Structured JSON audit logging to a dedicated logger category (
org.apache.camel.mcp.security.audit) — operators can route it independently - Good test coverage: 5 test files covering config, access filter, guardrail, audit logger, and secret redactor
AccessLevel.parse()is lenient (acceptsread-only,readonly,READ_ONLY) — good UX for config
Issues to address:
-
Binary test file —
McpAccessGuardrailTest.javacontains a literal null byte (0x00) in thestripControlCharsRemovesNullBytes()test string. This causes git to treat the entire file as binary, making it invisible to GitHub diff/review. Fix: use Java escape sequences instead —"hello\0world"for the null byte,"�"for the bell character in the other test. Same test coverage, reviewable file. -
Missing
McpOutputGuardrailTest— there's no test for the output guardrail, which handles secret redaction on tool responses and audit trail logging for results. TheMcpSecretRedactorTestcovers pattern matching, but the output guardrail's integration logic (iteratingContentitems, replacingTextContent, constructing newToolResponse) is untested.
Minor observations (not blocking):
McpOutputGuardrail.logToolResultalways passesdurationMs=0since the output guardrail has no access to when the tool started. Consider either omitting the field when unavailable, or documenting that it's only populated when a timing wrapper is present.- The
escape()method inMcpAuditLoggerhandles\,",\n,\r,\tbut not\b(U+0008) or\f(U+000C). Fine for practical purposes, but a dedicated JSON writer (Jackson/Vert.xJsonObject) would be more robust. - Custom redaction patterns from config (
camel.mcp.security.redaction.patterns) are compiled without any safeguard against pathological regexes. Consider adding a compile-time timeout or a brief note in docs about pattern complexity.
Checklist:
- Code quality — clean, well-separated concerns, proper CDI usage
- Security model — defense-in-depth (filter + guardrail), safe defaults (disabled, admin level)
- Backward compatible — all features opt-in, disabled by default
- No Lombok, no FQCNs, no Thread.sleep()
- Commit convention —
CAMEL-24218:prefix - Binary test file needs fix (literal null byte → escape sequence)
- Missing output guardrail test
- CI — pending
Reviewed with Claude Code on behalf of gnodet. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.
davsclaus
left a comment
There was a problem hiding this comment.
Thank you for the contribution! The security layer concept is well-designed — clean separation of concerns, safe defaults (disabled by default), and good test coverage for individual components. I appreciate the defense-in-depth approach with both filter and guardrail layers.
However, I found a critical issue with the guardrail wiring that prevents most of the security features from working at runtime. See inline comments for details.
Summary of findings (ordered by severity):
-
Critical — Guardrails never fire at runtime —
ToolInputGuardrailandToolOutputGuardrailrequire per-tool@ToolGuardrailsannotation (or programmaticToolManagerregistration) in the Quarkus MCP Server API. UnlikeToolFilter, they are NOT auto-discovered as global CDI beans. The upstream Quarkus MCP Server source and tests confirm this — every@Tooluses explicit@ToolGuardrails(input=..., output=...). This renders input sanitization, audit logging, and secret redaction non-functional. -
Medium — Binary test file —
McpAccessGuardrailTest.javacontains literal null bytes, causing Git to treat it as binary. The file is invisible in GitHub's diff view. Use Java escape sequences ("\0") instead of literal null bytes. -
Medium — Missing
McpOutputGuardrailTest— No test for the output guardrail's integration logic. -
Low — No documentation — User-facing config properties (
camel.mcp.security.*) need a documentation page. -
Low —
durationMs=0always — Output guardrail has no access to timing info; consider omitting the field or documenting this limitation.
This review does not replace specialized tools such as CodeRabbit, Sourcery, or SonarCloud.
This review was generated by an AI agent (Claude Code on behalf of davsclaus) and may contain inaccuracies. Please verify all suggestions before applying.
| * tool execution. | ||
| */ | ||
| @ApplicationScoped | ||
| public class McpAccessGuardrail implements ToolInputGuardrail { |
There was a problem hiding this comment.
Critical: This guardrail will never be invoked at runtime.
In the Quarkus MCP Server API, ToolFilter is auto-discovered as a global CDI bean (your McpAccessFilter works correctly), but ToolInputGuardrail requires explicit per-tool association via @ToolGuardrails(input = McpAccessGuardrail.class) on each @Tool method, or programmatic registration via ToolManager.setInputGuardrails().
I verified this against the upstream source and upstream tests — every tool that uses guardrails has an explicit @ToolGuardrails annotation.
Without this wiring, the input sanitization, authorization defense-in-depth, and audit logging handled by this class are completely non-functional. The unit tests pass because they test the logic directly, bypassing the Quarkus CDI/MCP wiring.
There are 266 @Tool methods in camel-jbang-mcp and zero @ToolGuardrails annotations. Options to fix:
- Add
@ToolGuardrailsto every@Toolmethod (high churn) - Move this logic into the
ToolFilter(which IS global) or a Vert.x/CDI interceptor - Check with the Quarkus MCP Server team if global guardrail support is planned
There was a problem hiding this comment.
Fixed — both guardrails were replaced with a CDI interceptor in the previous commit. The wasRedacted tracking bug is also fixed in the latest push.
Claude Code on behalf of oscerd
| * results. | ||
| */ | ||
| @ApplicationScoped | ||
| public class McpOutputGuardrail implements ToolOutputGuardrail { |
There was a problem hiding this comment.
Same issue as McpAccessGuardrail — this ToolOutputGuardrail implementation will never be invoked at runtime without per-tool @ToolGuardrails(output = McpOutputGuardrail.class) annotation on each @Tool method.
This means secret redaction on tool responses and audit logging of tool results are non-functional.
Also, durationMs is always 0 (line 86) since the output guardrail has no timing context — consider either omitting this field or documenting the limitation.
There was a problem hiding this comment.
Fixed — output guardrail was removed and replaced by the CDI interceptor. Duration is now tracked via System.nanoTime() and the redacted flag is properly set when redaction occurs.
Claude Code on behalf of oscerd
| Instant.now().toString()); | ||
| } | ||
|
|
||
| static String escape(String value) { |
There was a problem hiding this comment.
Minor: this hand-rolled JSON escaping handles \, ", \n, \r, \t but misses \b (U+0008) and \f (U+000C), which are also required JSON escapes per RFC 8259. Consider using Jackson's JsonStringEncoder or Vert.x JsonObject for more robust output — both are already on the classpath.
There was a problem hiding this comment.
Fixed — escape() now handles \\b (U+0008) and \\f (U+000C). Kept the hand-rolled approach since only two extra replacements were needed and both Jackson/Vert.x are heavier for this use case.
Claude Code on behalf of oscerd
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
|
🧪 CI tested the following changed modules:
🔬 Scalpel shadow comparison — Scalpel: 1 tested, 0 compile-only — current: 1 all testedMaveniverse Scalpel detected 1 affected modules (current approach: 1). Skip-tests mode would test 1 modules (1 direct + 0 downstream), skip tests for 0 (generated code, meta-modules) Modules Scalpel would test (1)
|
…terceptor Fixes the critical issue where ToolInputGuardrail/ToolOutputGuardrail were never invoked at runtime (they require per-tool @ToolGuardrails annotation, not auto-discovered as global CDI beans). Changes: - Replace McpAccessGuardrail and McpOutputGuardrail with McpSecurityInterceptor (CDI @AroundInvoke interceptor bound via @McpSecured annotation) - Add @McpSecured to all 19 tool classes for automatic interception - Create McpSecured interceptor binding annotation - Fix binary test file: use \0 escape sequence instead of literal null byte - Fix McpAuditLogger.escape() to handle \b and \f per RFC 8259 - Rename test to McpSecurityInterceptorTest Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
gnodet
left a comment
There was a problem hiding this comment.
LGTM — well-designed security layer for the Camel MCP server. The architecture uses McpAccessFilter (implementing ToolFilter, auto-discovered as a global CDI bean) for tool visibility filtering, and McpSecurityInterceptor (CDI @AroundInvoke bound via @McpSecured) for input sanitization, audit logging, and secret redaction. This is a clean alternative to the per-tool @ToolGuardrails approach.
Key observations:
- Comprehensive test coverage across 5 test files
- Proper
System.nanoTime()duration measurement in the interceptor - RFC 8259-compliant JSON escaping (including
\band\f) - Operator-controlled redaction patterns align with Camel's trust model
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of @gnodet
gnodet
left a comment
There was a problem hiding this comment.
Well-structured opt-in security layer for the MCP server — clean separation of concerns (filter, interceptor, config, audit, redaction) and the disabled-by-default approach is the right call for backward compatibility.
A few issues and observations below. None are blocking since the feature is entirely opt-in, but items marked with
Missing documentation: The PR adds 7 new user-facing configuration properties (camel.mcp.security.*) and a new security layer with annotations on all 19 tool classes. Per project conventions, user-visible changes should be documented in the component docs and mentioned in the upgrade guide for discoverability.
Minor config robustness: getMaxArgumentLength() silently treats negative values as the default (10000) without logging — could cause confusion during troubleshooting. getRedactionPatterns() compiles user-provided regexes without a try-catch for PatternSyntaxException — a malformed pattern would crash with an unhelpful stack trace on first tool invocation.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
| } | ||
|
|
||
| static String stripControlChars(String input) { | ||
| if (input == null) { |
There was a problem hiding this comment.
redacted parameter is always hardcoded to false here, but redaction can actually occur at lines 117-120 above. When redactor.containsSecret(s) is true and redactor.redact(s) is called, this audit entry should report redacted: true.
Consider tracking it as a local variable:
boolean wasRedacted = false;
// ... in try block:
if (config.isRedactionEnabled() && result instanceof String s) {
if (redactor.containsSecret(s)) {
result = redactor.redact(s);
wasRedacted = true;
}
}
// ... in finally block:
auditLogger.logToolResult(toolName, "", isError, wasRedacted, durationMs);There was a problem hiding this comment.
Fixed — introduced wasRedacted variable that's set to true when redaction occurs, and passed to the audit logger in the finally block. Good catch!
Claude Code on behalf of oscerd
| String tool, String connectionId, String clientName, | ||
| String accessLevel, String arguments) { | ||
| AUDIT_LOG.infof( | ||
| "{\"event\":\"tool_call\",\"tool\":\"%s\",\"connectionId\":\"%s\"," |
There was a problem hiding this comment.
logAccessDenied() is defined and unit-tested but never called in any production code path. When McpAccessFilter denies a tool, the framework itself handles the rejection — but no audit log entry is written for denied attempts.
This means denied access attempts are invisible in the audit trail, which undermines the security audit story. Consider calling this from McpAccessFilter.test() when it returns false, or removing it if execution-time denial auditing is out of scope for this PR.
There was a problem hiding this comment.
Fixed — logAccessDenied() is now called from McpAccessFilter.test() when a tool is filtered out due to insufficient access level. Also attempts to extract connectionId and clientName from the FilterContext (with fallback to empty strings if not available).
Claude Code on behalf of oscerd
| if (params == null) { | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
Minor: connectionId and clientName are always empty strings here. These are key forensic fields in the audit schema. If the MCP connection context provides this info (e.g., via InvocationContext or request scope), it would strengthen the audit trail. If not currently available, a TODO comment noting the limitation would help future contributors.
There was a problem hiding this comment.
Added a TODO comment documenting this limitation. CDI interceptors don't have access to the MCP connection context — these fields will be populated once the Quarkus MCP Server supports global guardrails. Meanwhile, McpAccessFilter now extracts connection context where available.
Claude Code on behalf of oscerd
…est file - Fix redacted flag always false in audit log (track wasRedacted variable) - Wire logAccessDenied() in McpAccessFilter for denied tool audit trail - Add PatternSyntaxException handling for custom redaction patterns - Replace literal control chars in test file with Java Unicode escapes - Add TODO for CDI interceptor connection context limitation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
39ec6d0 to
492dd43
Compare
gnodet
left a comment
There was a problem hiding this comment.
Re-reviewed after the latest commits. The author addressed the key findings from the previous review:
- ✅
wasRedactedflag now properly tracked and passed to audit logger - ✅
logAccessDeniedno longer dead code — called fromMcpAccessFilter.test() - ✅
PatternSyntaxExceptionhandling added ingetRedactionPatterns() - ✅ Binary test file issue resolved
- ✅ davsclaus's guardrails concern addressed — replaced with CDI
@AroundInvokeinterceptor
Two low-severity items remain (silent maxArgumentLength fallback, missing .adoc documentation) but are non-blocking since the feature is opt-in and disabled by default.
This review was generated by an AI agent (Claude Code) and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
Summary
Adds an opt-in security execution layer for the Camel JBang MCP Server that provides:
readOnlyHint/destructiveHint) — no separate ACL neededorg.apache.camel.mcp.security.audit@ConfigPropertywithcamel.mcp.security.*prefixArchitecture
Uses a CDI interceptor (
@McpSecured) for input sanitization, audit logging, and secret redaction, plus a globalToolFilterfor tool visibility control:McpAccessFilterToolFilter(global CDI bean)tools/list+ audit logs denied accessMcpSecurityInterceptor@AroundInvokeCDI interceptorMcpSecured@InterceptorBindingMcpSecurityConfigPatternSyntaxExceptionhandlingMcpAuditLoggerMcpSecretRedactorAll features are disabled by default (
camel.mcp.security.enabled=false) to preserve backward compatibility.Access Level Model
read-onlyreadOnlyHint=trueonlyread-writedestructiveHint=falseadmin(default)Test plan
McpSecurityConfigTest— AccessLevel.permits() logic (9 combinations), defaults, custom patternsMcpAccessFilterTest— Tool visibility at each access levelMcpSecretRedactorTest— All redaction patterns, false positives, null/empty inputMcpAuditLoggerTest— JSON escaping (including\b/\f), logger instantiationMcpSecurityInterceptorTest— Input sanitization (control chars via Unicode escapes, null, empty)Claude Code on behalf of oscerd
🤖 Generated with Claude Code