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&mcpServer.fs.transportType=stdio&mcpServer.fs.command=npx&mcpServer.fs.args=-y,@modelcontextprotocol/server-filesystem,/tmp&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&mcpServer.fs.transportType=stdio&mcpServer.fs.command=npx&mcpServer.fs.args=-y,@modelcontextprotocol/server-filesystem,/tmp&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<java.lang.String,
* java.lang.Object></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<java.lang.String,
* java.lang.Object></code> type.