This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 265137c5545d CAMEL-23948: Apply mcpToolProviderFilter to
endpoint-level MCP clients
265137c5545d is described below
commit 265137c5545d7055912f0df61e2d33241db40b04
Author: Omar Atie <[email protected]>
AuthorDate: Thu Jul 23 23:34:54 2026 -0700
CAMEL-23948: Apply mcpToolProviderFilter to endpoint-level MCP clients
Endpoint-level MCP clients (mcpClients and mcpServer.*) now honor the same
mcpToolProviderFilter configured on AgentConfiguration or registry
AbstractAgent
beans, matching the behavior already implemented for agent-level MCP
clients.
Closes #25037
Co-authored-by: Cursor <[email protected]>
---
.../camel/catalog/docs/langchain4j-agent-mcp.adoc | 2 +
.../langchain4j/agent/api/AbstractAgent.java | 17 ++
.../src/main/docs/langchain4j-agent-mcp.adoc | 2 +
.../agent/LangChain4jAgentProducer.java | 28 +++-
.../LangChain4jAgentMcpToolProviderFilterTest.java | 176 +++++++++++++++++++++
.../langchain4j/agent/support/StubMcpClient.java | 141 +++++++++++++++++
6 files changed, 365 insertions(+), 1 deletion(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-agent-mcp.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-agent-mcp.adoc
index 354e1175f6d0..0271864965c1 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-agent-mcp.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-agent-mcp.adoc
@@ -262,6 +262,8 @@ YAML::
Both `mcpClients` (bean references) and `mcpServer` (inline) can be used
together on the same endpoint. All tool sources -- Camel route tools,
endpoint-level MCP tools, and agent-level MCP tools -- are automatically
composed into a single tool provider.
+When MCP clients are configured at the endpoint level, the same
`withMcpToolProviderFilter(...)` predicate configured on the endpoint's
`agentConfiguration` (or on a registry `AbstractAgent` bean) is applied to
those clients as well. This keeps filtering behavior consistent regardless of
whether MCP clients are declared on the agent or on the endpoint URI.
+
=== Dynamic Tool Exclusion via Headers
Tools can be dynamically excluded on a per-message basis using Camel headers.
This allows routes to control which tools are available based on business
logic, user roles, or message content.
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
index bc837d6c2c42..9c0689f8f51a 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
@@ -18,8 +18,11 @@ package org.apache.camel.component.langchain4j.agent.api;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.BiPredicate;
+import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.mcp.McpToolProvider;
+import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.model.chat.request.ResponseFormat;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
@@ -61,6 +64,20 @@ public abstract class AbstractAgent<S> implements Agent {
return configuration;
}
+ /**
+ * Returns the MCP tool provider filter from the given agent's
configuration.
+ *
+ * @param agent the abstract agent, may be {@code null}
+ * @return the filter predicate, or {@code null} when the agent is
{@code null} or no filter is configured
+ * @since 4.22
+ */
+ public static BiPredicate<McpClient, ToolSpecification>
mcpToolProviderFilter(AbstractAgent<?> agent) {
+ if (agent == null) {
+ return null;
+ }
+ return agent.getConfiguration().getMcpToolProviderFilter();
+ }
+
/**
* Gets the response format for structured output.
*
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-mcp.adoc
b/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-mcp.adoc
index 354e1175f6d0..0271864965c1 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-mcp.adoc
+++
b/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-mcp.adoc
@@ -262,6 +262,8 @@ YAML::
Both `mcpClients` (bean references) and `mcpServer` (inline) can be used
together on the same endpoint. All tool sources -- Camel route tools,
endpoint-level MCP tools, and agent-level MCP tools -- are automatically
composed into a single tool provider.
+When MCP clients are configured at the endpoint level, the same
`withMcpToolProviderFilter(...)` predicate configured on the endpoint's
`agentConfiguration` (or on a registry `AbstractAgent` bean) is applied to
those clients as well. This keeps filtering behavior consistent regardless of
whether MCP clients are declared on the agent or on the endpoint URI.
+
=== Dynamic Tool Exclusion via Headers
Tools can be dynamically excluded on a per-message basis using Camel headers.
This allows routes to control which tools are available based on business
logic, user roles, or message content.
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
index 139ff4ad55a1..63d98402041c 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
@@ -26,6 +26,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -208,7 +209,12 @@ public class LangChain4jAgentProducer extends
DefaultProducer {
if (!allMcpClients.isEmpty()) {
LOG.debug("Adding {} MCP clients to tool provider",
allMcpClients.size());
-
providers.add(McpToolProvider.builder().mcpClients(allMcpClients).build());
+ McpToolProvider.Builder mcpBuilder =
McpToolProvider.builder().mcpClients(allMcpClients);
+ BiPredicate<McpClient, ToolSpecification> mcpFilter =
resolveMcpToolProviderFilter();
+ if (mcpFilter != null) {
+ mcpBuilder.filter(mcpFilter);
+ }
+ providers.add(mcpBuilder.build());
}
if (providers.isEmpty()) {
@@ -220,6 +226,26 @@ public class LangChain4jAgentProducer extends
DefaultProducer {
}
}
+ /**
+ * Resolves the MCP tool filter configured on the endpoint {@link
AgentConfiguration} or, when the agent is an
+ * {@link AbstractAgent}, from the agent's own configuration.
+ */
+ private BiPredicate<McpClient, ToolSpecification>
resolveMcpToolProviderFilter() {
+ AgentConfiguration endpointAgentConfiguration =
endpoint.getConfiguration().getAgentConfiguration();
+ if (endpointAgentConfiguration != null &&
endpointAgentConfiguration.getMcpToolProviderFilter() != null) {
+ return endpointAgentConfiguration.getMcpToolProviderFilter();
+ }
+
+ Agent configuredAgent = endpoint.getConfiguration().getAgent();
+ if (configuredAgent == null) {
+ configuredAgent = agent;
+ }
+ if (configuredAgent instanceof AbstractAgent<?> abstractAgent) {
+ return AbstractAgent.mcpToolProviderFilter(abstractAgent);
+ }
+ return null;
+ }
+
/**
* Creates a tool provider for Camel route tools discovered by tags. If
the {@link Headers#EXCLUDE_TAGS} header is
* set on the exchange, the specified tags (comma-separated) are excluded
from discovery.
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentMcpToolProviderFilterTest.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentMcpToolProviderFilterTest.java
new file mode 100644
index 000000000000..1b029e156dcb
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentMcpToolProviderFilterTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.langchain4j.agent;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.mcp.client.McpClient;
+import dev.langchain4j.service.Result;
+import dev.langchain4j.service.tool.ToolProvider;
+import dev.langchain4j.service.tool.ToolProviderRequest;
+import dev.langchain4j.service.tool.ToolProviderResult;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.langchain4j.agent.api.Agent;
+import org.apache.camel.component.langchain4j.agent.api.AgentConfiguration;
+import org.apache.camel.component.langchain4j.agent.api.AgentWithoutMemory;
+import org.apache.camel.component.langchain4j.agent.api.AiAgentBody;
+import org.apache.camel.component.langchain4j.agent.support.StubMcpClient;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Reproducer and regression tests for CAMEL-23948: endpoint-level MCP clients
must honor
+ * {@link
AgentConfiguration#withMcpToolProviderFilter(java.util.function.BiPredicate)}
the same way as MCP clients
+ * configured directly on the agent configuration.
+ */
+class LangChain4jAgentMcpToolProviderFilterTest extends CamelTestSupport {
+
+ private static final ToolProviderRequest TOOL_REQUEST = new
ToolProviderRequest("test", UserMessage.from("hello"));
+
+ private static final ToolSpecification WEATHER_TOOL =
ToolSpecification.builder()
+ .name("get_weather")
+ .description("Returns weather for a city")
+ .build();
+
+ private static final ToolSpecification INVENTORY_TOOL =
ToolSpecification.builder()
+ .name("check_inventory")
+ .description("Returns stock levels")
+ .build();
+
+ private final AtomicReference<ToolProvider> capturedToolProvider = new
AtomicReference<>();
+
+ @BeforeEach
+ void resetCapturedToolProvider() {
+ capturedToolProvider.set(null);
+ }
+
+ @Override
+ public boolean isUseAdviceWith() {
+ return true;
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:reject-all").to("langchain4j-agent:reject-all");
+ from("direct:selective").to("langchain4j-agent:selective");
+ from("direct:no-filter").to("langchain4j-agent:no-filter");
+
from("direct:registry-agent").to("langchain4j-agent:registry-agent");
+ }
+ };
+ }
+
+ @Test
+ void
shouldRejectAllEndpointMcpToolsWhenFilterConfiguredOnAgentConfiguration() {
+ configureEndpoint("langchain4j-agent:reject-all",
+ new AgentConfiguration().withMcpToolProviderFilter((client,
tool) -> false),
+ List.of(new StubMcpClient("weather-server", WEATHER_TOOL)),
+ capturingAgent());
+ context.start();
+
+ template.sendBody("direct:reject-all", new AiAgentBody<>("hello"));
+
+ assertThat(capturedToolProvider.get()).isNotNull();
+ assertThat(toolNames(capturedToolProvider.get())).isEmpty();
+ }
+
+ @Test
+ void shouldApplySelectiveFilterToEndpointMcpClients() {
+ configureEndpoint("langchain4j-agent:selective",
+ new AgentConfiguration().withMcpToolProviderFilter((client,
tool) -> "get_weather".equals(tool.name())),
+ List.of(new StubMcpClient("inventory-server", WEATHER_TOOL,
INVENTORY_TOOL)),
+ capturingAgent());
+ context.start();
+
+ template.sendBody("direct:selective", new AiAgentBody<>("hello"));
+
+ assertThat(toolNames(capturedToolProvider.get()))
+ .containsExactly("get_weather");
+ }
+
+ @Test
+ void shouldExposeAllEndpointMcpToolsWhenNoFilterConfigured() {
+ configureEndpoint("langchain4j-agent:no-filter",
+ new AgentConfiguration(),
+ List.of(new StubMcpClient("inventory-server", WEATHER_TOOL,
INVENTORY_TOOL)),
+ capturingAgent());
+ context.start();
+
+ template.sendBody("direct:no-filter", new AiAgentBody<>("hello"));
+
+ assertThat(toolNames(capturedToolProvider.get()))
+ .containsExactlyInAnyOrder("get_weather", "check_inventory");
+ }
+
+ @Test
+ void shouldApplyFilterFromRegistryAgentWhenEndpointHasMcpClients() {
+ AgentConfiguration registryConfig = new AgentConfiguration()
+ .withMcpToolProviderFilter((client, tool) -> false);
+ Agent registryAgent = new AgentWithoutMemory(registryConfig) {
+ @Override
+ public Result<String> chat(AiAgentBody<?> aiAgentBody,
ToolProvider toolProvider) {
+ capturedToolProvider.set(toolProvider);
+ return Result.<String> builder().content("ok").build();
+ }
+ };
+
+ configureEndpoint("langchain4j-agent:registry-agent", null,
+ List.of(new StubMcpClient("weather-server", WEATHER_TOOL)),
registryAgent);
+ context.start();
+
+ template.sendBody("direct:registry-agent", new AiAgentBody<>("hello"));
+
+ assertThat(toolNames(capturedToolProvider.get())).isEmpty();
+ }
+
+ private Agent capturingAgent() {
+ return new AgentWithoutMemory(new AgentConfiguration()) {
+ @Override
+ public Result<String> chat(AiAgentBody<?> aiAgentBody,
ToolProvider toolProvider) {
+ capturedToolProvider.set(toolProvider);
+ return Result.<String> builder().content("ok").build();
+ }
+ };
+ }
+
+ private void configureEndpoint(
+ String endpointUri, AgentConfiguration agentConfiguration,
List<McpClient> mcpClients, Agent agent) {
+ LangChain4jAgentEndpoint endpoint = context.getEndpoint(endpointUri,
LangChain4jAgentEndpoint.class);
+ endpoint.getConfiguration().setAgentConfiguration(agentConfiguration);
+ endpoint.getConfiguration().setMcpClients(mcpClients);
+ endpoint.getConfiguration().setAgent(agent);
+ }
+
+ private static Set<String> toolNames(ToolProvider toolProvider) {
+ assertThat(toolProvider).isNotNull();
+ ToolProviderResult result = toolProvider.provideTools(TOOL_REQUEST);
+ return result.tools().keySet().stream()
+ .map(ToolSpecification::name)
+ .collect(Collectors.toSet());
+ }
+}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/support/StubMcpClient.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/support/StubMcpClient.java
new file mode 100644
index 000000000000..897a85cb3665
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/support/StubMcpClient.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.langchain4j.agent.support;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import dev.langchain4j.agent.tool.ToolExecutionRequest;
+import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.invocation.InvocationContext;
+import dev.langchain4j.mcp.client.McpClient;
+import dev.langchain4j.mcp.client.McpGetPromptResult;
+import dev.langchain4j.mcp.client.McpPrompt;
+import dev.langchain4j.mcp.client.McpReadResourceResult;
+import dev.langchain4j.mcp.client.McpResource;
+import dev.langchain4j.mcp.client.McpResourceTemplate;
+import dev.langchain4j.mcp.client.McpRoot;
+import dev.langchain4j.service.tool.ToolExecutionResult;
+
+/**
+ * Minimal in-memory {@link McpClient} for unit tests.
+ */
+public final class StubMcpClient implements McpClient {
+
+ private final String key;
+ private final List<ToolSpecification> tools;
+
+ public StubMcpClient(String key, List<ToolSpecification> tools) {
+ this.key = key;
+ this.tools = List.copyOf(tools);
+ }
+
+ public StubMcpClient(String key, ToolSpecification... tools) {
+ this(key, List.of(tools));
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public List<ToolSpecification> listTools() {
+ return tools;
+ }
+
+ @Override
+ public List<ToolSpecification> listTools(InvocationContext
invocationContext) {
+ return listTools();
+ }
+
+ @Override
+ public ToolExecutionResult executeTool(ToolExecutionRequest
executionRequest) {
+ throw new UnsupportedOperationException("Not needed for filter tests");
+ }
+
+ @Override
+ public ToolExecutionResult executeTool(ToolExecutionRequest
executionRequest, InvocationContext invocationContext) {
+ throw new UnsupportedOperationException("Not needed for filter tests");
+ }
+
+ @Override
+ public List<McpResource> listResources() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<McpResource> listResources(InvocationContext
invocationContext) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<McpResourceTemplate> listResourceTemplates() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<McpResourceTemplate> listResourceTemplates(InvocationContext
invocationContext) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public McpReadResourceResult readResource(String uri) {
+ throw new UnsupportedOperationException("Not needed for filter tests");
+ }
+
+ @Override
+ public McpReadResourceResult readResource(String uri, InvocationContext
invocationContext) {
+ throw new UnsupportedOperationException("Not needed for filter tests");
+ }
+
+ @Override
+ public void subscribeToResource(String uri) {
+ throw new UnsupportedOperationException("Not needed for filter tests");
+ }
+
+ @Override
+ public void unsubscribeFromResource(String uri) {
+ throw new UnsupportedOperationException("Not needed for filter tests");
+ }
+
+ @Override
+ public List<McpPrompt> listPrompts() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public McpGetPromptResult getPrompt(String name, Map<String, Object>
arguments) {
+ throw new UnsupportedOperationException("Not needed for filter tests");
+ }
+
+ @Override
+ public void checkHealth() {
+ // no-op
+ }
+
+ @Override
+ public void setRoots(List<McpRoot> roots) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+}