This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-24390
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 0f07b447294dc9cffa1a4a5388e7cf8a4056b627
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Aug 13 15:05:05 2026 +0200

    CAMEL-24390: camel-mcp-server - support wildcard patterns in tag matching
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../component/mcp/server/McpServerBridge.java      | 29 ++++-----
 .../mcp/server/McpServerConfiguration.java         |  5 +-
 .../mcp/server/McpServerBridgePrefixTagTest.java   | 68 ++++++++++++++++++++++
 .../mcp/server/McpServerBridgeWildcardTagTest.java | 65 +++++++++++++++++++++
 .../camel-mcp-server/src/main/docs/mcp-server.adoc | 17 ++++--
 .../main/HttpServerConfigurationProperties.java    | 10 ++--
 6 files changed, 168 insertions(+), 26 deletions(-)

diff --git 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
index 70caf40f3570..91abc3f11f1c 100644
--- 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
+++ 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
@@ -16,11 +16,8 @@
  */
 package org.apache.camel.component.mcp.server;
 
-import java.util.Arrays;
 import java.util.HashMap;
-import java.util.LinkedHashSet;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
@@ -40,6 +37,7 @@ import org.apache.camel.component.ai.tool.AiToolRegistry;
 import org.apache.camel.component.ai.tool.AiToolRegistryListener;
 import org.apache.camel.component.ai.tool.AiToolResult;
 import org.apache.camel.component.ai.tool.AiToolSpec;
+import org.apache.camel.support.PatternHelper;
 import org.apache.camel.support.ResolverHelper;
 import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.support.service.ServiceSupport;
@@ -52,9 +50,9 @@ import org.slf4j.LoggerFactory;
  * <p>
  * Security notes:
  * <ul>
- * <li>Only tools whose tags intersect the configured {@code tags} are 
exposed. The untagged default pool is never
- * exposed — external MCP clients are untrusted senders and crossing that 
trust boundary is an explicit per-tool
- * opt-in.</li>
+ * <li>Only tools whose tags match the configured tag patterns are exposed. 
Tag patterns support exact match, wildcard
+ * prefix ({@code foo*}), and {@code *} to match all tags. The untagged 
default pool is never exposed — external MCP
+ * clients are untrusted senders and crossing that trust boundary is an 
explicit per-tool opt-in.</li>
  * <li>MCP has a flat tool namespace: a tool whose name collides with an 
already published tool is refused with an ERROR
  * log, never silently replaced.</li>
  * <li>Raw route exception messages never reach the engine: execution failures 
map to a generic error message and the
@@ -78,7 +76,7 @@ public class McpServerBridge extends ServiceSupport 
implements CamelContextAware
     private CamelContext camelContext;
     private McpServerEngine engine;
     private AiToolRegistry registry;
-    private Set<String> selectedTags = Set.of();
+    private String[] tagPatterns = new String[0];
     private ExecutorService executor;
 
     public McpServerBridge(McpServerConfiguration configuration) {
@@ -106,9 +104,9 @@ public class McpServerBridge extends ServiceSupport 
implements CamelContextAware
     @Override
     protected void doInit() throws Exception {
         if (configuration.getTags() != null) {
-            selectedTags = new 
LinkedHashSet<>(Arrays.asList(AiToolParameterHelper.splitTags(configuration.getTags())));
+            tagPatterns = 
AiToolParameterHelper.splitTags(configuration.getTags());
         }
-        if (selectedTags.isEmpty()) {
+        if (tagPatterns.length == 0) {
             LOG.warn("No MCP tags configured: no ai-tool routes will be 
exposed as MCP tools. "
                      + "Set tags to opt-in the tools to expose.");
         }
@@ -150,7 +148,7 @@ public class McpServerBridge extends ServiceSupport 
implements CamelContextAware
         // subscribe before snapshotting so no concurrent registration is 
missed; publishing is idempotent
         registry.addListener(listener);
         registry.getTools().forEach((tag, specs) -> {
-            if (selectedTags.contains(tag)) {
+            if (matchesTag(tag)) {
                 specs.forEach(this::publish);
             }
         });
@@ -213,7 +211,7 @@ public class McpServerBridge extends ServiceSupport 
implements CamelContextAware
             }
             // the same spec may be registered under several selected tags; 
only remove when it is gone from all
             boolean stillSelected = registry.getTools().entrySet().stream()
-                    .anyMatch(e -> selectedTags.contains(e.getKey()) && 
e.getValue().contains(spec));
+                    .anyMatch(e -> matchesTag(e.getKey()) && 
e.getValue().contains(spec));
             if (!stillSelected) {
                 published.remove(spec.getName());
                 engine.toolRemoved(spec.getName());
@@ -305,19 +303,22 @@ public class McpServerBridge extends ServiceSupport 
implements CamelContextAware
         }
     }
 
+    private boolean matchesTag(String tag) {
+        return tag != null && PatternHelper.matchPatterns(tag, tagPatterns);
+    }
+
     private final class RegistryListener implements AiToolRegistryListener {
 
         @Override
         public void toolRegistered(String tag, AiToolSpec spec) {
-            // the untagged default pool (tag == null) is never exposed
-            if (tag != null && selectedTags.contains(tag) && 
isStartingOrStarted()) {
+            if (matchesTag(tag) && isStartingOrStarted()) {
                 publish(spec);
             }
         }
 
         @Override
         public void toolDeregistered(String tag, AiToolSpec spec) {
-            if (tag != null && selectedTags.contains(tag) && 
isStartingOrStarted()) {
+            if (matchesTag(tag) && isStartingOrStarted()) {
                 unpublish(spec);
             }
         }
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
index 376b4c37637a..e2f69c04d099 100644
--- 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
+++ 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
@@ -35,8 +35,9 @@ public class McpServerConfiguration {
     private long sessionIdleTtl = McpServerConstants.DEFAULT_SESSION_IDLE_TTL;
 
     /**
-     * Comma-separated list of ai-tool tags to expose as MCP tools. Only tools 
registered under one of these tags are
-     * published; the untagged default pool is never exposed. When not set, no 
tools are published.
+     * Comma-separated list of ai-tool tag patterns to expose as MCP tools. 
Patterns support exact match, wildcard
+     * prefix ({@code foo*}), and {@code *} to match all tags. Only tools 
registered under a matching tag are published;
+     * the untagged default pool is never exposed. When not set, no tools are 
published.
      */
     public String getTags() {
         return tags;
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgePrefixTagTest.java
 
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgePrefixTagTest.java
new file mode 100644
index 000000000000..9be6e551da47
--- /dev/null
+++ 
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgePrefixTagTest.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.component.mcp.server;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class McpServerBridgePrefixTagTest extends CamelTestSupport {
+
+    private final RecordingMcpServerEngine engine = new 
RecordingMcpServerEngine();
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext camelContext = super.createCamelContext();
+        camelContext.getRegistry().bind("mcpServerEngine", engine);
+        McpServerConfiguration configuration = new McpServerConfiguration();
+        configuration.setTags("crm*,notify");
+        camelContext.addService(new McpServerBridge(configuration));
+        return camelContext;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("ai-tool:tool_a?tags=crm&description=CRM tool")
+                        .setBody(constant("a"));
+
+                from("ai-tool:tool_b?tags=crm-sales&description=CRM sales 
tool")
+                        .setBody(constant("b"));
+
+                from("ai-tool:tool_c?tags=notify&description=Notify tool")
+                        .setBody(constant("c"));
+
+                from("ai-tool:tool_d?tags=billing&description=Billing tool")
+                        .setBody(constant("d"));
+
+                from("ai-tool:hidden?description=Untagged tool")
+                        .setBody(constant("hidden"));
+            }
+        };
+    }
+
+    @Test
+    void testPrefixPatternMatchesTags() {
+        assertThat(engine.tools())
+                .containsKeys("tool_a", "tool_b", "tool_c")
+                .doesNotContainKeys("tool_d", "hidden");
+    }
+}
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgeWildcardTagTest.java
 
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgeWildcardTagTest.java
new file mode 100644
index 000000000000..6b63dd36dc2a
--- /dev/null
+++ 
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/McpServerBridgeWildcardTagTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.component.mcp.server;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class McpServerBridgeWildcardTagTest extends CamelTestSupport {
+
+    private final RecordingMcpServerEngine engine = new 
RecordingMcpServerEngine();
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext camelContext = super.createCamelContext();
+        camelContext.getRegistry().bind("mcpServerEngine", engine);
+        McpServerConfiguration configuration = new McpServerConfiguration();
+        configuration.setTags("*");
+        camelContext.addService(new McpServerBridge(configuration));
+        return camelContext;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("ai-tool:tool_a?tags=crm&description=CRM tool")
+                        .setBody(constant("a"));
+
+                from("ai-tool:tool_b?tags=notify&description=Notify tool")
+                        .setBody(constant("b"));
+
+                from("ai-tool:tool_c?tags=billing&description=Billing tool")
+                        .setBody(constant("c"));
+
+                from("ai-tool:hidden?description=Untagged tool")
+                        .setBody(constant("hidden"));
+            }
+        };
+    }
+
+    @Test
+    void testWildcardMatchesAllTaggedTools() {
+        assertThat(engine.tools())
+                .containsKeys("tool_a", "tool_b", "tool_c")
+                .doesNotContainKey("hidden");
+    }
+}
diff --git a/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc 
b/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
index fdc8fdef6e3a..6b761dc26029 100644
--- a/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
+++ b/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
@@ -120,6 +120,9 @@ camel.server.mcp-tags = crm,notify
 camel.server.mcp-server-name = my-integration-app
 ----
 
+Tag patterns support wildcards: use `*` to expose all tagged tools, or a
+prefix pattern like `crm*` to expose all tags starting with `crm`:
+
 On other runtimes, or when wiring programmatically, add the
 `McpServerBridge` service to the CamelContext instead:
 
@@ -157,10 +160,11 @@ JBang (see the xref:main.adoc[camel-main] options) or on
 
 | `camel.server.mcp-enabled` | Whether to expose ai-tool routes as MCP tools
   over streamable HTTP. | `false` | bridge
-| `camel.server.mcp-tags` | Comma-separated list of ai-tool tags to expose as
-  MCP tools. Only tools registered under one of these tags are published; the
-  untagged default pool is never exposed. When not set, no tools are
-  published. |  | bridge
+| `camel.server.mcp-tags` | Comma-separated list of ai-tool tag patterns to
+  expose as MCP tools. Patterns support exact match, wildcard prefix
+  (`foo*`), and `*` to match all tags. Only tools registered under a matching
+  tag are published; the untagged default pool is never exposed. When not
+  set, no tools are published. |  | bridge
 | `camel.server.mcp-tool-timeout` | Per-call tool execution timeout in
   milliseconds. A call exceeding the timeout returns an error result to the
   MCP client; the underlying route keeps running until it completes on its
@@ -215,8 +219,9 @@ External MCP clients are *untrusted senders* under the
 xref:manual::security-model.adoc[Camel security model]. The module applies the
 following rules:
 
-* *Explicit opt-in per tool*: only tools whose tags intersect the configured
-  `tags` are exposed. The untagged default pool is never exposed implicitly.
+* *Explicit opt-in per tool*: only tools whose tags match the configured tag
+  patterns are exposed. The untagged default pool is never exposed, even when
+  using the `*` wildcard.
 * *Flat namespace protection*: a tool whose name collides with an already
   exposed tool is refused with an ERROR log — never silently replaced.
 * *Error sanitization*: route exceptions are mapped to a generic error
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
index 5285a09a12af..b319500f6e36 100644
--- 
a/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
@@ -358,8 +358,9 @@ public class HttpServerConfigurationProperties implements 
BootstrapCloseable {
     }
 
     /**
-     * Comma-separated list of ai-tool tags to expose as MCP tools. Only tools 
registered under one of these tags are
-     * exposed; the untagged default pool is never exposed. When not set, no 
tools are exposed.
+     * Comma-separated list of ai-tool tag patterns to expose as MCP tools. 
Patterns support exact match, wildcard prefix
+     * ({@code foo*}), and {@code *} to match all tags. Only tools registered 
under a matching tag are exposed; the
+     * untagged default pool is never exposed. When not set, no tools are 
exposed.
      */
     public void setMcpTags(String mcpTags) {
         this.mcpTags = mcpTags;
@@ -613,8 +614,9 @@ public class HttpServerConfigurationProperties implements 
BootstrapCloseable {
     }
 
     /**
-     * Comma-separated list of ai-tool tags to expose as MCP tools. Only tools 
registered under one of these tags are
-     * exposed; the untagged default pool is never exposed. When not set, no 
tools are exposed.
+     * Comma-separated list of ai-tool tag patterns to expose as MCP tools. 
Patterns support exact match, wildcard prefix
+     * ({@code foo*}), and {@code *} to match all tags. Only tools registered 
under a matching tag are exposed; the
+     * untagged default pool is never exposed. When not set, no tools are 
exposed.
      */
     public HttpServerConfigurationProperties withMcpTags(String mcpTags) {
         this.mcpTags = mcpTags;

Reply via email to