This is an automated email from the ASF dual-hosted git repository.
oscerd 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 2b5be619f9fe CAMEL-24218: Add Security-First execution layer to Camel
JBang MCP Server (#25020)
2b5be619f9fe is described below
commit 2b5be619f9fe233e91f67098f996267bae16c518
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Jul 23 15:20:36 2026 +0200
CAMEL-24218: Add Security-First execution layer to Camel JBang MCP Server
(#25020)
* CAMEL-24218: Add Security-First execution layer to Camel JBang MCP Server
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) <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24218: Address review feedback - replace guardrails with CDI
interceptor
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) <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24218: Address review feedback - fix audit logging and binary test
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) <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
.../dsl/jbang/core/commands/mcp/AdvisoryTools.java | 1 +
.../dsl/jbang/core/commands/mcp/CatalogTools.java | 1 +
.../commands/mcp/ComponentPropertiesTools.java | 1 +
.../commands/mcp/ConfigurationValidateTools.java | 1 +
.../core/commands/mcp/DependencyCheckTools.java | 1 +
.../dsl/jbang/core/commands/mcp/DiagnoseTools.java | 1 +
.../dsl/jbang/core/commands/mcp/ExampleTools.java | 1 +
.../dsl/jbang/core/commands/mcp/ExplainTools.java | 1 +
.../dsl/jbang/core/commands/mcp/HardenTools.java | 1 +
.../dsl/jbang/core/commands/mcp/KameletTools.java | 1 +
.../jbang/core/commands/mcp/McpAccessFilter.java | 68 ++++++++
.../jbang/core/commands/mcp/McpAuditLogger.java | 86 ++++++++++
.../jbang/core/commands/mcp/McpSecretRedactor.java | 75 +++++++++
.../dsl/jbang/core/commands/mcp/McpSecured.java | 38 +++++
.../jbang/core/commands/mcp/McpSecurityConfig.java | 141 +++++++++++++++++
.../core/commands/mcp/McpSecurityInterceptor.java | 175 +++++++++++++++++++++
.../jbang/core/commands/mcp/MigrationTools.java | 1 +
.../commands/mcp/MigrationWildflyKarafTools.java | 1 +
.../dsl/jbang/core/commands/mcp/OpenApiTools.java | 1 +
.../commands/mcp/PropertiesTranslateTools.java | 1 +
.../jbang/core/commands/mcp/RouteDiagramTools.java | 1 +
.../dsl/jbang/core/commands/mcp/RuntimeTools.java | 1 +
.../jbang/core/commands/mcp/TestScaffoldTools.java | 1 +
.../jbang/core/commands/mcp/TransformTools.java | 1 +
.../dsl/jbang/core/commands/mcp/VersionTools.java | 1 +
.../src/main/resources/application.properties | 14 ++
.../core/commands/mcp/McpAccessFilterTest.java | 76 +++++++++
.../core/commands/mcp/McpAuditLoggerTest.java | 55 +++++++
.../core/commands/mcp/McpSecretRedactorTest.java | 136 ++++++++++++++++
.../core/commands/mcp/McpSecurityConfigTest.java | 134 ++++++++++++++++
.../commands/mcp/McpSecurityInterceptorTest.java | 67 ++++++++
31 files changed, 1084 insertions(+)
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
index 454873b1942d..1fd76914592c 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
@@ -33,6 +33,7 @@ import org.apache.camel.tooling.model.SecurityAdvisoryModel;
* Lets an LLM answer questions such as "is my Camel 4.10.1 project affected
by known CVEs?" or "which CVEs were
* published for camel-kafka and in which versions are they fixed?".
*/
+@McpSecured
@ApplicationScoped
public class AdvisoryTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
index 6358023f74ae..22298f6ee71f 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
@@ -37,6 +37,7 @@ import org.apache.camel.tooling.model.LanguageModel;
/**
* MCP Tools for querying the Camel Catalog using Quarkus MCP Server.
*/
+@McpSecured
@ApplicationScoped
public class CatalogTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ComponentPropertiesTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ComponentPropertiesTools.java
index 596c8bca1403..3b14f515aee2 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ComponentPropertiesTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ComponentPropertiesTools.java
@@ -33,6 +33,7 @@ import org.apache.camel.tooling.model.ComponentModel;
* MCP Tool that lists valid {@code application.properties} keys for a Camel
component, so AI agents can build
* configuration without having to parse the full component documentation.
*/
+@McpSecured
@ApplicationScoped
public class ComponentPropertiesTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java
index d575e5a84e41..8bff50d125c9 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java
@@ -36,6 +36,7 @@ import
org.apache.camel.catalog.ConfigurationPropertiesValidationResult;
* Boot or Quarkus). Wraps {@link
CamelCatalog#validateConfigurationProperty(String)} so that misspelled option
names,
* invalid values and suggested corrections can be surfaced to LLMs.
*/
+@McpSecured
@ApplicationScoped
public class ConfigurationValidateTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
index d9fbfda06a1c..bc5b92da8616 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
@@ -34,6 +34,7 @@ import org.apache.camel.tooling.model.ComponentModel;
* Analyzes a project's pom.xml (and optionally route definitions) to detect
outdated Camel dependencies, missing
* dependencies for components used in routes, and version conflicts between
the Camel BOM and explicit overrides.
*/
+@McpSecured
@ApplicationScoped
public class DependencyCheckTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
index 6a90580c87b8..6a473a662c5d 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
@@ -37,6 +37,7 @@ import org.apache.camel.tooling.model.EipModel;
* Accepts a Camel stack trace or error message and returns the likely
component/EIP involved, common causes, links to
* relevant documentation, and suggested fixes.
*/
+@McpSecured
@ApplicationScoped
public class DiagnoseTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
index 9f1410275d2c..c2c2d3d5c6e6 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
@@ -33,6 +33,7 @@ import org.apache.camel.util.json.JsonObject;
/**
* MCP Tools for browsing Camel CLI examples.
*/
+@McpSecured
@ApplicationScoped
public class ExampleTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
index db3b1bce230f..46cbe959d89d 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
@@ -35,6 +35,7 @@ import org.apache.camel.tooling.model.EipModel;
* This tool extracts components and EIPs used in a route and returns their
documentation from the Camel Catalog. The
* calling LLM can use this context to formulate its own explanation of the
route.
*/
+@McpSecured
@ApplicationScoped
public class ExplainTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
index ef7768dcc4fa..7f7bd5213ec1 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
@@ -39,6 +39,7 @@ import org.apache.camel.tooling.model.SecurityAdvisoryModel;
* This tool analyzes routes for security-sensitive components, identifies
potential vulnerabilities, and provides
* structured context that an LLM can use to formulate security hardening
recommendations.
*/
+@McpSecured
@ApplicationScoped
public class HardenTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
index 0610376a1156..3167220bf593 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
@@ -34,6 +34,7 @@ import org.apache.camel.dsl.jbang.core.common.VersionHelper;
/**
* MCP Tools for querying the Kamelet Catalog using Quarkus MCP Server.
*/
+@McpSecured
@ApplicationScoped
public class KameletTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAccessFilter.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAccessFilter.java
new file mode 100644
index 000000000000..542b46ee2532
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAccessFilter.java
@@ -0,0 +1,68 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import io.quarkiverse.mcp.server.FilterContext;
+import io.quarkiverse.mcp.server.ToolFilter;
+import io.quarkiverse.mcp.server.ToolManager;
+
+/**
+ * Filters tool visibility in {@code tools/list} responses based on the
configured access level.
+ * <p>
+ * When security is enabled, tools whose access tier exceeds the configured
level are hidden from discovery. This
+ * prevents LLM clients from attempting to call tools they cannot execute.
+ */
+@ApplicationScoped
+public class McpAccessFilter implements ToolFilter {
+
+ @Inject
+ McpSecurityConfig config;
+
+ @Inject
+ McpAuditLogger auditLogger;
+
+ @Override
+ public boolean test(ToolManager.ToolInfo tool, FilterContext context) {
+ if (!config.isEnabled()) {
+ return true;
+ }
+
+ boolean readOnlyHint =
tool.annotations().map(ToolManager.ToolAnnotations::readOnlyHint).orElse(true);
+ boolean destructiveHint =
tool.annotations().map(ToolManager.ToolAnnotations::destructiveHint).orElse(false);
+ boolean permitted = config.getAccessLevel().permits(readOnlyHint,
destructiveHint);
+
+ if (!permitted && config.isAuditEnabled()) {
+ String requiredLevel = destructiveHint ? "admin" : (!readOnlyHint
? "read-write" : "read-only");
+ String connectionId = "";
+ String clientName = "";
+ try {
+ connectionId = context.connection().id();
+ clientName =
context.connection().initialRequest().implementation().name();
+ } catch (Exception e) {
+ // connection context may not be fully available during
filtering
+ }
+ auditLogger.logAccessDenied(
+ tool.name(), connectionId, clientName,
+ requiredLevel,
config.getAccessLevel().name().toLowerCase().replace("_", "-"));
+ }
+
+ return permitted;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAuditLogger.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAuditLogger.java
new file mode 100644
index 000000000000..6e85fdb92a8e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAuditLogger.java
@@ -0,0 +1,86 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.time.Instant;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import org.jboss.logging.Logger;
+
+/**
+ * Structured audit logger for MCP tool invocations.
+ * <p>
+ * Produces single-line JSON log entries to a dedicated logger category,
allowing operators to route audit logs
+ * independently via Quarkus logging configuration.
+ */
+@ApplicationScoped
+public class McpAuditLogger {
+
+ static final String AUDIT_LOGGER_NAME =
"org.apache.camel.mcp.security.audit";
+
+ private static final Logger AUDIT_LOG =
Logger.getLogger(AUDIT_LOGGER_NAME);
+
+ public void logToolCall(
+ String tool, String connectionId, String clientName,
+ String accessLevel, String arguments) {
+ AUDIT_LOG.infof(
+
"{\"event\":\"tool_call\",\"tool\":\"%s\",\"connectionId\":\"%s\","
+ +
"\"clientName\":\"%s\",\"accessLevel\":\"%s\",\"arguments\":%s,"
+ + "\"timestamp\":\"%s\"}",
+ escape(tool), escape(connectionId), escape(clientName),
+ escape(accessLevel), arguments != null ? "\"" +
escape(arguments) + "\"" : "null",
+ Instant.now().toString());
+ }
+
+ public void logToolResult(
+ String tool, String connectionId, boolean isError,
+ boolean redacted, long durationMs) {
+ AUDIT_LOG.infof(
+
"{\"event\":\"tool_result\",\"tool\":\"%s\",\"connectionId\":\"%s\","
+ +
"\"outcome\":\"%s\",\"redacted\":%s,\"durationMs\":%d,"
+ + "\"timestamp\":\"%s\"}",
+ escape(tool), escape(connectionId),
+ isError ? "error" : "success", redacted, durationMs,
+ Instant.now().toString());
+ }
+
+ public void logAccessDenied(
+ String tool, String connectionId, String clientName,
+ String requiredLevel, String currentLevel) {
+ AUDIT_LOG.warnf(
+
"{\"event\":\"access_denied\",\"tool\":\"%s\",\"connectionId\":\"%s\","
+ +
"\"clientName\":\"%s\",\"requiredLevel\":\"%s\",\"currentLevel\":\"%s\","
+ + "\"timestamp\":\"%s\"}",
+ escape(tool), escape(connectionId), escape(clientName),
+ escape(requiredLevel), escape(currentLevel),
+ Instant.now().toString());
+ }
+
+ static String escape(String value) {
+ if (value == null) {
+ return "";
+ }
+ return value.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t")
+ .replace("\b", "\\b")
+ .replace("\f", "\\f");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecretRedactor.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecretRedactor.java
new file mode 100644
index 000000000000..d9df90aa21ed
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecretRedactor.java
@@ -0,0 +1,75 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+/**
+ * Regex-based secret redaction engine for MCP tool responses.
+ * <p>
+ * Applies configurable patterns to detect and replace credentials, tokens,
API keys, and connection strings in tool
+ * output text.
+ */
+@ApplicationScoped
+public class McpSecretRedactor {
+
+ static final String REDACTED = "***REDACTED***";
+
+ static final List<Pattern> DEFAULT_PATTERNS = List.of(
+ // password/passwd/pwd key-value pairs
+
Pattern.compile("(?i)(password|passwd|pwd)\\s*[=:]\\s*[^\\s,;}'\"\\]]+"),
+ // API keys, secret keys, access keys
+
Pattern.compile("(?i)(api[_-]?key|apikey|secret[_-]?key|access[_-]?key)\\s*[=:]\\s*[^\\s,;}'\"\\]]+"),
+ // tokens and bearer authorization
+
Pattern.compile("(?i)(token|bearer|authorization)\\s*[=:]\\s*[^\\s,;}'\"\\]]+"),
+ // AWS Access Key IDs (AKIA prefix + 16 alphanumeric)
+ Pattern.compile("AKIA[0-9A-Z]{16}"),
+ // Connection strings with embedded credentials
+
Pattern.compile("(?i)(mongodb(\\+srv)?://|amqp://|redis://|jdbc:)[^\\s\"']+@[^\\s\"']+"));
+
+ @Inject
+ McpSecurityConfig config;
+
+ public String redact(String text) {
+ if (text == null || text.isEmpty()) {
+ return text;
+ }
+ List<Pattern> patterns = config.getRedactionPatterns();
+ String result = text;
+ for (Pattern pattern : patterns) {
+ result = pattern.matcher(result).replaceAll(REDACTED);
+ }
+ return result;
+ }
+
+ public boolean containsSecret(String text) {
+ if (text == null || text.isEmpty()) {
+ return false;
+ }
+ List<Pattern> patterns = config.getRedactionPatterns();
+ for (Pattern pattern : patterns) {
+ if (pattern.matcher(text).find()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecured.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecured.java
new file mode 100644
index 000000000000..9097bf0179a4
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecured.java
@@ -0,0 +1,38 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import jakarta.interceptor.InterceptorBinding;
+
+/**
+ * CDI interceptor binding for the MCP security execution layer.
+ * <p>
+ * Apply to {@code @ApplicationScoped} tool classes to enable security
interception (authorization, audit logging, input
+ * sanitization, secret redaction) on all {@code @Tool}-annotated methods.
+ */
+@Inherited
+@InterceptorBinding
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+public @interface McpSecured {
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityConfig.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityConfig.java
new file mode 100644
index 000000000000..25f21a5bf2f1
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityConfig.java
@@ -0,0 +1,141 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.logging.Logger;
+
+/**
+ * Central configuration for the MCP security execution layer.
+ * <p>
+ * All settings are opt-in and disabled by default to preserve backward
compatibility. Enable via properties or
+ * environment variables (e.g., {@code CAMEL_MCP_SECURITY_ENABLED=true}).
+ */
+@ApplicationScoped
+public class McpSecurityConfig {
+
+ private static final Logger LOG =
Logger.getLogger(McpSecurityConfig.class);
+
+ static final int DEFAULT_MAX_ARGUMENT_LENGTH = 10_000;
+
+ @ConfigProperty(name = "camel.mcp.security.enabled", defaultValue =
"false")
+ boolean enabled;
+
+ @ConfigProperty(name = "camel.mcp.security.access-level", defaultValue =
"admin")
+ String accessLevel;
+
+ @ConfigProperty(name = "camel.mcp.security.audit.enabled", defaultValue =
"false")
+ boolean auditEnabled;
+
+ @ConfigProperty(name = "camel.mcp.security.audit.include-arguments",
defaultValue = "true")
+ boolean auditIncludeArguments;
+
+ @ConfigProperty(name = "camel.mcp.security.redaction.enabled",
defaultValue = "false")
+ boolean redactionEnabled;
+
+ @ConfigProperty(name = "camel.mcp.security.redaction.patterns")
+ Optional<String> redactionPatterns;
+
+ @ConfigProperty(name = "camel.mcp.security.max-argument-length",
defaultValue = "10000")
+ int maxArgumentLength;
+
+ private volatile List<Pattern> compiledPatterns;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public AccessLevel getAccessLevel() {
+ return AccessLevel.parse(accessLevel);
+ }
+
+ public boolean isAuditEnabled() {
+ return auditEnabled;
+ }
+
+ public boolean isAuditIncludeArguments() {
+ return auditIncludeArguments;
+ }
+
+ public boolean isRedactionEnabled() {
+ return redactionEnabled;
+ }
+
+ public int getMaxArgumentLength() {
+ return maxArgumentLength > 0 ? maxArgumentLength :
DEFAULT_MAX_ARGUMENT_LENGTH;
+ }
+
+ public List<Pattern> getRedactionPatterns() {
+ List<Pattern> cached = compiledPatterns;
+ if (cached != null) {
+ return cached;
+ }
+ List<Pattern> patterns = new
ArrayList<>(McpSecretRedactor.DEFAULT_PATTERNS);
+ if (redactionPatterns.isPresent()) {
+ for (String p : redactionPatterns.get().split(",")) {
+ String trimmed = p.trim();
+ if (!trimmed.isEmpty()) {
+ try {
+ patterns.add(Pattern.compile(trimmed));
+ } catch (PatternSyntaxException e) {
+ LOG.warnf("Ignoring invalid redaction pattern '%s':
%s", trimmed, e.getMessage());
+ }
+ }
+ }
+ }
+ compiledPatterns = Collections.unmodifiableList(patterns);
+ return compiledPatterns;
+ }
+
+ /**
+ * Access levels derived from MCP tool annotations.
+ */
+ public enum AccessLevel {
+ READ_ONLY,
+ READ_WRITE,
+ ADMIN;
+
+ public boolean permits(boolean readOnlyHint, boolean destructiveHint) {
+ return switch (this) {
+ case READ_ONLY -> readOnlyHint;
+ case READ_WRITE -> !destructiveHint;
+ case ADMIN -> true;
+ };
+ }
+
+ static AccessLevel parse(String value) {
+ if (value == null || value.isBlank()) {
+ return ADMIN;
+ }
+ return switch (value.trim().toLowerCase().replace("_", "-")) {
+ case "read-only", "readonly" -> READ_ONLY;
+ case "read-write", "readwrite" -> READ_WRITE;
+ case "admin" -> ADMIN;
+ default -> ADMIN;
+ };
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityInterceptor.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityInterceptor.java
new file mode 100644
index 000000000000..2d0fc19f3e1b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityInterceptor.java
@@ -0,0 +1,175 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.lang.reflect.Method;
+
+import jakarta.annotation.Priority;
+import jakarta.inject.Inject;
+import jakarta.interceptor.AroundInvoke;
+import jakarta.interceptor.Interceptor;
+import jakarta.interceptor.InvocationContext;
+
+import io.quarkiverse.mcp.server.Tool;
+
+/**
+ * CDI interceptor for the MCP security execution layer.
+ * <p>
+ * Intercepts {@code @Tool}-annotated methods on classes marked with {@link
McpSecured} to provide input sanitization,
+ * audit logging, and secret redaction. Authorization is handled by {@link
McpAccessFilter} which is a global
+ * {@code ToolFilter}.
+ * <p>
+ * This replaces the ToolInputGuardrail/ToolOutputGuardrail approach since
those require per-tool
+ * {@code @ToolGuardrails} annotations and are not auto-discovered as global
CDI beans.
+ */
+@McpSecured
+@Interceptor
+@Priority(Interceptor.Priority.APPLICATION)
+public class McpSecurityInterceptor {
+
+ @Inject
+ McpSecurityConfig config;
+
+ @Inject
+ McpAuditLogger auditLogger;
+
+ @Inject
+ McpSecretRedactor redactor;
+
+ @AroundInvoke
+ Object intercept(InvocationContext ctx) throws Exception {
+ if (!config.isEnabled()) {
+ return ctx.proceed();
+ }
+
+ Method method = ctx.getMethod();
+ if (!method.isAnnotationPresent(Tool.class)) {
+ return ctx.proceed();
+ }
+
+ String toolName = method.getName();
+
+ // Input sanitization
+ sanitizeParameters(ctx);
+
+ // TODO: CDI interceptors do not have access to MCP connection context
(connectionId, clientName).
+ // These will be populated once global guardrail support is available
in Quarkus MCP Server.
+
+ // Audit: log tool call
+ if (config.isAuditEnabled()) {
+ String arguments = null;
+ if (config.isAuditIncludeArguments()) {
+ arguments = summarizeParameters(ctx);
+ }
+ auditLogger.logToolCall(toolName, "", "",
config.getAccessLevel().name(), arguments);
+ }
+
+ long start = System.nanoTime();
+ boolean isError = false;
+ boolean wasRedacted = false;
+ try {
+ Object result = ctx.proceed();
+
+ // Secret redaction on string results
+ if (config.isRedactionEnabled() && result instanceof String s) {
+ if (redactor.containsSecret(s)) {
+ result = redactor.redact(s);
+ wasRedacted = true;
+ }
+ }
+
+ return result;
+ } catch (Exception e) {
+ isError = true;
+ throw e;
+ } finally {
+ if (config.isAuditEnabled()) {
+ long durationMs = (System.nanoTime() - start) / 1_000_000;
+ auditLogger.logToolResult(toolName, "", isError, wasRedacted,
durationMs);
+ }
+ }
+ }
+
+ private void sanitizeParameters(InvocationContext ctx) {
+ Object[] params = ctx.getParameters();
+ if (params == null) {
+ return;
+ }
+
+ int maxLen = config.getMaxArgumentLength();
+ boolean modified = false;
+
+ for (int i = 0; i < params.length; i++) {
+ if (params[i] instanceof String s) {
+ String clean = stripControlChars(s);
+ if (clean.length() > maxLen) {
+ clean = clean.substring(0, maxLen);
+ }
+ if (!clean.equals(s)) {
+ params[i] = clean;
+ modified = true;
+ }
+ }
+ }
+
+ if (modified) {
+ ctx.setParameters(params);
+ }
+ }
+
+ static String stripControlChars(String input) {
+ if (input == null) {
+ return null;
+ }
+ StringBuilder sb = null;
+ for (int i = 0; i < input.length(); i++) {
+ char c = input.charAt(i);
+ if (c < 0x20 && c != '\n' && c != '\r' && c != '\t') {
+ if (sb == null) {
+ sb = new StringBuilder(input.length());
+ sb.append(input, 0, i);
+ }
+ } else if (sb != null) {
+ sb.append(c);
+ }
+ }
+ return sb != null ? sb.toString() : input;
+ }
+
+ private String summarizeParameters(InvocationContext ctx) {
+ Object[] params = ctx.getParameters();
+ if (params == null || params.length == 0) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < params.length; i++) {
+ if (params[i] == null) {
+ continue;
+ }
+ String value = params[i].toString();
+ if (value.length() > 200) {
+ value = value.substring(0, 200) + "...";
+ }
+ if (!sb.isEmpty()) {
+ sb.append(", ");
+ }
+ sb.append("arg").append(i).append("=").append(value);
+ }
+ return sb.toString();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
index 503705f906df..23b1ec5365fb 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
@@ -34,6 +34,7 @@ import io.quarkiverse.mcp.server.ToolCallException;
* returns a {@code nextStep} hint that guides the LLM to the next action in
the workflow. For migration summaries, use
* {@code git diff --shortstat} directly.
*/
+@McpSecured
@ApplicationScoped
public class MigrationTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java
index 8d001a258755..5d11a943f7be 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java
@@ -33,6 +33,7 @@ import io.quarkiverse.mcp.server.ToolCallException;
* Handles the special case where projects running on legacy runtimes need to
be replatformed to Spring Boot or Quarkus.
* Uses Maven archetypes for new project creation and migration guides for
component mapping details.
*/
+@McpSecured
@ApplicationScoped
public class MigrationWildflyKarafTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
index 5b7f00ae423d..8e50784a4beb 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
@@ -43,6 +43,7 @@ import io.swagger.v3.parser.core.models.SwaggerParseResult;
* Since Camel 4.6, the recommended approach is contract-first: referencing
the OpenAPI spec directly at runtime via
* rest:openApi. These tools help validate, scaffold, and provide mock
guidance for that workflow.
*/
+@McpSecured
@ApplicationScoped
public class OpenApiTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
index 0c6cd955ddad..17a4d2fbc768 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
@@ -38,6 +38,7 @@ import io.quarkiverse.mcp.server.ToolCallException;
* ({@code camel.server.*} / {@code camel.management.*} are jbang-only) and
the legacy {@code camel.springboot.*}
* configuration that was harmonized to {@code camel.main.*} in Camel 4.5 and
removed in 4.13.
*/
+@McpSecured
@ApplicationScoped
public class PropertiesTranslateTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
index f51a9cca1c55..e7a5bd920ab6 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
@@ -34,6 +34,7 @@ import org.apache.camel.dsl.jbang.core.common.Printer;
* MCP Tool for generating a visual diagram of Camel routes from a source file
(non-running integration). Wraps the
* {@code camel route-diagram} jbang command. Supports both PNG image output
and plain-text ASCII/Unicode output.
*/
+@McpSecured
@ApplicationScoped
public class RouteDiagramTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index d0affc50fc51..e89bfa66ce23 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -42,6 +42,7 @@ import org.apache.camel.util.json.Jsoner;
* The {@code @Tool} and {@code @ToolArg} annotations are required by the
Quarkus MCP server for protocol discovery but
* all tool logic lives in {@link ToolRegistry}, ensuring a single source of
truth shared with the Agent REPL.
*/
+@McpSecured
@ApplicationScoped
public class RuntimeTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
index 457ea6db86d6..35ec249b8f87 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
@@ -41,6 +41,7 @@ import org.apache.camel.util.json.JsonObject;
* {@code @CamelSpringBootTest} for Spring Boot), mock endpoints for
producers, and {@code @RegisterExtension} stubs for
* infrastructure components like Kafka, JMS, MongoDB, etc.
*/
+@McpSecured
@ApplicationScoped
public class TestScaffoldTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
index 604f7339b7da..37eb945716a3 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
@@ -51,6 +51,7 @@ import org.apache.camel.yaml.out.YamlModelWriter;
/**
* MCP Tools for validating and transforming Camel routes using Quarkus MCP
Server.
*/
+@McpSecured
@ApplicationScoped
public class TransformTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/VersionTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/VersionTools.java
index 3460af5c8fb8..38bbed64491d 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/VersionTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/VersionTools.java
@@ -35,6 +35,7 @@ import org.apache.camel.util.json.Jsoner;
/**
* MCP Tools for querying available Camel versions using Quarkus MCP Server.
*/
+@McpSecured
@ApplicationScoped
public class VersionTools {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties
b/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties
index 934c9a801297..c6ac18f277e4 100644
--- a/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties
@@ -36,3 +36,17 @@ quarkus.http.host-enabled=false
# Strip null fields from JSON responses to reduce LLM token consumption.
# Applies to @Tool result records serialized via JsonTextContentEncoder.
quarkus.jackson.serialization-inclusion=non-null
+
+## MCP Security Configuration (opt-in, disabled by default)
+## Enable the security execution layer for tool-level authorization,
+## audit logging, input sanitization, and secret redaction.
+# camel.mcp.security.enabled=true
+# camel.mcp.security.access-level=admin
+# camel.mcp.security.audit.enabled=true
+# camel.mcp.security.audit.include-arguments=true
+# camel.mcp.security.redaction.enabled=true
+# camel.mcp.security.redaction.patterns=
+# camel.mcp.security.max-argument-length=10000
+
+## Audit log routing (optional — route to a separate file or syslog)
+# quarkus.log.category."org.apache.camel.mcp.security.audit".level=INFO
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAccessFilterTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAccessFilterTest.java
new file mode 100644
index 000000000000..4a7d78b85ae6
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAccessFilterTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class McpAccessFilterTest {
+
+ @Test
+ void securityDisabledAllowsAllAccessLevels() {
+ McpSecurityConfig config = createConfig(false, "read-only");
+
+ // Even read-only level permits everything when security is disabled
+ assertThat(config.isEnabled()).isFalse();
+ }
+
+ @Test
+ void readOnlyLevelBlocksWriteAndDestructive() {
+ McpSecurityConfig config = createConfig(true, "read-only");
+ McpSecurityConfig.AccessLevel level = config.getAccessLevel();
+
+ assertThat(level.permits(true, false)).as("read-only tool should be
visible").isTrue();
+ assertThat(level.permits(false, false)).as("write tool should be
hidden").isFalse();
+ assertThat(level.permits(false, true)).as("destructive tool should be
hidden").isFalse();
+ }
+
+ @Test
+ void readWriteLevelBlocksDestructiveOnly() {
+ McpSecurityConfig config = createConfig(true, "read-write");
+ McpSecurityConfig.AccessLevel level = config.getAccessLevel();
+
+ assertThat(level.permits(true, false)).as("read-only tool should be
visible").isTrue();
+ assertThat(level.permits(false, false)).as("write tool should be
visible").isTrue();
+ assertThat(level.permits(false, true)).as("destructive tool should be
hidden").isFalse();
+ }
+
+ @Test
+ void adminLevelAllowsEverything() {
+ McpSecurityConfig config = createConfig(true, "admin");
+ McpSecurityConfig.AccessLevel level = config.getAccessLevel();
+
+ assertThat(level.permits(true, false)).as("read-only tool").isTrue();
+ assertThat(level.permits(false, false)).as("write tool").isTrue();
+ assertThat(level.permits(false, true)).as("destructive tool").isTrue();
+ }
+
+ private static McpSecurityConfig createConfig(boolean enabled, String
accessLevel) {
+ McpSecurityConfig config = new McpSecurityConfig();
+ config.enabled = enabled;
+ config.accessLevel = accessLevel;
+ config.auditEnabled = false;
+ config.auditIncludeArguments = true;
+ config.redactionEnabled = false;
+ config.redactionPatterns = Optional.empty();
+ config.maxArgumentLength =
McpSecurityConfig.DEFAULT_MAX_ARGUMENT_LENGTH;
+ return config;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAuditLoggerTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAuditLoggerTest.java
new file mode 100644
index 000000000000..994786e180ec
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpAuditLoggerTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class McpAuditLoggerTest {
+
+ @Test
+ void escapeHandlesSpecialCharacters() {
+
assertThat(McpAuditLogger.escape("line1\nline2")).isEqualTo("line1\\nline2");
+ assertThat(McpAuditLogger.escape("tab\there")).isEqualTo("tab\\there");
+
assertThat(McpAuditLogger.escape("quote\"here")).isEqualTo("quote\\\"here");
+
assertThat(McpAuditLogger.escape("back\\slash")).isEqualTo("back\\\\slash");
+
assertThat(McpAuditLogger.escape("return\rhere")).isEqualTo("return\\rhere");
+
assertThat(McpAuditLogger.escape("back\bspace")).isEqualTo("back\\bspace");
+
assertThat(McpAuditLogger.escape("form\ffeed")).isEqualTo("form\\ffeed");
+ }
+
+ @Test
+ void escapeHandlesNull() {
+ assertThat(McpAuditLogger.escape(null)).isEmpty();
+ }
+
+ @Test
+ void escapePreservesNormalText() {
+ assertThat(McpAuditLogger.escape("normal text")).isEqualTo("normal
text");
+ }
+
+ @Test
+ void loggerCanBeInstantiated() {
+ McpAuditLogger logger = new McpAuditLogger();
+ // Verify the logger doesn't throw on invocation — actual log output
+ // goes to JBoss LogManager which is not captured in unit tests
+ logger.logToolCall("test_tool", "conn-1", "claude", "ADMIN",
"{\"arg\":\"val\"}");
+ logger.logToolResult("test_tool", "conn-1", false, false, 42);
+ logger.logAccessDenied("test_tool", "conn-1", "claude", "admin",
"read-only");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecretRedactorTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecretRedactorTest.java
new file mode 100644
index 000000000000..f958b45bc81e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecretRedactorTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class McpSecretRedactorTest {
+
+ private McpSecretRedactor createRedactor(String extraPatterns) {
+ McpSecurityConfig config = new McpSecurityConfig();
+ config.enabled = true;
+ config.redactionEnabled = true;
+ config.redactionPatterns = Optional.ofNullable(extraPatterns);
+ config.maxArgumentLength =
McpSecurityConfig.DEFAULT_MAX_ARGUMENT_LENGTH;
+ config.accessLevel = "admin";
+ config.auditEnabled = false;
+ config.auditIncludeArguments = true;
+
+ McpSecretRedactor redactor = new McpSecretRedactor();
+ redactor.config = config;
+ return redactor;
+ }
+
+ @Test
+ void redactsPasswordKeyValue() {
+ McpSecretRedactor redactor = createRedactor(null);
+
+ assertThat(redactor.redact("password=secret123"))
+ .isEqualTo(McpSecretRedactor.REDACTED);
+ assertThat(redactor.redact("Password: myP@ssw0rd"))
+ .isEqualTo(McpSecretRedactor.REDACTED);
+ assertThat(redactor.redact("pwd=abc"))
+ .isEqualTo(McpSecretRedactor.REDACTED);
+ }
+
+ @Test
+ void redactsApiKeys() {
+ McpSecretRedactor redactor = createRedactor(null);
+
+ assertThat(redactor.redact("api_key=sk-12345"))
+ .isEqualTo(McpSecretRedactor.REDACTED);
+ assertThat(redactor.redact("apiKey: abc123"))
+ .isEqualTo(McpSecretRedactor.REDACTED);
+ assertThat(redactor.redact("secret-key=xyz789"))
+ .isEqualTo(McpSecretRedactor.REDACTED);
+ }
+
+ @Test
+ void redactsTokensAndBearer() {
+ McpSecretRedactor redactor = createRedactor(null);
+
+ assertThat(redactor.redact("token=eyJhbGciOiJIUzI1NiJ9"))
+ .isEqualTo(McpSecretRedactor.REDACTED);
+ assertThat(redactor.redact("bearer: some-bearer-token"))
+ .isEqualTo(McpSecretRedactor.REDACTED);
+ }
+
+ @Test
+ void redactsAwsAccessKeyIds() {
+ McpSecretRedactor redactor = createRedactor(null);
+
+ assertThat(redactor.redact("key is AKIAIOSFODNN7EXAMPLE"))
+ .isEqualTo("key is " + McpSecretRedactor.REDACTED);
+ }
+
+ @Test
+ void redactsConnectionStringsWithCredentials() {
+ McpSecretRedactor redactor = createRedactor(null);
+
+ assertThat(redactor.redact("uri: mongodb://user:pass@host:27017/db"))
+ .isEqualTo("uri: " + McpSecretRedactor.REDACTED);
+
assertThat(redactor.redact("url=amqp://admin:secret@broker:5672/vhost"))
+ .isEqualTo("url=" + McpSecretRedactor.REDACTED);
+ }
+
+ @Test
+ void doesNotRedactNormalText() {
+ McpSecretRedactor redactor = createRedactor(null);
+
+ String normalText = "Route started successfully with 3 endpoints";
+ assertThat(redactor.redact(normalText)).isEqualTo(normalText);
+ }
+
+ @Test
+ void handlesMultipleSecretsInSameString() {
+ McpSecretRedactor redactor = createRedactor(null);
+
+ String input = "password=secret, apiKey=abc123";
+ String result = redactor.redact(input);
+ assertThat(result).doesNotContain("secret").doesNotContain("abc123");
+ }
+
+ @Test
+ void handlesNullAndEmpty() {
+ McpSecretRedactor redactor = createRedactor(null);
+
+ assertThat(redactor.redact(null)).isNull();
+ assertThat(redactor.redact("")).isEmpty();
+ }
+
+ @Test
+ void containsSecretDetectsSecrets() {
+ McpSecretRedactor redactor = createRedactor(null);
+
+ assertThat(redactor.containsSecret("password=secret")).isTrue();
+ assertThat(redactor.containsSecret("normal text")).isFalse();
+ assertThat(redactor.containsSecret(null)).isFalse();
+ assertThat(redactor.containsSecret("")).isFalse();
+ }
+
+ @Test
+ void customPatternsAreApplied() {
+ McpSecretRedactor redactor = createRedactor("CUSTOM_\\d+");
+
+ assertThat(redactor.redact("value: CUSTOM_12345 here"))
+ .isEqualTo("value: " + McpSecretRedactor.REDACTED + " here");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityConfigTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityConfigTest.java
new file mode 100644
index 000000000000..b1a9e793b4fe
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityConfigTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class McpSecurityConfigTest {
+
+ // ---- AccessLevel.permits() ----
+
+ @Test
+ void readOnlyPermitsOnlyReadOnlyTools() {
+ McpSecurityConfig.AccessLevel level =
McpSecurityConfig.AccessLevel.READ_ONLY;
+
+ assertThat(level.permits(true, false)).as("read-only tool").isTrue();
+ assertThat(level.permits(false, false)).as("read-write
tool").isFalse();
+ assertThat(level.permits(false, true)).as("destructive
tool").isFalse();
+ }
+
+ @Test
+ void readWritePermitsNonDestructiveTools() {
+ McpSecurityConfig.AccessLevel level =
McpSecurityConfig.AccessLevel.READ_WRITE;
+
+ assertThat(level.permits(true, false)).as("read-only tool").isTrue();
+ assertThat(level.permits(false, false)).as("read-write tool").isTrue();
+ assertThat(level.permits(false, true)).as("destructive
tool").isFalse();
+ }
+
+ @Test
+ void adminPermitsAllTools() {
+ McpSecurityConfig.AccessLevel level =
McpSecurityConfig.AccessLevel.ADMIN;
+
+ assertThat(level.permits(true, false)).as("read-only tool").isTrue();
+ assertThat(level.permits(false, false)).as("read-write tool").isTrue();
+ assertThat(level.permits(false, true)).as("destructive tool").isTrue();
+ }
+
+ // ---- AccessLevel.parse() ----
+
+ @Test
+ void parseAccessLevelVariants() {
+
assertThat(McpSecurityConfig.AccessLevel.parse("read-only")).isEqualTo(McpSecurityConfig.AccessLevel.READ_ONLY);
+
assertThat(McpSecurityConfig.AccessLevel.parse("readonly")).isEqualTo(McpSecurityConfig.AccessLevel.READ_ONLY);
+
assertThat(McpSecurityConfig.AccessLevel.parse("READ_ONLY")).isEqualTo(McpSecurityConfig.AccessLevel.READ_ONLY);
+
assertThat(McpSecurityConfig.AccessLevel.parse("read-write")).isEqualTo(McpSecurityConfig.AccessLevel.READ_WRITE);
+
assertThat(McpSecurityConfig.AccessLevel.parse("readwrite")).isEqualTo(McpSecurityConfig.AccessLevel.READ_WRITE);
+
assertThat(McpSecurityConfig.AccessLevel.parse("admin")).isEqualTo(McpSecurityConfig.AccessLevel.ADMIN);
+
assertThat(McpSecurityConfig.AccessLevel.parse("ADMIN")).isEqualTo(McpSecurityConfig.AccessLevel.ADMIN);
+ }
+
+ @Test
+ void parseUnknownDefaultsToAdmin() {
+
assertThat(McpSecurityConfig.AccessLevel.parse("unknown")).isEqualTo(McpSecurityConfig.AccessLevel.ADMIN);
+
assertThat(McpSecurityConfig.AccessLevel.parse("")).isEqualTo(McpSecurityConfig.AccessLevel.ADMIN);
+
assertThat(McpSecurityConfig.AccessLevel.parse(null)).isEqualTo(McpSecurityConfig.AccessLevel.ADMIN);
+ }
+
+ // ---- Config defaults ----
+
+ @Test
+ void defaultConfigIsDisabled() {
+ McpSecurityConfig config = createConfig(false, "admin", false, true,
false, null);
+
+ assertThat(config.isEnabled()).isFalse();
+
assertThat(config.getAccessLevel()).isEqualTo(McpSecurityConfig.AccessLevel.ADMIN);
+ assertThat(config.isAuditEnabled()).isFalse();
+ assertThat(config.isAuditIncludeArguments()).isTrue();
+ assertThat(config.isRedactionEnabled()).isFalse();
+ }
+
+ @Test
+ void maxArgumentLengthFallsBackToDefault() {
+ McpSecurityConfig config = createConfig(false, "admin", false, true,
false, null);
+ config.maxArgumentLength = 0;
+
+
assertThat(config.getMaxArgumentLength()).isEqualTo(McpSecurityConfig.DEFAULT_MAX_ARGUMENT_LENGTH);
+ }
+
+ // ---- Redaction patterns ----
+
+ @Test
+ void defaultRedactionPatternsIncludeBuiltins() {
+ McpSecurityConfig config = createConfig(true, "admin", false, true,
true, null);
+
+ List<Pattern> patterns = config.getRedactionPatterns();
+
+
assertThat(patterns).hasSizeGreaterThanOrEqualTo(McpSecretRedactor.DEFAULT_PATTERNS.size());
+ }
+
+ @Test
+ void customRedactionPatternsAppended() {
+ McpSecurityConfig config = createConfig(true, "admin", false, true,
true, "SECRET_\\d+,TOKEN_[A-Z]+");
+
+ List<Pattern> patterns = config.getRedactionPatterns();
+
+
assertThat(patterns.size()).isEqualTo(McpSecretRedactor.DEFAULT_PATTERNS.size()
+ 2);
+ }
+
+ // ---- Helper ----
+
+ private static McpSecurityConfig createConfig(
+ boolean enabled, String accessLevel, boolean auditEnabled,
+ boolean auditIncludeArguments, boolean redactionEnabled, String
redactionPatterns) {
+ McpSecurityConfig config = new McpSecurityConfig();
+ config.enabled = enabled;
+ config.accessLevel = accessLevel;
+ config.auditEnabled = auditEnabled;
+ config.auditIncludeArguments = auditIncludeArguments;
+ config.redactionEnabled = redactionEnabled;
+ config.redactionPatterns = Optional.ofNullable(redactionPatterns);
+ config.maxArgumentLength =
McpSecurityConfig.DEFAULT_MAX_ARGUMENT_LENGTH;
+ return config;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityInterceptorTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityInterceptorTest.java
new file mode 100644
index 000000000000..64c7eb586072
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpSecurityInterceptorTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class McpSecurityInterceptorTest {
+
+ @Test
+ void stripControlCharsRemovesNullBytes() {
+ assertThat(McpSecurityInterceptor.stripControlChars("hello\0world"))
+ .isEqualTo("helloworld");
+ }
+
+ @Test
+ void stripControlCharsPreservesNewlinesTabsReturns() {
+
assertThat(McpSecurityInterceptor.stripControlChars("line1\nline2\ttab\rreturn"))
+ .isEqualTo("line1\nline2\ttab\rreturn");
+ }
+
+ @Test
+ void stripControlCharsRemovesBellAndOtherControls() {
+ assertThat(McpSecurityInterceptor.stripControlChars("a\u0007b\u0001c"))
+ .isEqualTo("abc");
+ }
+
+ @Test
+ void stripControlCharsPreservesNormalText() {
+ String normal = "normal text with spaces and symbols!@#$%^&*()";
+
assertThat(McpSecurityInterceptor.stripControlChars(normal)).isEqualTo(normal);
+ }
+
+ @Test
+ void stripControlCharsHandlesNull() {
+ assertThat(McpSecurityInterceptor.stripControlChars(null)).isNull();
+ }
+
+ @Test
+ void stripControlCharsHandlesEmptyString() {
+ assertThat(McpSecurityInterceptor.stripControlChars("")).isEmpty();
+ }
+
+ @Test
+ void accessDenialMessageFormat() {
+ McpSecurityConfig.AccessLevel readOnly =
McpSecurityConfig.AccessLevel.READ_ONLY;
+
+ assertThat(readOnly.permits(false, true)).isFalse();
+ assertThat(readOnly.permits(false, false)).isFalse();
+ assertThat(readOnly.permits(true, false)).isTrue();
+ }
+}