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 68914926ebc1 camel-jbang - Redact secrets in structured MCP tool 
results, not only String results (#25093)
68914926ebc1 is described below

commit 68914926ebc1a3a8694690a82e3612bfb8944416
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Jul 28 09:19:49 2026 +0200

    camel-jbang - Redact secrets in structured MCP tool results, not only 
String results (#25093)
    
    * camel-jbang - Redact secrets in structured MCP tool results, not only 
String results
    
    The MCP security layer's secret redaction only inspected String tool 
results,
    so tools returning a JsonObject (the runtime tools, for example
    camel_runtime_properties which dumps a running application's configuration)
    bypassed redaction even when it was enabled. The value regex also cannot 
match
    JSON-quoted values, so structured output was doubly unprotected.
    
    Add McpSecretRedactor.redactStructured() which walks Map/List results in 
place,
    blanking the value of any entry whose key name denotes a secret and running 
the
    regex over string leaf values. The result is mutated in place so a tool's
    declared return type is preserved. Strengthen the default patterns to cover
    client_secret, secrets carried as a URL query parameter (jdbc:...?password=,
    which has no userinfo @), PEM private-key blocks and passphrase. Route 
Map/List
    results through the new path in McpSecurityInterceptor.
    
    Plain record-returning tools remain a follow-up: redacting them type-safely
    needs reflective record rebuilding or a framework-level output filter once 
the
    Quarkus MCP server exposes global guardrails.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    
    * camel-jbang - Use camel-util SensitiveUtils for MCP secret redaction
    
    Address review feedback (davsclaus): drop the hand-rolled regex word list 
and
    reuse Camel's own knowledge of what a secret is.
    
    Structured (Map/List) results are now redacted by key name via
    SensitiveUtils.containsSensitive(), the same helper the jbang ListProperties
    command already uses. Free-text results are masked with 
DefaultMaskingFormatter,
    which blanks key=value, XML and JSON values keyed off
    SensitiveUtils.getSensitiveKeys(). The hand-maintained DEFAULT_PATTERNS and
    SECRET_KEY_NAME are removed; operator-supplied custom patterns still apply 
on
    top.
    
    Connection-string userinfo (user:pass@host) and PEM blocks are no longer
    special-cased here; if wanted they belong in Camel's masker / SensitiveUtils
    rather than in the MCP layer.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    
    * camel-jbang-mcp: redact strings once instead of containsSecret + redact
    
    Addresses the gnodet/davsclaus review: the String branch called
    containsSecret(s) (which internally runs redact(s)) and then redact(s)
    again, running the full masking pipeline twice for every secret-bearing
    result. Use redact() as the single source of truth and compare its output
    to the input. containsSecret() stays as a tested public API on the redactor.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../jbang/core/commands/mcp/McpSecretRedactor.java | 102 +++++++++++++----
 .../jbang/core/commands/mcp/McpSecurityConfig.java |   4 +-
 .../core/commands/mcp/McpSecurityInterceptor.java  |  21 +++-
 .../core/commands/mcp/McpSecretRedactorTest.java   | 126 +++++++++++++--------
 .../core/commands/mcp/McpSecurityConfigTest.java   |  10 +-
 5 files changed, 184 insertions(+), 79 deletions(-)

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
index d9df90aa21ed..75030e2dfc4e 100644
--- 
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
@@ -17,44 +17,57 @@
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
 import java.util.List;
+import java.util.Map;
 import java.util.regex.Pattern;
 
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 
+import org.apache.camel.support.processor.DefaultMaskingFormatter;
+import org.apache.camel.util.SensitiveUtils;
+
 /**
- * Regex-based secret redaction engine for MCP tool responses.
+ * 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.
+ * Both modes reuse Camel's own knowledge of what a secret is, rather than a 
hand-maintained word list. Free-text output
+ * is masked with {@link DefaultMaskingFormatter}, which blanks the value of 
any {@code key=value}, XML element or JSON
+ * field whose name is one of {@link SensitiveUtils#getSensitiveKeys()}. 
Structured results ({@link Map}/{@link List},
+ * such as the {@code JsonObject} returned by the runtime tools) are walked 
and any entry whose key
+ * {@link SensitiveUtils#containsSensitive(String) is sensitive} has its value 
blanked, which is what catches
+ * JSON-quoted values inside an object tree. Operators may add extra regex 
patterns via
+ * {@code camel.mcp.security.redaction.patterns}.
  */
 @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;
 
+    private volatile DefaultMaskingFormatter maskingFormatter;
+
+    private DefaultMaskingFormatter maskingFormatter() {
+        DefaultMaskingFormatter mf = maskingFormatter;
+        if (mf == null) {
+            // Masks key=value, XML and JSON using 
SensitiveUtils.getSensitiveKeys() as the keyword set.
+            mf = new DefaultMaskingFormatter();
+            mf.setMaskString(REDACTED);
+            maskingFormatter = mf;
+        }
+        return mf;
+    }
+
+    /**
+     * Masks secrets in free-text tool output using Camel's masking formatter, 
then applies any operator-supplied custom
+     * patterns.
+     */
     public String redact(String text) {
         if (text == null || text.isEmpty()) {
             return text;
         }
-        List<Pattern> patterns = config.getRedactionPatterns();
-        String result = text;
-        for (Pattern pattern : patterns) {
+        String result = maskingFormatter().format(text);
+        for (Pattern pattern : config.getRedactionPatterns()) {
             result = pattern.matcher(result).replaceAll(REDACTED);
         }
         return result;
@@ -64,12 +77,55 @@ public class McpSecretRedactor {
         if (text == null || text.isEmpty()) {
             return false;
         }
-        List<Pattern> patterns = config.getRedactionPatterns();
-        for (Pattern pattern : patterns) {
-            if (pattern.matcher(text).find()) {
-                return true;
+        return !redact(text).equals(text);
+    }
+
+    /**
+     * Redacts a structured tool result in place. In a {@link Map} (such as 
the {@code JsonObject} returned by the
+     * runtime tools) any entry whose key {@link 
SensitiveUtils#containsSensitive(String) is sensitive} has its value
+     * blanked, and every string value is passed through {@link 
#redact(String)}; {@link List} elements are recursed.
+     * The same instance is mutated and returned, so a tool's declared return 
type is preserved.
+     *
+     * @param  value the result to scrub (a {@code Map}, {@code List}, or 
anything else which is returned untouched)
+     * @return       {@code true} if anything was changed
+     */
+    @SuppressWarnings("unchecked")
+    public boolean redactStructured(Object value) {
+        boolean changed = false;
+        if (value instanceof Map<?, ?> map) {
+            Map<Object, Object> mutable = (Map<Object, Object>) map;
+            for (Object key : List.copyOf(mutable.keySet())) {
+                Object current = mutable.get(key);
+                if (key instanceof String name && 
SensitiveUtils.containsSensitive(name)) {
+                    if (current != null && !REDACTED.equals(current)) {
+                        mutable.put(key, REDACTED);
+                        changed = true;
+                    }
+                } else if (current instanceof String s) {
+                    String scrubbed = redact(s);
+                    if (!scrubbed.equals(s)) {
+                        mutable.put(key, scrubbed);
+                        changed = true;
+                    }
+                } else if (current instanceof Map<?, ?> || current instanceof 
List<?>) {
+                    changed |= redactStructured(current);
+                }
+            }
+        } else if (value instanceof List<?> list) {
+            List<Object> mutable = (List<Object>) list;
+            for (int i = 0; i < mutable.size(); i++) {
+                Object current = mutable.get(i);
+                if (current instanceof String s) {
+                    String scrubbed = redact(s);
+                    if (!scrubbed.equals(s)) {
+                        mutable.set(i, scrubbed);
+                        changed = true;
+                    }
+                } else if (current instanceof Map<?, ?> || current instanceof 
List<?>) {
+                    changed |= redactStructured(current);
+                }
             }
         }
-        return false;
+        return changed;
     }
 }
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
index 25f21a5bf2f1..0648afa5dbac 100644
--- 
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
@@ -93,7 +93,9 @@ public class McpSecurityConfig {
         if (cached != null) {
             return cached;
         }
-        List<Pattern> patterns = new 
ArrayList<>(McpSecretRedactor.DEFAULT_PATTERNS);
+        // Built-in secret detection is handled by McpSecretRedactor via 
Camel's DefaultMaskingFormatter and
+        // SensitiveUtils. These are only the extra, operator-supplied 
patterns.
+        List<Pattern> patterns = new ArrayList<>();
         if (redactionPatterns.isPresent()) {
             for (String p : redactionPatterns.get().split(",")) {
                 String trimmed = p.trim();
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
index 2d0fc19f3e1b..bbc59e988379 100644
--- 
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
@@ -17,6 +17,8 @@
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
 import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
 
 import jakarta.annotation.Priority;
 import jakarta.inject.Inject;
@@ -84,11 +86,20 @@ public class McpSecurityInterceptor {
         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;
+            // Secret redaction. Free-text (String) results go through the 
regex redactor; structured results
+            // (Map/List, such as the JsonObject returned by the runtime 
tools) are scrubbed by key name in place,
+            // which is what catches JSON-quoted values the value regex cannot 
match.
+            if (config.isRedactionEnabled() && result != null) {
+                if (result instanceof String s) {
+                    // redact() is the single source of truth: if it changes 
the string, a secret was masked.
+                    // Calling containsSecret() first would run the full 
masking pipeline a second time.
+                    String redacted = redactor.redact(s);
+                    if (!redacted.equals(s)) {
+                        result = redacted;
+                        wasRedacted = true;
+                    }
+                } else if (result instanceof Map<?, ?> || result instanceof 
List<?>) {
+                    wasRedacted = redactor.redactStructured(result);
                 }
             }
 
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
index f958b45bc81e..e009972be071 100644
--- 
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
@@ -18,6 +18,8 @@ package org.apache.camel.dsl.jbang.core.commands.mcp;
 
 import java.util.Optional;
 
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -40,97 +42,129 @@ class McpSecretRedactorTest {
     }
 
     @Test
-    void redactsPasswordKeyValue() {
+    void redactsKeyValueUsingCamelSensitiveKeywords() {
         McpSecretRedactor redactor = createRedactor(null);
 
+        // The value is masked and the key is preserved (Camel's 
DefaultMaskingFormatter behaviour).
         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);
+                .isEqualTo("password=" + McpSecretRedactor.REDACTED);
+        assertThat(redactor.redact("apikey=sk-12345"))
+                .isEqualTo("apikey=" + McpSecretRedactor.REDACTED);
+        assertThat(redactor.redact("accessToken=abc123"))
+                .isEqualTo("accessToken=" + McpSecretRedactor.REDACTED);
+        // A compound property name is matched via the embedded keyword.
+        assertThat(redactor.redact("datasource.password=hunter2"))
+                .doesNotContain("hunter2")
+                .contains(McpSecretRedactor.REDACTED);
     }
 
     @Test
-    void redactsApiKeys() {
+    void redactsSecretCarriedAsUrlQueryParameter() {
         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);
+        assertThat(redactor.redact("uri: 
jdbc:postgresql://db:5432/app?ssl=true&password=hunter2"))
+                .doesNotContain("hunter2")
+                .contains(McpSecretRedactor.REDACTED);
     }
 
     @Test
-    void redactsTokensAndBearer() {
+    void redactsSensitiveJsonField() {
         McpSecretRedactor redactor = createRedactor(null);
 
-        assertThat(redactor.redact("token=eyJhbGciOiJIUzI1NiJ9"))
-                .isEqualTo(McpSecretRedactor.REDACTED);
-        assertThat(redactor.redact("bearer: some-bearer-token"))
-                .isEqualTo(McpSecretRedactor.REDACTED);
+        
assertThat(redactor.redact("{\"password\":\"hunter2\",\"name\":\"orders\"}"))
+                .doesNotContain("hunter2")
+                .contains("orders");
     }
 
     @Test
-    void redactsAwsAccessKeyIds() {
+    void doesNotRedactNonSensitiveKeyValue() {
         McpSecretRedactor redactor = createRedactor(null);
 
-        assertThat(redactor.redact("key is AKIAIOSFODNN7EXAMPLE"))
-                .isEqualTo("key is " + McpSecretRedactor.REDACTED);
+        
assertThat(redactor.redact("route=timer-1")).isEqualTo("route=timer-1");
+        assertThat(redactor.redact("Route started successfully with 3 
endpoints"))
+                .isEqualTo("Route started successfully with 3 endpoints");
     }
 
     @Test
-    void redactsConnectionStringsWithCredentials() {
+    void handlesNullAndEmpty() {
         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);
+        assertThat(redactor.redact(null)).isNull();
+        assertThat(redactor.redact("")).isEmpty();
     }
 
     @Test
-    void doesNotRedactNormalText() {
+    void containsSecretDetectsSecrets() {
         McpSecretRedactor redactor = createRedactor(null);
 
-        String normalText = "Route started successfully with 3 endpoints";
-        assertThat(redactor.redact(normalText)).isEqualTo(normalText);
+        assertThat(redactor.containsSecret("password=secret")).isTrue();
+        assertThat(redactor.containsSecret("normal text")).isFalse();
+        assertThat(redactor.containsSecret(null)).isFalse();
+        assertThat(redactor.containsSecret("")).isFalse();
     }
 
     @Test
-    void handlesMultipleSecretsInSameString() {
-        McpSecretRedactor redactor = createRedactor(null);
+    void customPatternsAreApplied() {
+        McpSecretRedactor redactor = createRedactor("CUSTOM_\\d+");
 
-        String input = "password=secret, apiKey=abc123";
-        String result = redactor.redact(input);
-        assertThat(result).doesNotContain("secret").doesNotContain("abc123");
+        assertThat(redactor.redact("value: CUSTOM_12345 here"))
+                .isEqualTo("value: " + McpSecretRedactor.REDACTED + " here");
     }
 
     @Test
-    void handlesNullAndEmpty() {
+    void redactsStructuredJsonObjectByKeyName() {
         McpSecretRedactor redactor = createRedactor(null);
 
-        assertThat(redactor.redact(null)).isNull();
-        assertThat(redactor.redact("")).isEmpty();
+        // The case the String-only path missed: a JsonObject (as returned by 
the runtime tools) whose value is
+        // JSON-quoted. Structured redaction blanks it by key name using 
SensitiveUtils.containsSensitive, which
+        // strips a compound property to its last segment (datasource.password 
-> password).
+        JsonObject properties = new JsonObject();
+        properties.put("datasource.url", "jdbc:postgresql://db:5432/app");
+        properties.put("datasource.password", "hunter2");
+        properties.put("app.name", "orders");
+        JsonObject nested = new JsonObject();
+        nested.put("client-secret", "s3cr3t");
+        properties.put("oauth", nested);
+
+        boolean changed = redactor.redactStructured(properties);
+
+        assertThat(changed).isTrue();
+        
assertThat(properties.getString("datasource.password")).isEqualTo(McpSecretRedactor.REDACTED);
+        assertThat(properties.getString("app.name")).isEqualTo("orders");
+        
assertThat(properties.getString("datasource.url")).isEqualTo("jdbc:postgresql://db:5432/app");
+        assertThat(((JsonObject) 
properties.get("oauth")).getString("client-secret"))
+                .isEqualTo(McpSecretRedactor.REDACTED);
     }
 
     @Test
-    void containsSecretDetectsSecrets() {
+    void redactsSecretsEmbeddedInStructuredStringValues() {
         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();
+        // A non-secret key whose string value nonetheless embeds a key=value 
credential.
+        JsonObject config = new JsonObject();
+        config.put("name", "orders");
+        JsonArray endpoints = new JsonArray();
+        endpoints.add("timer://foo");
+        endpoints.add("sql://bar?dataSource=#ds&password=leaked");
+        config.put("endpoints", endpoints);
+
+        boolean changed = redactor.redactStructured(config);
+
+        assertThat(changed).isTrue();
+        assertThat(config.getString("name")).isEqualTo("orders");
+        assertThat(endpoints.getString(0)).isEqualTo("timer://foo");
+        
assertThat(endpoints.getString(1)).doesNotContain("leaked").contains(McpSecretRedactor.REDACTED);
     }
 
     @Test
-    void customPatternsAreApplied() {
-        McpSecretRedactor redactor = createRedactor("CUSTOM_\\d+");
+    void structuredRedactionReportsNoChangeForCleanData() {
+        McpSecretRedactor redactor = createRedactor(null);
 
-        assertThat(redactor.redact("value: CUSTOM_12345 here"))
-                .isEqualTo("value: " + McpSecretRedactor.REDACTED + " here");
+        JsonObject clean = new JsonObject();
+        clean.put("name", "orders");
+        clean.put("routes", 3);
+
+        assertThat(redactor.redactStructured(clean)).isFalse();
+        assertThat(clean.getString("name")).isEqualTo("orders");
     }
 }
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
index b1a9e793b4fe..99574a8f03e1 100644
--- 
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
@@ -99,21 +99,23 @@ class McpSecurityConfigTest {
     // ---- Redaction patterns ----
 
     @Test
-    void defaultRedactionPatternsIncludeBuiltins() {
+    void redactionPatternsAreEmptyWithoutCustomPatterns() {
         McpSecurityConfig config = createConfig(true, "admin", false, true, 
true, null);
 
+        // Built-in secret detection is handled by McpSecretRedactor 
(DefaultMaskingFormatter + SensitiveUtils),
+        // so getRedactionPatterns() returns only the operator-supplied extra 
patterns.
         List<Pattern> patterns = config.getRedactionPatterns();
 
-        
assertThat(patterns).hasSizeGreaterThanOrEqualTo(McpSecretRedactor.DEFAULT_PATTERNS.size());
+        assertThat(patterns).isEmpty();
     }
 
     @Test
-    void customRedactionPatternsAppended() {
+    void customRedactionPatternsAreReturned() {
         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);
+        assertThat(patterns).hasSize(2);
     }
 
     // ---- Helper ----

Reply via email to