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

gnodet 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 169989c85cd0 CAMEL-23964: camel-openai - Add per-server MCP tool 
filtering via toolNames
169989c85cd0 is described below

commit 169989c85cd02a1ff59af6b091bf40a35d603d38
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jul 29 16:36:32 2026 +0200

    CAMEL-23964: camel-openai - Add per-server MCP tool filtering via toolNames
    
    Add a per-server toolNames include list for MCP tool filtering. When
    mcpServer.<name>.toolNames is set (comma-separated), only the named tools
    are registered from that server. The filter applies to both initial
    connection and reconnection paths.
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../apache/camel/catalog/components/openai.json    |   2 +-
 .../org/apache/camel/catalog/docs/openai-mcp.adoc  |  58 ++++
 .../org/apache/camel/component/openai/openai.json  |   2 +-
 .../camel-openai/src/main/docs/openai-mcp.adoc     |  58 ++++
 .../component/openai/OpenAIConfiguration.java      |   3 +-
 .../camel/component/openai/OpenAIEndpoint.java     |  43 ++-
 .../openai/OpenAIEndpointMcpToolFilteringTest.java | 332 +++++++++++++++++++++
 .../endpoint/dsl/OpenAIEndpointBuilderFactory.java |  10 +-
 8 files changed, 499 insertions(+), 9 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
index a8ae3a444deb..f0c60e6ac00c 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
@@ -117,7 +117,7 @@
     "maxToolIterations": { "index": 28, "kind": "parameter", "displayName": 
"Max Tool Iterations", "group": "producer", "label": "", "required": false, 
"type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "defaultValue": 50, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Maximum number of tool 
call loop iterations to prevent infinite loops" },
     "mcpProtocolVersions": { "index": 29, "kind": "parameter", "displayName": 
"Mcp Protocol Versions", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Comma-separated list of 
MCP protocol versions to advertise when connect [...]
     "mcpReconnect": { "index": 30, "kind": "parameter", "displayName": "Mcp 
Reconnect", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": true, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Automatically reconnect 
to MCP servers when a tool call fails due to a t [...]
-    "mcpServer": { "index": 31, "kind": "parameter", "displayName": "Mcp 
Server", "group": "producer", "label": "", "required": false, "type": "object", 
"javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": 
"mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "MCP (Model Cont [...]
+    "mcpServer": { "index": 31, "kind": "parameter", "displayName": "Mcp 
Server", "group": "producer", "label": "", "required": false, "type": "object", 
"javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": 
"mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "MCP (Model Cont [...]
     "mcpTimeout": { "index": 32, "kind": "parameter", "displayName": "Mcp 
Timeout", "group": "producer", "label": "", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": 20, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Timeout in seconds for MCP tool call requests. 
Applies to all MCP operations inclu [...]
     "model": { "index": 33, "kind": "parameter", "displayName": "Model", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The model to use for chat completion" },
     "outputClass": { "index": 34, "kind": "parameter", "displayName": "Output 
Class", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Fully qualified class name for structured 
output using response format" },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-mcp.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-mcp.adoc
index 866621a92f51..8635736c2da3 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-mcp.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-mcp.adoc
@@ -177,6 +177,64 @@ YAML::
 ----
 ====
 
+==== Tool Filtering
+
+By default, all tools from every configured MCP server are registered and 
advertised to the model.
+Use the per-server `toolNames` property to restrict which tools are included. 
This is useful to
+reduce prompt size and cost, keep destructive tools away from the model, or 
avoid duplicate tool
+names across servers.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:chat")
+    .to("openai:chat-completion?model=gpt-4"
+        + "&mcpServer.fs.transportType=stdio"
+        + "&mcpServer.fs.command=npx"
+        + "&mcpServer.fs.args=-y,@modelcontextprotocol/server-filesystem,/tmp"
+        + "&mcpServer.fs.toolNames=read_file,list_directory");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="direct:chat"/>
+  <to 
uri="openai:chat-completion?model=gpt-4&amp;mcpServer.fs.transportType=stdio&amp;mcpServer.fs.command=npx&amp;mcpServer.fs.args=-y,@modelcontextprotocol/server-filesystem,/tmp&amp;mcpServer.fs.toolNames=read_file,list_directory"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:chat
+      steps:
+        - to:
+            uri: openai:chat-completion
+            parameters:
+              model: gpt-4
+              mcpServer.fs.transportType: stdio
+              mcpServer.fs.command: npx
+              mcpServer.fs.args: 
"-y,@modelcontextprotocol/server-filesystem,/tmp"
+              mcpServer.fs.toolNames: read_file,list_directory
+----
+====
+
+When `toolNames` is set, only tools whose names appear in the comma-separated 
list are registered.
+Tools not in the list are silently excluded. If a name in the list does not 
match any tool provided
+by the server, a warning is logged. When `toolNames` is not set or is empty, 
all tools from the
+server are registered (the default behavior).
+
+The filter is also applied consistently on MCP server reconnection 
(`mcpReconnect=true`), so the
+tool set remains stable across connection recovery.
+
 === Agentic Loop Behavior
 
 When the model responds with tool calls, the component automatically:
diff --git 
a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
 
b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
index a8ae3a444deb..f0c60e6ac00c 100644
--- 
a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
+++ 
b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
@@ -117,7 +117,7 @@
     "maxToolIterations": { "index": 28, "kind": "parameter", "displayName": 
"Max Tool Iterations", "group": "producer", "label": "", "required": false, 
"type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "defaultValue": 50, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Maximum number of tool 
call loop iterations to prevent infinite loops" },
     "mcpProtocolVersions": { "index": 29, "kind": "parameter", "displayName": 
"Mcp Protocol Versions", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Comma-separated list of 
MCP protocol versions to advertise when connect [...]
     "mcpReconnect": { "index": 30, "kind": "parameter", "displayName": "Mcp 
Reconnect", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": true, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Automatically reconnect 
to MCP servers when a tool call fails due to a t [...]
-    "mcpServer": { "index": 31, "kind": "parameter", "displayName": "Mcp 
Server", "group": "producer", "label": "", "required": false, "type": "object", 
"javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": 
"mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "MCP (Model Cont [...]
+    "mcpServer": { "index": 31, "kind": "parameter", "displayName": "Mcp 
Server", "group": "producer", "label": "", "required": false, "type": "object", 
"javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": 
"mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "MCP (Model Cont [...]
     "mcpTimeout": { "index": 32, "kind": "parameter", "displayName": "Mcp 
Timeout", "group": "producer", "label": "", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": 20, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Timeout in seconds for MCP tool call requests. 
Applies to all MCP operations inclu [...]
     "model": { "index": 33, "kind": "parameter", "displayName": "Model", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The model to use for chat completion" },
     "outputClass": { "index": 34, "kind": "parameter", "displayName": "Output 
Class", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Fully qualified class name for structured 
output using response format" },
diff --git a/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc 
b/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc
index 866621a92f51..8635736c2da3 100644
--- a/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc
+++ b/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc
@@ -177,6 +177,64 @@ YAML::
 ----
 ====
 
+==== Tool Filtering
+
+By default, all tools from every configured MCP server are registered and 
advertised to the model.
+Use the per-server `toolNames` property to restrict which tools are included. 
This is useful to
+reduce prompt size and cost, keep destructive tools away from the model, or 
avoid duplicate tool
+names across servers.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:chat")
+    .to("openai:chat-completion?model=gpt-4"
+        + "&mcpServer.fs.transportType=stdio"
+        + "&mcpServer.fs.command=npx"
+        + "&mcpServer.fs.args=-y,@modelcontextprotocol/server-filesystem,/tmp"
+        + "&mcpServer.fs.toolNames=read_file,list_directory");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="direct:chat"/>
+  <to 
uri="openai:chat-completion?model=gpt-4&amp;mcpServer.fs.transportType=stdio&amp;mcpServer.fs.command=npx&amp;mcpServer.fs.args=-y,@modelcontextprotocol/server-filesystem,/tmp&amp;mcpServer.fs.toolNames=read_file,list_directory"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:chat
+      steps:
+        - to:
+            uri: openai:chat-completion
+            parameters:
+              model: gpt-4
+              mcpServer.fs.transportType: stdio
+              mcpServer.fs.command: npx
+              mcpServer.fs.args: 
"-y,@modelcontextprotocol/server-filesystem,/tmp"
+              mcpServer.fs.toolNames: read_file,list_directory
+----
+====
+
+When `toolNames` is set, only tools whose names appear in the comma-separated 
list are registered.
+Tools not in the list are silently excluded. If a name in the list does not 
match any tool provided
+by the server, a warning is logged. When `toolNames` is not set or is empty, 
all tools from the
+server are registered (the default behavior).
+
+The filter is also applied consistently on MCP server reconnection 
(`mcpReconnect=true`), so the
+tool set remains stable across connection recovery.
+
 === Agentic Loop Behavior
 
 When the model responds with tool calls, the component automatically:
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java
index 51c80de213f3..ba8ea8f484dd 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java
@@ -172,7 +172,8 @@ public class OpenAIConfiguration implements Cloneable {
                             + "Define servers using prefix notation: 
mcpServer.<name>.transportType=stdio|sse|streamableHttp, (Note that sse is 
deprecated) "
                             + "mcpServer.<name>.command=<cmd> (stdio), 
mcpServer.<name>.args=<comma-separated> (stdio), "
                             + "mcpServer.<name>.url=<url> 
(sse/streamableHttp), "
-                            + "mcpServer.<name>.oauthProfile=<profile> (OAuth 
profile for HTTP auth, requires camel-oauth)")
+                            + "mcpServer.<name>.oauthProfile=<profile> (OAuth 
profile for HTTP auth, requires camel-oauth), "
+                            + "mcpServer.<name>.toolNames=<comma-separated> 
(optional include list to restrict which tools are registered from this 
server)")
     private Map<String, Object> mcpServer;
 
     @UriParam(defaultValue = "50")
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
index db715e187a6e..e33369776aa3 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
@@ -196,7 +196,7 @@ public class OpenAIEndpoint extends DefaultEndpoint {
             LOG.debug("MCP server '{}' initialized, listing tools", 
serverName);
 
             McpSchema.ListToolsResult toolsResult = mcpClient.listTools();
-            List<McpSchema.Tool> serverTools = toolsResult.tools();
+            List<McpSchema.Tool> serverTools = 
filterTools(toolsResult.tools(), serverName, props);
 
             for (McpSchema.Tool tool : serverTools) {
                 if (toolClientMap.putIfAbsent(tool.name(), mcpClient) != null) 
{
@@ -363,7 +363,7 @@ public class OpenAIEndpoint extends DefaultEndpoint {
         try {
             McpSyncClient newClient = createMcpClient(serverName, props);
 
-            List<McpSchema.Tool> tools = newClient.listTools().tools();
+            List<McpSchema.Tool> tools = 
filterTools(newClient.listTools().tools(), serverName, props);
 
             globalMcpLock.lock();
             try {
@@ -412,6 +412,45 @@ public class OpenAIEndpoint extends DefaultEndpoint {
         }
     }
 
+    /**
+     * Filters the tools listed from an MCP server according to the per-server 
{@code toolNames} include list. When no
+     * {@code toolNames} property is configured, all tools are returned 
unchanged.
+     *
+     * @param  allTools   the full list of tools from the server
+     * @param  serverName the logical server name (for logging)
+     * @param  props      the per-server configuration properties
+     * @return            the filtered tool list (or the original list if no 
filter is configured)
+     */
+    private List<McpSchema.Tool> filterTools(List<McpSchema.Tool> allTools, 
String serverName, Map<String, String> props) {
+        String toolNamesCsv = props.get("toolNames");
+        if (toolNamesCsv == null || toolNamesCsv.isBlank()) {
+            return allTools;
+        }
+
+        Set<String> allowed = new HashSet<>();
+        for (String name : toolNamesCsv.split(",")) {
+            String trimmed = name.trim();
+            if (!trimmed.isEmpty()) {
+                allowed.add(trimmed);
+            }
+        }
+
+        List<McpSchema.Tool> filtered = allTools.stream()
+                .filter(t -> allowed.contains(t.name()))
+                .toList();
+
+        Set<String> found = 
filtered.stream().map(McpSchema.Tool::name).collect(Collectors.toSet());
+        Set<String> missing = new HashSet<>(allowed);
+        missing.removeAll(found);
+        if (!missing.isEmpty()) {
+            LOG.warn("MCP server '{}' does not provide the following 
toolNames: {}", serverName, missing);
+        }
+
+        LOG.info("MCP server '{}': filtered {} tools to {} via toolNames 
include list",
+                serverName, allTools.size(), filtered.size());
+        return filtered;
+    }
+
     private Set<String> toolsForServer(String serverName) {
         return mcpToolState.toolToServerName().entrySet().stream()
                 .filter(e -> serverName.equals(e.getValue()))
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEndpointMcpToolFilteringTest.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEndpointMcpToolFilteringTest.java
new file mode 100644
index 000000000000..39f2f23497ca
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEndpointMcpToolFilteringTest.java
@@ -0,0 +1,332 @@
+/*
+ * 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.openai;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.openai.client.OpenAIClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for CAMEL-23964: per-server MCP tool filtering via the {@code 
toolNames} include list.
+ */
+class OpenAIEndpointMcpToolFilteringTest {
+
+    private static final String SERVER_A = "serverA";
+    private static final String SERVER_B = "serverB";
+    private static final List<String> ALL_TOOLS = List.of("read_file", 
"write_file", "list_directory", "delete_file");
+
+    /**
+     * Overrides the client-creation seam so that tests return mock MCP 
clients instead of opening real transports, and
+     * stubs createClient to avoid needing real API keys.
+     */
+    private static class TestEndpoint extends OpenAIEndpoint {
+        private final Map<String, List<String>> serverToolNames;
+
+        TestEndpoint(OpenAIComponent component, OpenAIConfiguration config,
+                     Map<String, List<String>> serverToolNames) {
+            super("openai:chat-completion", component, config);
+            this.serverToolNames = serverToolNames;
+        }
+
+        @Override
+        McpSyncClient createMcpClient(String serverName, Map<String, String> 
props) {
+            List<String> toolNames = serverToolNames.getOrDefault(serverName, 
List.of());
+            McpSyncClient client = mock(McpSyncClient.class);
+            List<McpSchema.Tool> tools = toolNames.stream()
+                    .map(n -> McpSchema.Tool.builder(n, Map.of("type", 
"object")).description("mock " + n).build())
+                    .toList();
+            
when(client.listTools()).thenReturn(McpSchema.ListToolsResult.builder(tools).build());
+            return client;
+        }
+
+        @Override
+        protected OpenAIClient createClient() {
+            return mock(OpenAIClient.class);
+        }
+    }
+
+    private TestEndpoint createAndStartEndpoint(
+            Map<String, Map<String, String>> serverConfigs,
+            Map<String, List<String>> serverToolNames)
+            throws Exception {
+        DefaultCamelContext ctx = new DefaultCamelContext();
+        OpenAIComponent component = new OpenAIComponent();
+        component.setCamelContext(ctx);
+
+        // Build the flat mcpServer config map that OpenAIConfiguration expects
+        Map<String, Object> flatConfig = new HashMap<>();
+        for (Map.Entry<String, Map<String, String>> entry : 
serverConfigs.entrySet()) {
+            String name = entry.getKey();
+            for (Map.Entry<String, String> prop : entry.getValue().entrySet()) 
{
+                flatConfig.put(name + "." + prop.getKey(), prop.getValue());
+            }
+        }
+
+        OpenAIConfiguration config = new OpenAIConfiguration();
+        config.setMcpServer(flatConfig);
+
+        TestEndpoint endpoint = new TestEndpoint(component, config, 
serverToolNames);
+        endpoint.setCamelContext(ctx);
+        endpoint.setOperation(OpenAIOperations.chatCompletion);
+        endpoint.doStart();
+        return endpoint;
+    }
+
+    @Test
+    void toolNamesFilterKeepsOnlySpecifiedTools() throws Exception {
+        Map<String, Map<String, String>> serverConfigs = Map.of(
+                SERVER_A, Map.of("transportType", "stdio", "command", "echo", 
"toolNames", "read_file,list_directory"));
+
+        Map<String, List<String>> serverToolNames = Map.of(SERVER_A, 
ALL_TOOLS);
+
+        TestEndpoint endpoint = createAndStartEndpoint(serverConfigs, 
serverToolNames);
+
+        McpToolState state = endpoint.getMcpToolState();
+        Set<String> registeredNames = state.tools().stream()
+                .map(t -> t.function().name())
+                .collect(Collectors.toSet());
+
+        assertThat(registeredNames).containsExactlyInAnyOrder("read_file", 
"list_directory");
+        assertThat(registeredNames).doesNotContain("write_file", 
"delete_file");
+
+        // Also verify tool-to-client and tool-to-server maps are consistent
+        assertThat(state.toolClientMap()).containsOnlyKeys("read_file", 
"list_directory");
+        assertThat(state.toolToServerName()).containsOnlyKeys("read_file", 
"list_directory");
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void noToolNamesPropertyRegistersAllTools() throws Exception {
+        Map<String, Map<String, String>> serverConfigs = Map.of(
+                SERVER_A, Map.of("transportType", "stdio", "command", "echo"));
+
+        Map<String, List<String>> serverToolNames = Map.of(SERVER_A, 
ALL_TOOLS);
+
+        TestEndpoint endpoint = createAndStartEndpoint(serverConfigs, 
serverToolNames);
+
+        McpToolState state = endpoint.getMcpToolState();
+        Set<String> registeredNames = state.tools().stream()
+                .map(t -> t.function().name())
+                .collect(Collectors.toSet());
+
+        assertThat(registeredNames).containsExactlyInAnyOrder("read_file", 
"write_file", "list_directory", "delete_file");
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void emptyToolNamesPropertyRegistersAllTools() throws Exception {
+        Map<String, Map<String, String>> serverConfigs = Map.of(
+                SERVER_A, Map.of("transportType", "stdio", "command", "echo", 
"toolNames", ""));
+
+        Map<String, List<String>> serverToolNames = Map.of(SERVER_A, 
ALL_TOOLS);
+
+        TestEndpoint endpoint = createAndStartEndpoint(serverConfigs, 
serverToolNames);
+
+        McpToolState state = endpoint.getMcpToolState();
+        Set<String> registeredNames = state.tools().stream()
+                .map(t -> t.function().name())
+                .collect(Collectors.toSet());
+
+        assertThat(registeredNames).containsExactlyInAnyOrder("read_file", 
"write_file", "list_directory", "delete_file");
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void toolNamesWithSpacesAreTrimmed() throws Exception {
+        Map<String, Map<String, String>> serverConfigs = Map.of(
+                SERVER_A, Map.of("transportType", "stdio", "command", "echo", 
"toolNames", " read_file , list_directory "));
+
+        Map<String, List<String>> serverToolNames = Map.of(SERVER_A, 
ALL_TOOLS);
+
+        TestEndpoint endpoint = createAndStartEndpoint(serverConfigs, 
serverToolNames);
+
+        McpToolState state = endpoint.getMcpToolState();
+        Set<String> registeredNames = state.tools().stream()
+                .map(t -> t.function().name())
+                .collect(Collectors.toSet());
+
+        assertThat(registeredNames).containsExactlyInAnyOrder("read_file", 
"list_directory");
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void multipleServersWithDifferentFilters() throws Exception {
+        Map<String, Map<String, String>> serverConfigs = new HashMap<>();
+        serverConfigs.put(SERVER_A, Map.of("transportType", "stdio", 
"command", "echo", "toolNames", "read_file"));
+        serverConfigs.put(SERVER_B, Map.of("transportType", "stdio", 
"command", "echo", "toolNames", "search"));
+
+        Map<String, List<String>> serverToolNames = new HashMap<>();
+        serverToolNames.put(SERVER_A, ALL_TOOLS);
+        serverToolNames.put(SERVER_B, List.of("search", "index", "delete"));
+
+        TestEndpoint endpoint = createAndStartEndpoint(serverConfigs, 
serverToolNames);
+
+        McpToolState state = endpoint.getMcpToolState();
+        Set<String> registeredNames = state.tools().stream()
+                .map(t -> t.function().name())
+                .collect(Collectors.toSet());
+
+        assertThat(registeredNames).containsExactlyInAnyOrder("read_file", 
"search");
+
+        // Verify correct server mapping
+        
assertThat(state.toolToServerName().get("read_file")).isEqualTo(SERVER_A);
+        assertThat(state.toolToServerName().get("search")).isEqualTo(SERVER_B);
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void oneServerFilteredOtherUnfiltered() throws Exception {
+        Map<String, Map<String, String>> serverConfigs = new HashMap<>();
+        serverConfigs.put(SERVER_A, Map.of("transportType", "stdio", 
"command", "echo", "toolNames", "read_file"));
+        serverConfigs.put(SERVER_B, Map.of("transportType", "stdio", 
"command", "echo"));
+
+        Map<String, List<String>> serverToolNames = new HashMap<>();
+        serverToolNames.put(SERVER_A, ALL_TOOLS);
+        serverToolNames.put(SERVER_B, List.of("search", "index"));
+
+        TestEndpoint endpoint = createAndStartEndpoint(serverConfigs, 
serverToolNames);
+
+        McpToolState state = endpoint.getMcpToolState();
+        Set<String> registeredNames = state.tools().stream()
+                .map(t -> t.function().name())
+                .collect(Collectors.toSet());
+
+        // Server A: only read_file (filtered); Server B: search and index 
(unfiltered)
+        assertThat(registeredNames).containsExactlyInAnyOrder("read_file", 
"search", "index");
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void toolNamesFilterAppliedOnReconnect() throws Exception {
+        // Set up a server with toolNames filter
+        Map<String, Map<String, String>> serverConfigs = new HashMap<>();
+        serverConfigs.put(SERVER_A,
+                new HashMap<>(Map.of("transportType", "stdio", "command", 
"echo", "toolNames", "read_file,list_directory")));
+
+        Map<String, List<String>> serverToolNames = new HashMap<>();
+        serverToolNames.put(SERVER_A, ALL_TOOLS);
+
+        TestEndpoint endpoint = createAndStartEndpoint(serverConfigs, 
serverToolNames);
+
+        // Verify initial state is filtered
+        McpToolState initialState = endpoint.getMcpToolState();
+        assertThat(initialState.tools()).hasSize(2);
+
+        // Now trigger reconnect — the filter should still apply
+        McpSyncClient oldClient = 
initialState.toolClientMap().get("read_file");
+        McpSyncClient newClient = endpoint.reconnectMcpServer(oldClient, 
"read_file");
+        assertThat(newClient).isNotNull();
+
+        // Verify the reconnected state still has only the filtered tools
+        McpToolState reconnectedState = endpoint.getMcpToolState();
+        Set<String> reconnectedNames = reconnectedState.tools().stream()
+                .map(t -> t.function().name())
+                .collect(Collectors.toSet());
+
+        assertThat(reconnectedNames).containsExactlyInAnyOrder("read_file", 
"list_directory");
+        assertThat(reconnectedNames).doesNotContain("write_file", 
"delete_file");
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void singleToolNameFilter() throws Exception {
+        Map<String, Map<String, String>> serverConfigs = Map.of(
+                SERVER_A, Map.of("transportType", "stdio", "command", "echo", 
"toolNames", "delete_file"));
+
+        Map<String, List<String>> serverToolNames = Map.of(SERVER_A, 
ALL_TOOLS);
+
+        TestEndpoint endpoint = createAndStartEndpoint(serverConfigs, 
serverToolNames);
+
+        McpToolState state = endpoint.getMcpToolState();
+        Set<String> registeredNames = state.tools().stream()
+                .map(t -> t.function().name())
+                .collect(Collectors.toSet());
+
+        assertThat(registeredNames).containsExactly("delete_file");
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void returnDirectToolsRespectedWithFilter() throws Exception {
+        DefaultCamelContext ctx = new DefaultCamelContext();
+        OpenAIComponent component = new OpenAIComponent();
+        component.setCamelContext(ctx);
+
+        Map<String, Object> flatConfig = new HashMap<>();
+        flatConfig.put(SERVER_A + ".transportType", "stdio");
+        flatConfig.put(SERVER_A + ".command", "echo");
+        flatConfig.put(SERVER_A + ".toolNames", "read_file,direct_tool");
+
+        OpenAIConfiguration config = new OpenAIConfiguration();
+        config.setMcpServer(flatConfig);
+
+        // Create endpoint with a tool that has returnDirect=true annotation
+        McpSchema.ToolAnnotations returnDirectAnnotations = 
McpSchema.ToolAnnotations.builder()
+                .returnDirect(true)
+                .build();
+
+        TestEndpoint endpoint = new TestEndpoint(
+                component, config,
+                Map.of(SERVER_A, List.of("read_file", "direct_tool", 
"other_tool"))) {
+            @Override
+            McpSyncClient createMcpClient(String serverName, Map<String, 
String> props) {
+                McpSyncClient client = mock(McpSyncClient.class);
+                List<McpSchema.Tool> tools = List.of(
+                        McpSchema.Tool.builder("read_file", Map.of("type", 
"object")).description("read").build(),
+                        McpSchema.Tool.builder("direct_tool", Map.of("type", 
"object"))
+                                .description("direct")
+                                .annotations(returnDirectAnnotations)
+                                .build(),
+                        McpSchema.Tool.builder("other_tool", Map.of("type", 
"object")).description("other").build());
+                
when(client.listTools()).thenReturn(McpSchema.ListToolsResult.builder(tools).build());
+                return client;
+            }
+        };
+        endpoint.setCamelContext(ctx);
+        endpoint.setOperation(OpenAIOperations.chatCompletion);
+        endpoint.doStart();
+
+        McpToolState state = endpoint.getMcpToolState();
+
+        // Only read_file and direct_tool should be registered
+        assertThat(state.tools()).hasSize(2);
+        assertThat(state.returnDirectTools()).containsExactly("direct_tool");
+        assertThat(state.returnDirectTools()).doesNotContain("other_tool");
+
+        endpoint.doStop();
+    }
+}
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
index 2f003379e2f3..62deec247e96 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
@@ -800,8 +800,9 @@ public interface OpenAIEndpointBuilderFactory {
          * mcpServer..transportType=stdiossestreamableHttp, (Note that sse is
          * deprecated) mcpServer..command= (stdio), mcpServer..args= (stdio),
          * mcpServer..url= (sse/streamableHttp), mcpServer..oauthProfile= 
(OAuth
-         * profile for HTTP auth, requires camel-oauth). This is a multi-value
-         * option with prefix: mcpServer.
+         * profile for HTTP auth, requires camel-oauth), mcpServer..toolNames=
+         * (optional include list to restrict which tools are registered from
+         * this server). This is a multi-value option with prefix: mcpServer.
          * 
          * The option is a: <code>java.util.Map&lt;java.lang.String,
          * java.lang.Object&gt;</code> type.
@@ -825,8 +826,9 @@ public interface OpenAIEndpointBuilderFactory {
          * mcpServer..transportType=stdiossestreamableHttp, (Note that sse is
          * deprecated) mcpServer..command= (stdio), mcpServer..args= (stdio),
          * mcpServer..url= (sse/streamableHttp), mcpServer..oauthProfile= 
(OAuth
-         * profile for HTTP auth, requires camel-oauth). This is a multi-value
-         * option with prefix: mcpServer.
+         * profile for HTTP auth, requires camel-oauth), mcpServer..toolNames=
+         * (optional include list to restrict which tools are registered from
+         * this server). This is a multi-value option with prefix: mcpServer.
          * 
          * The option is a: <code>java.util.Map&lt;java.lang.String,
          * java.lang.Object&gt;</code> type.

Reply via email to