This is an automated email from the ASF dual-hosted git repository.
fmariani 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 df6ee42c664e CAMEL-16861: Add MCP client support to
camel-langchain4j-agent
df6ee42c664e is described below
commit df6ee42c664e28db2b913d4f5de2ce799c3907c3
Author: Croway <[email protected]>
AuthorDate: Tue Feb 24 19:01:57 2026 +0100
CAMEL-16861: Add MCP client support to camel-langchain4j-agent
---
.../catalog/components/langchain4j-agent.json | 12 +-
.../langchain4j/agent/api/AbstractAgent.java | 15 +-
.../agent/api/CompositeToolProvider.java | 57 ++++
.../component/langchain4j/agent/api/Headers.java | 8 +
.../agent/api/CompositeToolProviderTest.java | 148 +++++++++
.../camel-ai/camel-langchain4j-agent/pom.xml | 11 +
.../agent/LangChain4jAgentComponentConfigurer.java | 23 ++
.../LangChain4jAgentConfigurationConfigurer.java | 23 ++
.../agent/LangChain4jAgentEndpointConfigurer.java | 23 ++
.../agent/LangChain4jAgentEndpointUriFactory.java | 8 +-
.../langchain4j/agent/langchain4j-agent.json | 12 +-
.../src/main/docs/langchain4j-agent-component.adoc | 91 +++++-
.../agent/LangChain4jAgentConfiguration.java | 53 ++++
.../agent/LangChain4jAgentProducer.java | 212 ++++++++++++-
.../agent/LangChain4jMcpServerDefinition.java | 238 +++++++++++++++
.../LangChain4jAgentMcpAndCamelToolsIT.java | 330 +++++++++++++++++++++
.../src/test/resources/log4j2.properties | 30 ++
.../Langchain4jAgentComponentBuilderFactory.java | 40 +++
.../LangChain4jAgentEndpointBuilderFactory.java | 106 +++++++
test-infra/camel-test-infra-mcp-everything/pom.xml | 53 ++++
.../everything/common/McpEverythingProperties.java | 28 ++
.../services/McpEverythingInfraService.java | 36 +++
.../McpEverythingLocalContainerInfraService.java | 121 ++++++++
.../everything/services/McpEverythingService.java | 26 ++
.../services/McpEverythingServiceFactory.java | 39 +++
.../mcp/everything/services/container.properties | 18 ++
test-infra/pom.xml | 1 +
27 files changed, 1735 insertions(+), 27 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json
index 28dd67fe2a67..593d99e60555 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json
@@ -29,19 +29,25 @@
"configuration": { "index": 2, "kind": "property", "displayName":
"Configuration", "group": "producer", "label": "", "required": false, "type":
"object", "javaType":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
configuration" },
"lazyStartProducer": { "index": 3, "kind": "property", "displayName":
"Lazy Start Producer", "group": "producer", "label": "producer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether the producer should be started lazy (on the first message). By
starting lazy you can use this to allow CamelContext and routes to startup in
situations where a producer may otherwise fail [...]
"tags": { "index": 4, "kind": "property", "displayName": "Tags", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "Tags for discovering and
calling Camel route tools" },
- "autowiredEnabled": { "index": 5, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "autowiredEnabled": { "index": 5, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "mcpClients": { "index": 6, "kind": "property", "displayName": "Mcp
Clients", "group": "advanced", "label": "advanced", "required": false, "type":
"array", "javaType": "java.util.List<dev.langchain4j.mcp.client.McpClient>",
"deprecated": false, "autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "Pre-built MCP (Model
Context Protocol) client insta [...]
+ "mcpServer": { "index": 7, "kind": "property", "displayName": "Mcp
Server", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>",
"prefix": "mcpServer.", "multiValue": true, "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "MCP server [...]
},
"headers": {
"CamelLangChain4jAgentSystemMessage": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The system prompt.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#SYSTEM_MESSAGE" },
"CamelLangChain4jAgentMemoryId": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Memory ID.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEMORY_ID" },
"CamelLangChain4jAgentUserMessage": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The user message to accompany file
content when using WrappedFile as input.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#USER_MESSAGE" },
- "CamelLangChain4jAgentMediaType": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The media type (MIME type) of the file
content. Overrides auto-detection from file extension.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEDIA_TYPE" }
+ "CamelLangChain4jAgentMediaType": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The media type (MIME type) of the file
content. Overrides auto-detection from file extension.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEDIA_TYPE" },
+ "CamelLangChain4jAgentExcludeTags": { "index": 4, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Comma-separated list of Camel tool tags
to exclude from this agent invocation.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#EXCLUDE_TAGS" },
+ "CamelLangChain4jAgentExcludeMcpServers": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Comma-separated list of MCP server
names (keys) to exclude from this agent invocation.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#EXCLUDE_MCP_SERVERS" }
},
"properties": {
"agentId": { "index": 0, "kind": "path", "displayName": "Agent Id",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The Agent id" },
"agent": { "index": 1, "kind": "parameter", "displayName": "Agent",
"group": "producer", "label": "", "required": false, "type": "object",
"javaType": "org.apache.camel.component.langchain4j.agent.api.Agent",
"deprecated": false, "deprecationNote": "", "autowired": true, "secret": false,
"configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "The agent to use for the
component" },
"agentFactory": { "index": 2, "kind": "parameter", "displayName": "Agent
Factory", "group": "producer", "label": "", "required": false, "type":
"object", "javaType":
"org.apache.camel.component.langchain4j.agent.api.AgentFactory", "deprecated":
false, "deprecationNote": "", "autowired": true, "secret": false,
"configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "The agent factory to u
[...]
"tags": { "index": 3, "kind": "parameter", "displayName": "Tags", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "Tags for discovering and
calling Camel route tools" },
- "lazyStartProducer": { "index": 4, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "lazyStartProducer": { "index": 4, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "mcpClients": { "index": 5, "kind": "parameter", "displayName": "Mcp
Clients", "group": "advanced", "label": "advanced", "required": false, "type":
"array", "javaType": "java.util.List<dev.langchain4j.mcp.client.McpClient>",
"deprecated": false, "autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "Pre-built MCP (Model
Context Protocol) client inst [...]
+ "mcpServer": { "index": 6, "kind": "parameter", "displayName": "Mcp
Server", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>",
"prefix": "mcpServer.", "multiValue": true, "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "MCP server [...]
}
}
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 f7a19d62973b..4334011cab94 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
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.langchain4j.agent.api;
+import java.util.ArrayList;
import java.util.List;
import dev.langchain4j.mcp.McpToolProvider;
@@ -77,9 +78,12 @@ public abstract class AbstractAgent<S> implements Agent {
*/
@SuppressWarnings("unchecked")
protected void configureBuilder(AiServices<S> builder, ToolProvider
toolProvider) {
+ // Collect all tool providers to compose them into a single provider
+ List<ToolProvider> toolProviders = new ArrayList<>();
+
// Apache Camel Tool Provider
if (toolProvider != null) {
- builder.toolProvider(toolProvider);
+ toolProviders.add(toolProvider);
}
// MCP Clients - create MCP ToolProvider if MCP clients are configured
@@ -92,7 +96,14 @@ public abstract class AbstractAgent<S> implements Agent {
mcpBuilder.filter(configuration.getMcpToolProviderFilter());
}
- builder.toolProvider(mcpBuilder.build());
+ toolProviders.add(mcpBuilder.build());
+ }
+
+ // Set the composed tool provider (single or composite)
+ if (toolProviders.size() == 1) {
+ builder.toolProvider(toolProviders.get(0));
+ } else if (toolProviders.size() > 1) {
+ builder.toolProvider(new CompositeToolProvider(toolProviders));
}
// Additional custom LangChain4j Tool Instances (objects with @Tool
methods)
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/CompositeToolProvider.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/CompositeToolProvider.java
new file mode 100644
index 000000000000..da50c9c4c3a6
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/CompositeToolProvider.java
@@ -0,0 +1,57 @@
+/*
+ * 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.api;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.langchain4j.service.tool.ToolProvider;
+import dev.langchain4j.service.tool.ToolProviderRequest;
+import dev.langchain4j.service.tool.ToolProviderResult;
+
+/**
+ * A composite {@link ToolProvider} that aggregates tools from multiple
underlying {@link ToolProvider} instances.
+ *
+ * <p>
+ * This class enables the coexistence of different tool sources (e.g., Camel
route tools and MCP tools) within a single
+ * AI agent. When {@link #provideTools(ToolProviderRequest)} is called, it
delegates to each underlying provider and
+ * merges all returned tools into a single {@link ToolProviderResult}.
+ * </p>
+ *
+ * @since 4.19.0
+ */
+public class CompositeToolProvider implements ToolProvider {
+
+ private final List<ToolProvider> providers;
+
+ public CompositeToolProvider(List<ToolProvider> providers) {
+ this.providers = new ArrayList<>(providers);
+ }
+
+ @Override
+ public ToolProviderResult provideTools(ToolProviderRequest request) {
+ ToolProviderResult.Builder resultBuilder =
ToolProviderResult.builder();
+
+ for (ToolProvider provider : providers) {
+ ToolProviderResult result = provider.provideTools(request);
+ result.tools().forEach(resultBuilder::add);
+
resultBuilder.immediateReturnToolNames(result.immediateReturnToolNames());
+ }
+
+ return resultBuilder.build();
+ }
+}
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java
index c6a56b3a26df..a68606da9674 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java
@@ -35,4 +35,12 @@ public class Headers {
@Metadata(description = "The media type (MIME type) of the file content.
Overrides auto-detection from file extension.",
javaType = "String")
public static final String MEDIA_TYPE = "CamelLangChain4jAgentMediaType";
+
+ @Metadata(description = "Comma-separated list of Camel tool tags to
exclude from this agent invocation.",
+ javaType = "String")
+ public static final String EXCLUDE_TAGS =
"CamelLangChain4jAgentExcludeTags";
+
+ @Metadata(description = "Comma-separated list of MCP server names (keys)
to exclude from this agent invocation.",
+ javaType = "String")
+ public static final String EXCLUDE_MCP_SERVERS =
"CamelLangChain4jAgentExcludeMcpServers";
}
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/CompositeToolProviderTest.java
b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/CompositeToolProviderTest.java
new file mode 100644
index 000000000000..907247077c3f
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/CompositeToolProviderTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.api;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.service.tool.ToolExecutor;
+import dev.langchain4j.service.tool.ToolProvider;
+import dev.langchain4j.service.tool.ToolProviderRequest;
+import dev.langchain4j.service.tool.ToolProviderResult;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CompositeToolProviderTest {
+
+ private static final ToolExecutor NOOP_EXECUTOR = (request, memoryId) ->
"ok";
+ private static final ToolProviderRequest DUMMY_REQUEST = new
ToolProviderRequest("test", UserMessage.from("test"));
+
+ @Test
+ public void testSingleProvider() {
+ ToolSpecification spec =
ToolSpecification.builder().name("tool1").description("A tool").build();
+
+ ToolProvider provider = request -> ToolProviderResult.builder()
+ .add(spec, NOOP_EXECUTOR)
+ .build();
+
+ CompositeToolProvider composite = new
CompositeToolProvider(List.of(provider));
+ ToolProviderResult result = composite.provideTools(DUMMY_REQUEST);
+
+ assertNotNull(result);
+ assertEquals(1, result.tools().size());
+ assertTrue(result.tools().containsKey(spec));
+ }
+
+ @Test
+ public void testMultipleProviders() {
+ ToolSpecification spec1 =
ToolSpecification.builder().name("tool1").description("Tool 1").build();
+ ToolSpecification spec2 =
ToolSpecification.builder().name("tool2").description("Tool 2").build();
+
+ ToolProvider provider1 = request -> ToolProviderResult.builder()
+ .add(spec1, NOOP_EXECUTOR)
+ .build();
+
+ ToolProvider provider2 = request -> ToolProviderResult.builder()
+ .add(spec2, NOOP_EXECUTOR)
+ .build();
+
+ CompositeToolProvider composite = new
CompositeToolProvider(List.of(provider1, provider2));
+ ToolProviderResult result = composite.provideTools(DUMMY_REQUEST);
+
+ assertNotNull(result);
+ assertEquals(2, result.tools().size());
+ assertTrue(result.tools().containsKey(spec1));
+ assertTrue(result.tools().containsKey(spec2));
+ }
+
+ @Test
+ public void testEmptyProviderList() {
+ CompositeToolProvider composite = new
CompositeToolProvider(Collections.emptyList());
+ ToolProviderResult result = composite.provideTools(DUMMY_REQUEST);
+
+ assertNotNull(result);
+ assertTrue(result.tools().isEmpty());
+ }
+
+ @Test
+ public void testDuplicateToolNamesThrows() {
+ ToolSpecification spec1 =
ToolSpecification.builder().name("sameName").description("Tool 1").build();
+ ToolSpecification spec2 =
ToolSpecification.builder().name("sameName").description("Tool 2").build();
+
+ ToolProvider provider1 = request -> ToolProviderResult.builder()
+ .add(spec1, NOOP_EXECUTOR)
+ .build();
+
+ ToolProvider provider2 = request -> ToolProviderResult.builder()
+ .add(spec2, NOOP_EXECUTOR)
+ .build();
+
+ CompositeToolProvider composite = new
CompositeToolProvider(List.of(provider1, provider2));
+
+ // LangChain4j ToolProviderResult.Builder throws on duplicate names
+ assertThrows(Exception.class, () ->
composite.provideTools(DUMMY_REQUEST));
+ }
+
+ @Test
+ public void testImmediateReturnToolNamesMerged() {
+ ToolSpecification spec1 =
ToolSpecification.builder().name("tool1").description("Tool 1").build();
+ ToolSpecification spec2 =
ToolSpecification.builder().name("tool2").description("Tool 2").build();
+
+ ToolProvider provider1 = request -> ToolProviderResult.builder()
+ .add(spec1, NOOP_EXECUTOR)
+ .immediateReturnToolNames(Set.of("tool1"))
+ .build();
+
+ ToolProvider provider2 = request -> ToolProviderResult.builder()
+ .add(spec2, NOOP_EXECUTOR)
+ .immediateReturnToolNames(Set.of("tool2"))
+ .build();
+
+ CompositeToolProvider composite = new
CompositeToolProvider(List.of(provider1, provider2));
+ ToolProviderResult result = composite.provideTools(DUMMY_REQUEST);
+
+ assertNotNull(result);
+ assertEquals(2, result.tools().size());
+ assertTrue(result.immediateReturnToolNames().contains("tool1"));
+ assertTrue(result.immediateReturnToolNames().contains("tool2"));
+ }
+
+ @Test
+ public void testProviderWithNoTools() {
+ ToolSpecification spec1 =
ToolSpecification.builder().name("tool1").description("Tool 1").build();
+
+ ToolProvider provider1 = request -> ToolProviderResult.builder()
+ .add(spec1, NOOP_EXECUTOR)
+ .build();
+
+ ToolProvider emptyProvider = request ->
ToolProviderResult.builder().build();
+
+ CompositeToolProvider composite = new
CompositeToolProvider(List.of(provider1, emptyProvider));
+ ToolProviderResult result = composite.provideTools(DUMMY_REQUEST);
+
+ assertNotNull(result);
+ assertEquals(1, result.tools().size());
+ assertTrue(result.tools().containsKey(spec1));
+ }
+}
diff --git a/components/camel-ai/camel-langchain4j-agent/pom.xml
b/components/camel-ai/camel-langchain4j-agent/pom.xml
index 35ca9854931f..947f894048b9 100644
--- a/components/camel-ai/camel-langchain4j-agent/pom.xml
+++ b/components/camel-ai/camel-langchain4j-agent/pom.xml
@@ -55,6 +55,11 @@
<artifactId>langchain4j</artifactId>
<version>${langchain4j-version}</version>
</dependency>
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-mcp</artifactId>
+ <version>${langchain4j-beta-version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-langchain4j-tools</artifactId>
@@ -109,6 +114,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-infra-mcp-everything</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentComponentConfigurer.java
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentComponentConfigurer.java
index 319f695178b3..35edb4062ab1 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentComponentConfigurer.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentComponentConfigurer.java
@@ -38,6 +38,10 @@ public class LangChain4jAgentComponentConfigurer extends
PropertyConfigurerSuppo
case "configuration": target.setConfiguration(property(camelContext,
org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration.class,
value)); return true;
case "lazystartproducer":
case "lazyStartProducer":
target.setLazyStartProducer(property(camelContext, boolean.class, value));
return true;
+ case "mcpclients":
+ case "mcpClients":
getOrCreateConfiguration(target).setMcpClients(property(camelContext,
java.util.List.class, value)); return true;
+ case "mcpserver":
+ case "mcpServer":
getOrCreateConfiguration(target).setMcpServer(property(camelContext,
java.util.Map.class, value)); return true;
case "tags":
getOrCreateConfiguration(target).setTags(property(camelContext,
java.lang.String.class, value)); return true;
default: return false;
}
@@ -59,6 +63,10 @@ public class LangChain4jAgentComponentConfigurer extends
PropertyConfigurerSuppo
case "configuration": return
org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
+ case "mcpclients":
+ case "mcpClients": return java.util.List.class;
+ case "mcpserver":
+ case "mcpServer": return java.util.Map.class;
case "tags": return java.lang.String.class;
default: return null;
}
@@ -76,9 +84,24 @@ public class LangChain4jAgentComponentConfigurer extends
PropertyConfigurerSuppo
case "configuration": return target.getConfiguration();
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
+ case "mcpclients":
+ case "mcpClients": return
getOrCreateConfiguration(target).getMcpClients();
+ case "mcpserver":
+ case "mcpServer": return
getOrCreateConfiguration(target).getMcpServer();
case "tags": return getOrCreateConfiguration(target).getTags();
default: return null;
}
}
+
+ @Override
+ public Object getCollectionValueType(Object target, String name, boolean
ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase() : name) {
+ case "mcpclients":
+ case "mcpClients": return dev.langchain4j.mcp.client.McpClient.class;
+ case "mcpserver":
+ case "mcpServer": return java.lang.Object.class;
+ default: return null;
+ }
+ }
}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConfigurationConfigurer.java
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConfigurationConfigurer.java
index 4e61552d23a1..69a1f67aaede 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConfigurationConfigurer.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConfigurationConfigurer.java
@@ -26,6 +26,10 @@ public class LangChain4jAgentConfigurationConfigurer extends
org.apache.camel.su
case "agent": target.setAgent(property(camelContext,
org.apache.camel.component.langchain4j.agent.api.Agent.class, value)); return
true;
case "agentfactory":
case "agentFactory": target.setAgentFactory(property(camelContext,
org.apache.camel.component.langchain4j.agent.api.AgentFactory.class, value));
return true;
+ case "mcpclients":
+ case "mcpClients": target.setMcpClients(property(camelContext,
java.util.List.class, value)); return true;
+ case "mcpserver":
+ case "mcpServer": target.setMcpServer(property(camelContext,
java.util.Map.class, value)); return true;
case "tags": target.setTags(property(camelContext,
java.lang.String.class, value)); return true;
default: return false;
}
@@ -37,6 +41,10 @@ public class LangChain4jAgentConfigurationConfigurer extends
org.apache.camel.su
case "agent": return
org.apache.camel.component.langchain4j.agent.api.Agent.class;
case "agentfactory":
case "agentFactory": return
org.apache.camel.component.langchain4j.agent.api.AgentFactory.class;
+ case "mcpclients":
+ case "mcpClients": return java.util.List.class;
+ case "mcpserver":
+ case "mcpServer": return java.util.Map.class;
case "tags": return java.lang.String.class;
default: return null;
}
@@ -49,9 +57,24 @@ public class LangChain4jAgentConfigurationConfigurer extends
org.apache.camel.su
case "agent": return target.getAgent();
case "agentfactory":
case "agentFactory": return target.getAgentFactory();
+ case "mcpclients":
+ case "mcpClients": return target.getMcpClients();
+ case "mcpserver":
+ case "mcpServer": return target.getMcpServer();
case "tags": return target.getTags();
default: return null;
}
}
+
+ @Override
+ public Object getCollectionValueType(Object target, String name, boolean
ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase() : name) {
+ case "mcpclients":
+ case "mcpClients": return dev.langchain4j.mcp.client.McpClient.class;
+ case "mcpserver":
+ case "mcpServer": return java.lang.Object.class;
+ default: return null;
+ }
+ }
}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentEndpointConfigurer.java
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentEndpointConfigurer.java
index a5643be29293..ff73e3cd6af3 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentEndpointConfigurer.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentEndpointConfigurer.java
@@ -28,6 +28,10 @@ public class LangChain4jAgentEndpointConfigurer extends
PropertyConfigurerSuppor
case "agentFactory":
target.getConfiguration().setAgentFactory(property(camelContext,
org.apache.camel.component.langchain4j.agent.api.AgentFactory.class, value));
return true;
case "lazystartproducer":
case "lazyStartProducer":
target.setLazyStartProducer(property(camelContext, boolean.class, value));
return true;
+ case "mcpclients":
+ case "mcpClients":
target.getConfiguration().setMcpClients(property(camelContext,
java.util.List.class, value)); return true;
+ case "mcpserver":
+ case "mcpServer":
target.getConfiguration().setMcpServer(property(camelContext,
java.util.Map.class, value)); return true;
case "tags": target.getConfiguration().setTags(property(camelContext,
java.lang.String.class, value)); return true;
default: return false;
}
@@ -46,6 +50,10 @@ public class LangChain4jAgentEndpointConfigurer extends
PropertyConfigurerSuppor
case "agentFactory": return
org.apache.camel.component.langchain4j.agent.api.AgentFactory.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
+ case "mcpclients":
+ case "mcpClients": return java.util.List.class;
+ case "mcpserver":
+ case "mcpServer": return java.util.Map.class;
case "tags": return java.lang.String.class;
default: return null;
}
@@ -60,9 +68,24 @@ public class LangChain4jAgentEndpointConfigurer extends
PropertyConfigurerSuppor
case "agentFactory": return
target.getConfiguration().getAgentFactory();
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
+ case "mcpclients":
+ case "mcpClients": return target.getConfiguration().getMcpClients();
+ case "mcpserver":
+ case "mcpServer": return target.getConfiguration().getMcpServer();
case "tags": return target.getConfiguration().getTags();
default: return null;
}
}
+
+ @Override
+ public Object getCollectionValueType(Object target, String name, boolean
ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase() : name) {
+ case "mcpclients":
+ case "mcpClients": return dev.langchain4j.mcp.client.McpClient.class;
+ case "mcpserver":
+ case "mcpServer": return java.lang.Object.class;
+ default: return null;
+ }
+ }
}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentEndpointUriFactory.java
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentEndpointUriFactory.java
index b32835caf0fa..952244774942 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentEndpointUriFactory.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentEndpointUriFactory.java
@@ -23,15 +23,19 @@ public class LangChain4jAgentEndpointUriFactory extends
org.apache.camel.support
private static final Set<String> SECRET_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(5);
+ Set<String> props = new HashSet<>(7);
props.add("agent");
props.add("agentFactory");
props.add("agentId");
props.add("lazyStartProducer");
+ props.add("mcpClients");
+ props.add("mcpServer");
props.add("tags");
PROPERTY_NAMES = Collections.unmodifiableSet(props);
SECRET_PROPERTY_NAMES = Collections.emptySet();
- MULTI_VALUE_PREFIXES = Collections.emptyMap();
+ Map<String, String> prefixes = new HashMap<>(1);
+ prefixes.put("mcpServer", "mcpServer.");
+ MULTI_VALUE_PREFIXES = Collections.unmodifiableMap(prefixes);
}
@Override
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json
b/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json
index 28dd67fe2a67..593d99e60555 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json
+++
b/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json
@@ -29,19 +29,25 @@
"configuration": { "index": 2, "kind": "property", "displayName":
"Configuration", "group": "producer", "label": "", "required": false, "type":
"object", "javaType":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
configuration" },
"lazyStartProducer": { "index": 3, "kind": "property", "displayName":
"Lazy Start Producer", "group": "producer", "label": "producer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether the producer should be started lazy (on the first message). By
starting lazy you can use this to allow CamelContext and routes to startup in
situations where a producer may otherwise fail [...]
"tags": { "index": 4, "kind": "property", "displayName": "Tags", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "Tags for discovering and
calling Camel route tools" },
- "autowiredEnabled": { "index": 5, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "autowiredEnabled": { "index": 5, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "mcpClients": { "index": 6, "kind": "property", "displayName": "Mcp
Clients", "group": "advanced", "label": "advanced", "required": false, "type":
"array", "javaType": "java.util.List<dev.langchain4j.mcp.client.McpClient>",
"deprecated": false, "autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "Pre-built MCP (Model
Context Protocol) client insta [...]
+ "mcpServer": { "index": 7, "kind": "property", "displayName": "Mcp
Server", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>",
"prefix": "mcpServer.", "multiValue": true, "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "MCP server [...]
},
"headers": {
"CamelLangChain4jAgentSystemMessage": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The system prompt.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#SYSTEM_MESSAGE" },
"CamelLangChain4jAgentMemoryId": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Memory ID.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEMORY_ID" },
"CamelLangChain4jAgentUserMessage": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The user message to accompany file
content when using WrappedFile as input.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#USER_MESSAGE" },
- "CamelLangChain4jAgentMediaType": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The media type (MIME type) of the file
content. Overrides auto-detection from file extension.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEDIA_TYPE" }
+ "CamelLangChain4jAgentMediaType": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The media type (MIME type) of the file
content. Overrides auto-detection from file extension.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEDIA_TYPE" },
+ "CamelLangChain4jAgentExcludeTags": { "index": 4, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Comma-separated list of Camel tool tags
to exclude from this agent invocation.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#EXCLUDE_TAGS" },
+ "CamelLangChain4jAgentExcludeMcpServers": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Comma-separated list of MCP server
names (keys) to exclude from this agent invocation.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#EXCLUDE_MCP_SERVERS" }
},
"properties": {
"agentId": { "index": 0, "kind": "path", "displayName": "Agent Id",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The Agent id" },
"agent": { "index": 1, "kind": "parameter", "displayName": "Agent",
"group": "producer", "label": "", "required": false, "type": "object",
"javaType": "org.apache.camel.component.langchain4j.agent.api.Agent",
"deprecated": false, "deprecationNote": "", "autowired": true, "secret": false,
"configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "The agent to use for the
component" },
"agentFactory": { "index": 2, "kind": "parameter", "displayName": "Agent
Factory", "group": "producer", "label": "", "required": false, "type":
"object", "javaType":
"org.apache.camel.component.langchain4j.agent.api.AgentFactory", "deprecated":
false, "deprecationNote": "", "autowired": true, "secret": false,
"configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "The agent factory to u
[...]
"tags": { "index": 3, "kind": "parameter", "displayName": "Tags", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "Tags for discovering and
calling Camel route tools" },
- "lazyStartProducer": { "index": 4, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "lazyStartProducer": { "index": 4, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "mcpClients": { "index": 5, "kind": "parameter", "displayName": "Mcp
Clients", "group": "advanced", "label": "advanced", "required": false, "type":
"array", "javaType": "java.util.List<dev.langchain4j.mcp.client.McpClient>",
"deprecated": false, "autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "Pre-built MCP (Model
Context Protocol) client inst [...]
+ "mcpServer": { "index": 6, "kind": "parameter", "displayName": "Mcp
Server", "group": "advanced", "label": "advanced", "required": false, "type":
"object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>",
"prefix": "mcpServer.", "multiValue": true, "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.langchain4j.agent.LangChain4jAgentConfiguration",
"configurationField": "configuration", "description": "MCP server [...]
}
}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc
b/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc
index 551433aee59e..aa1416c042f8 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc
+++
b/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc
@@ -463,10 +463,99 @@ from("direct:chat")
[source,java]
----
-String response = template.requestBody("direct:chat",
+String response = template.requestBody("direct:chat",
"Can you list the files in the current directory and read the content of
file.txt?", String.class);
----
+==== Endpoint-Level MCP Configuration
+
+MCP servers can also be configured directly on the endpoint, without setting
them on the `AgentConfiguration`. This is useful for declarative route
definitions and allows MCP tools to coexist with Camel route tools (via `tags`).
+
+===== Bean References
+
+Register pre-built `McpClient` beans and reference them via the `mcpClients`
endpoint parameter:
+
+[source,java]
+----
+// Register MCP clients as beans
+McpClient timeClient = new DefaultMcpClient.Builder()
+ .key("time")
+ .transport(new StdioMcpTransport.Builder()
+ .command(Arrays.asList("docker", "run", "-i", "--rm", "mcp/time"))
+ .build())
+ .build();
+context.getRegistry().bind("timeMcpClient", timeClient);
+
+// Reference in endpoint URI
+from("direct:chat")
+
.to("langchain4j-agent:assistant?agent=#myAgent&tags=users&mcpClients=#timeMcpClient")
+ .to("mock:response");
+----
+
+===== Inline URI Configuration
+
+MCP servers can be defined inline on the endpoint URI using the
`mcpServer.<name>.<property>` pattern. The component creates and manages the
MCP client lifecycle automatically.
+
+Supported properties:
+
+[cols="1,1,3"]
+|===
+| Property | Default | Description
+
+| `transportType` | `stdio` | Transport type: `stdio`, `http` (Streamable
HTTP), or `sse` (legacy SSE)
+| `command` | | Comma-separated command for stdio transport (e.g.,
`docker,run,-i,--rm,mcp/time`)
+| `url` | | Server URL for `http` or `sse` transport
+| `environment.<key>` | | Environment variables for stdio transport
+| `timeout` | `60` | Timeout in seconds
+| `logRequests` | `false` | Log requests sent to the MCP server
+| `logResponses` | `false` | Log responses from the MCP server
+|===
+
+Example with two MCP servers configured inline:
+
+[source,java]
+----
+from("direct:chat")
+ .to("langchain4j-agent:assistant?agent=#myAgent&tags=users"
+ + "&mcpServer.everything.transportType=http"
+ + "&mcpServer.everything.url=http://localhost:3001/mcp"
+ + "&mcpServer.time.transportType=stdio"
+ + "&mcpServer.time.command=docker,run,-i,--rm,mcp/time")
+ .to("mock:response");
+----
+
+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.
+
+==== 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.
+
+[cols="1,3"]
+|===
+| Header | Description
+
+| `CamelLangChain4jAgentExcludeTags` | Comma-separated list of Camel tool tags
to exclude
+| `CamelLangChain4jAgentExcludeMcpServers` | Comma-separated list of MCP
server names (keys) to exclude
+|===
+
+Example -- exclude specific tools based on a condition:
+
+[source,java]
+----
+from("direct:chat")
+ .choice()
+ .when(header("userRole").isEqualTo("readonly"))
+ // Exclude write-capable tools for read-only users
+ .setHeader("CamelLangChain4jAgentExcludeTags",
constant("admin-tools"))
+ .setHeader("CamelLangChain4jAgentExcludeMcpServers",
constant("filesystem"))
+ .end()
+ .to("langchain4j-agent:assistant?agent=#myAgent&tags=users,admin-tools"
+ + "&mcpClients=#fsMcpClient,#timeMcpClient")
+ .to("mock:response");
+----
+
+MCP servers are matched by their key, which is set via
`DefaultMcpClient.Builder.key(name)` or automatically from the server name in
inline `mcpServer.<name>` configuration. MCP clients without a key are never
excluded.
+
=== RAG Integration
RAG (Retrieval-Augmented Generation) is supported by configuring a
`RetrievalAugmentor` in the `AgentConfiguration`. Create an agent with RAG
capabilities:
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConfiguration.java
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConfiguration.java
index fa886ec04508..8ef15e74a167 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConfiguration.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConfiguration.java
@@ -16,6 +16,10 @@
*/
package org.apache.camel.component.langchain4j.agent;
+import java.util.List;
+import java.util.Map;
+
+import dev.langchain4j.mcp.client.McpClient;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.langchain4j.agent.api.Agent;
import org.apache.camel.component.langchain4j.agent.api.AgentFactory;
@@ -39,6 +43,19 @@ public class LangChain4jAgentConfiguration implements
Cloneable {
@UriParam(description = "Tags for discovering and calling Camel route
tools")
private String tags;
+ @UriParam(description = "Pre-built MCP (Model Context Protocol) client
instances for external tool integration."
+ + " Reference beans from the registry, e.g.,
#myMcpClient1,#myMcpClient2",
+ label = "advanced")
+ private List<McpClient> mcpClients;
+
+ @UriParam(description = "MCP server definitions in the form of
mcpServer.<name>.<property>=<value>."
+ + " Supported properties: transportType (stdio or
http, default: stdio),"
+ + " command (comma-separated, for stdio), url (for
http),"
+ + " environment.<key>=<value> (for stdio), timeout
(in seconds, default: 60),"
+ + " logRequests, logResponses.",
+ prefix = "mcpServer.", multiValue = true, label = "advanced")
+ private Map<String, Object> mcpServer;
+
public LangChain4jAgentConfiguration() {
}
@@ -88,4 +105,40 @@ public class LangChain4jAgentConfiguration implements
Cloneable {
public void setAgentFactory(AgentFactory agentFactory) {
this.agentFactory = agentFactory;
}
+
+ /**
+ * Pre-built MCP client instances for external tool integration
+ *
+ * @return the list of MCP clients
+ */
+ public List<McpClient> getMcpClients() {
+ return mcpClients;
+ }
+
+ public void setMcpClients(List<McpClient> mcpClients) {
+ this.mcpClients = mcpClients;
+ }
+
+ /**
+ * MCP server definitions for inline URI configuration.
+ *
+ * <p>
+ * The map keys are in the form {@code <serverName>.<property>} and are
collected from URI parameters with the
+ * {@code mcpServer.} prefix. For example:
+ * </p>
+ *
+ * <pre>
+ *
mcpServer.weather.transportType=http&mcpServer.weather.url=http://localhost:8080
+ *
mcpServer.fs.transportType=stdio&mcpServer.fs.command=npx,-y,@modelcontextprotocol/server-filesystem
+ * </pre>
+ *
+ * @return the map of MCP server properties
+ */
+ public Map<String, Object> getMcpServer() {
+ return mcpServer;
+ }
+
+ public void setMcpServer(Map<String, Object> mcpServer) {
+ this.mcpServer = mcpServer;
+ }
}
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 41fc57ea0001..f74743644275 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
@@ -16,12 +16,15 @@
*/
package org.apache.camel.component.langchain4j.agent;
+import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.mcp.McpToolProvider;
+import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.service.tool.ToolExecutor;
import dev.langchain4j.service.tool.ToolProvider;
import dev.langchain4j.service.tool.ToolProviderRequest;
@@ -30,6 +33,8 @@ import org.apache.camel.Exchange;
import org.apache.camel.component.langchain4j.agent.api.Agent;
import org.apache.camel.component.langchain4j.agent.api.AgentFactory;
import org.apache.camel.component.langchain4j.agent.api.AiAgentBody;
+import org.apache.camel.component.langchain4j.agent.api.CompositeToolProvider;
+import org.apache.camel.component.langchain4j.agent.api.Headers;
import
org.apache.camel.component.langchain4j.tools.spec.CamelToolExecutorCache;
import
org.apache.camel.component.langchain4j.tools.spec.CamelToolSpecification;
import org.apache.camel.support.DefaultProducer;
@@ -44,6 +49,7 @@ public class LangChain4jAgentProducer extends DefaultProducer
{
private final ObjectMapper objectMapper = new ObjectMapper();
private AgentFactory agentFactory;
private Agent agent;
+ private List<McpClient> materializedMcpClients;
public LangChain4jAgentProducer(LangChain4jAgentEndpoint endpoint) {
super(endpoint);
@@ -75,34 +81,102 @@ public class LangChain4jAgentProducer extends
DefaultProducer {
AiAgentBody<?> aiAgentBody = agent.processBody(messagePayload,
exchange);
- ToolProvider toolProvider = createCamelToolProvider(tags, exchange);
+ ToolProvider toolProvider = createComposedToolProvider(tags, exchange);
String response = agent.chat(aiAgentBody, toolProvider);
exchange.getMessage().setBody(response);
}
/**
- * We create our own Tool Provider
+ * Creates a composed tool provider that aggregates tools from all
configured sources: Camel route tools (via tags)
+ * and MCP tools (via endpoint-level mcpClients and mcpServers
configuration).
*
- * @param tags
- * @param exchange
+ * <p>
+ * Supports dynamic filtering via exchange headers:
+ * <ul>
+ * <li>{@link Headers#EXCLUDE_TAGS} — exclude Camel tools by tag (handled
in
+ * {@link #createCamelToolProvider(String, Exchange)})</li>
+ * <li>{@link Headers#EXCLUDE_MCP_SERVERS} — exclude MCP servers by
key/name</li>
+ * </ul>
*/
- private ToolProvider createCamelToolProvider(String tags, Exchange
exchange) {
- ToolProvider toolProvider = null;
- if (tags != null && !tags.trim().isEmpty()) {
- // Discover tools from Camel LangChain4j Tools routes
- Map<String, CamelToolSpecification> availableTools =
discoverToolsByName(tags);
+ private ToolProvider createComposedToolProvider(String tags, Exchange
exchange) {
+ List<ToolProvider> providers = new ArrayList<>();
- if (!availableTools.isEmpty()) {
- LOG.debug("Creating AI Service with {} tools for tags: {}",
availableTools.size(), tags);
+ // 1. Camel route tools from tags (handles EXCLUDE_TAGS header
internally)
+ ToolProvider camelToolProvider = createCamelToolProvider(tags,
exchange);
+ if (camelToolProvider != null) {
+ providers.add(camelToolProvider);
+ }
- // Create dynamic tool provider that returns Camel routes as
tools
- toolProvider = createCamelToolProvider(availableTools,
exchange);
+ // 2. MCP tools from endpoint configuration (pre-built clients +
materialized from server definitions)
+ List<McpClient> allMcpClients = new ArrayList<>();
- } else {
- LOG.debug("No tools found for tags: {}, using simple AI
Service", tags);
+ List<McpClient> endpointMcpClients =
endpoint.getConfiguration().getMcpClients();
+ if (endpointMcpClients != null) {
+ allMcpClients.addAll(endpointMcpClients);
+ }
+ if (materializedMcpClients != null) {
+ allMcpClients.addAll(materializedMcpClients);
+ }
+
+ // Apply MCP server exclusion from header
+ String excludeMcpServers =
exchange.getIn().getHeader(Headers.EXCLUDE_MCP_SERVERS, String.class);
+ if (excludeMcpServers != null && !excludeMcpServers.trim().isEmpty()) {
+ Set<String> excludeSet = new
HashSet<>(Arrays.asList(ToolsTagsHelper.splitTags(excludeMcpServers)));
+ allMcpClients = allMcpClients.stream()
+ .filter(client -> client.key() == null ||
!excludeSet.contains(client.key()))
+ .collect(Collectors.toList());
+ LOG.debug("After MCP server exclusion (excluded: {}): {} clients
remaining",
+ excludeMcpServers, allMcpClients.size());
+ }
+
+ if (!allMcpClients.isEmpty()) {
+ LOG.debug("Adding {} MCP clients to tool provider",
allMcpClients.size());
+
providers.add(McpToolProvider.builder().mcpClients(allMcpClients).build());
+ }
+
+ if (providers.isEmpty()) {
+ return null;
+ } else if (providers.size() == 1) {
+ return providers.get(0);
+ } else {
+ return new CompositeToolProvider(providers);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ private ToolProvider createCamelToolProvider(String tags, Exchange
exchange) {
+ if (tags == null || tags.trim().isEmpty()) {
+ return null;
+ }
+
+ // Apply tag exclusion from header
+ String excludeTags = exchange.getIn().getHeader(Headers.EXCLUDE_TAGS,
String.class);
+ if (excludeTags != null && !excludeTags.trim().isEmpty()) {
+ Set<String> excludeSet = new
HashSet<>(Arrays.asList(ToolsTagsHelper.splitTags(excludeTags)));
+ String[] allTags = ToolsTagsHelper.splitTags(tags);
+ tags = Arrays.stream(allTags)
+ .filter(t -> !excludeSet.contains(t))
+ .collect(Collectors.joining(","));
+ LOG.debug("After tag exclusion (excluded: {}): remaining tags:
{}", excludeTags, tags);
+ if (tags.isEmpty()) {
+ LOG.debug("All Camel tool tags excluded by header");
+ return null;
}
}
- return toolProvider;
+
+ // Discover tools from Camel LangChain4j Tools routes
+ Map<String, CamelToolSpecification> availableTools =
discoverToolsByName(tags);
+
+ if (!availableTools.isEmpty()) {
+ LOG.debug("Creating AI Service with {} tools for tags: {}",
availableTools.size(), tags);
+ return createCamelToolProvider(availableTools, exchange);
+ } else {
+ LOG.debug("No tools found for tags: {}, using simple AI Service",
tags);
+ return null;
+ }
}
/**
@@ -191,5 +265,111 @@ public class LangChain4jAgentProducer extends
DefaultProducer {
if (agentFactory != null) {
agentFactory.setCamelContext(this.endpoint.getCamelContext());
}
+
+ // Materialize MCP clients from inline mcpServer.<name>.<property>
configuration
+ Map<String, Object> mcpServerMap =
endpoint.getConfiguration().getMcpServer();
+ if (mcpServerMap != null && !mcpServerMap.isEmpty()) {
+ List<LangChain4jMcpServerDefinition> serverDefs =
parseMcpServerDefinitions(mcpServerMap);
+ if (!serverDefs.isEmpty()) {
+ materializedMcpClients = new ArrayList<>();
+ for (LangChain4jMcpServerDefinition def : serverDefs) {
+ LOG.debug("Building MCP client '{}' (transport: {})",
def.getServerName(), def.getTransportType());
+ materializedMcpClients.add(def.buildClient());
+ }
+ LOG.debug("Materialized {} MCP clients from server
definitions", materializedMcpClients.size());
+ }
+ }
+ }
+
+ /**
+ * Parses the flat mcpServer map (from URI parameters like
mcpServer.weather.url=...) into grouped
+ * {@link LangChain4jMcpServerDefinition} instances.
+ *
+ * <p>
+ * The map keys are in the form {@code <serverName>.<property>}. For
example:
+ * </p>
+ * <ul>
+ * <li>{@code weather.transportType=http}</li>
+ * <li>{@code weather.url=http://localhost:8080}</li>
+ * <li>{@code
filesystem.command=npx,-y,@modelcontextprotocol/server-filesystem}</li>
+ * </ul>
+ */
+ private List<LangChain4jMcpServerDefinition>
parseMcpServerDefinitions(Map<String, Object> mcpServerMap) {
+ // Group entries by server name (the first dot-separated segment)
+ Map<String, Map<String, String>> serverGroups = new LinkedHashMap<>();
+ for (Map.Entry<String, Object> entry : mcpServerMap.entrySet()) {
+ String key = entry.getKey();
+ int dotIndex = key.indexOf('.');
+ if (dotIndex > 0 && dotIndex < key.length() - 1) {
+ String serverName = key.substring(0, dotIndex);
+ String property = key.substring(dotIndex + 1);
+ serverGroups.computeIfAbsent(serverName, k -> new
LinkedHashMap<>())
+ .put(property, String.valueOf(entry.getValue()));
+ } else {
+ LOG.warn("Ignoring invalid mcpServer property key: '{}'.
Expected format: <serverName>.<property>", key);
+ }
+ }
+
+ List<LangChain4jMcpServerDefinition> definitions = new ArrayList<>();
+ for (Map.Entry<String, Map<String, String>> group :
serverGroups.entrySet()) {
+ String serverName = group.getKey();
+ Map<String, String> props = group.getValue();
+
+ LangChain4jMcpServerDefinition def = new
LangChain4jMcpServerDefinition();
+ def.setServerName(serverName);
+
+ if (props.containsKey("transportType")) {
+ def.setTransportType(props.get("transportType"));
+ }
+ if (props.containsKey("url")) {
+ def.setUrl(props.get("url"));
+ }
+ if (props.containsKey("command")) {
+ def.setCommand(Arrays.asList(props.get("command").split(",")));
+ }
+ if (props.containsKey("timeout")) {
+
def.setTimeout(Duration.ofSeconds(Long.parseLong(props.get("timeout"))));
+ }
+ if (props.containsKey("logRequests")) {
+
def.setLogRequests(Boolean.parseBoolean(props.get("logRequests")));
+ }
+ if (props.containsKey("logResponses")) {
+
def.setLogResponses(Boolean.parseBoolean(props.get("logResponses")));
+ }
+
+ // Collect environment variables (environment.<key>=<value>)
+ Map<String, String> environment = new LinkedHashMap<>();
+ for (Map.Entry<String, String> prop : props.entrySet()) {
+ if (prop.getKey().startsWith("environment.")) {
+ String envKey =
prop.getKey().substring("environment.".length());
+ environment.put(envKey, prop.getValue());
+ }
+ }
+ if (!environment.isEmpty()) {
+ def.setEnvironment(environment);
+ }
+
+ definitions.add(def);
+ LOG.debug("Parsed MCP server definition '{}' (transport: {})",
serverName, def.getTransportType());
+ }
+
+ return definitions;
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ // Close only MCP clients that were materialized from server
definitions.
+ // Pre-built McpClient beans (from mcpClients parameter) are managed
by the registry/container.
+ if (materializedMcpClients != null) {
+ for (McpClient client : materializedMcpClients) {
+ try {
+ client.close();
+ } catch (Exception e) {
+ LOG.warn("Error closing MCP client: {}", e.getMessage(),
e);
+ }
+ }
+ materializedMcpClients = null;
+ }
+ super.doStop();
}
}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jMcpServerDefinition.java
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jMcpServerDefinition.java
new file mode 100644
index 000000000000..d196ae1d2da4
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jMcpServerDefinition.java
@@ -0,0 +1,238 @@
+/*
+ * 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.time.Duration;
+import java.util.List;
+import java.util.Map;
+
+import dev.langchain4j.mcp.client.DefaultMcpClient;
+import dev.langchain4j.mcp.client.McpClient;
+import dev.langchain4j.mcp.client.transport.McpTransport;
+import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
+import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport;
+import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
+
+/**
+ * A simplified configuration POJO for declaratively defining MCP (Model
Context Protocol) servers.
+ *
+ * <p>
+ * This class provides a convenient way to configure MCP server connections
without manually building
+ * {@link DefaultMcpClient} and transport instances. It supports both
stdio-based and HTTP-based transports.
+ * </p>
+ *
+ * <p>
+ * Example usage as a Spring bean:
+ * </p>
+ *
+ * <pre>{@code
+ * // Stdio transport (e.g., local MCP server via npx)
+ * LangChain4jMcpServerDefinition serverDef = new
LangChain4jMcpServerDefinition();
+ * serverDef.setTransportType("stdio");
+ * serverDef.setCommand(List.of("npx", "-y",
"@modelcontextprotocol/server-everything"));
+ *
+ * // HTTP transport (e.g., remote MCP server)
+ * LangChain4jMcpServerDefinition serverDef = new
LangChain4jMcpServerDefinition();
+ * serverDef.setTransportType("http");
+ * serverDef.setUrl("http://localhost:3001/mcp");
+ * }</pre>
+ *
+ * @since 4.19.0
+ */
+public class LangChain4jMcpServerDefinition {
+
+ private String serverName;
+ private String transportType = "stdio";
+ private List<String> command;
+ private Map<String, String> environment;
+ private String url;
+ private Duration timeout = Duration.ofSeconds(60);
+ private boolean logRequests;
+ private boolean logResponses;
+
+ /**
+ * Gets the server name (used for identification and as the MCP client
key).
+ */
+ public String getServerName() {
+ return serverName;
+ }
+
+ /**
+ * Sets the server name (used for identification and as the MCP client
key).
+ */
+ public void setServerName(String serverName) {
+ this.serverName = serverName;
+ }
+
+ /**
+ * Gets the transport type. Supported values: "stdio", "http".
+ */
+ public String getTransportType() {
+ return transportType;
+ }
+
+ /**
+ * Sets the transport type. Supported values: "stdio" (default), "http".
+ */
+ public void setTransportType(String transportType) {
+ this.transportType = transportType;
+ }
+
+ /**
+ * Gets the command for stdio transport.
+ */
+ public List<String> getCommand() {
+ return command;
+ }
+
+ /**
+ * Sets the command for stdio transport. For example: ["npx", "-y",
"@modelcontextprotocol/server-everything"].
+ */
+ public void setCommand(List<String> command) {
+ this.command = command;
+ }
+
+ /**
+ * Gets the environment variables for stdio transport.
+ */
+ public Map<String, String> getEnvironment() {
+ return environment;
+ }
+
+ /**
+ * Sets the environment variables for stdio transport.
+ */
+ public void setEnvironment(Map<String, String> environment) {
+ this.environment = environment;
+ }
+
+ /**
+ * Gets the URL for HTTP transport.
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ /**
+ * Sets the URL for HTTP transport. For example:
"http://localhost:3001/mcp".
+ */
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ /**
+ * Gets the timeout for tool execution and connection.
+ */
+ public Duration getTimeout() {
+ return timeout;
+ }
+
+ /**
+ * Sets the timeout for tool execution and connection. Default: 60 seconds.
+ */
+ public void setTimeout(Duration timeout) {
+ this.timeout = timeout;
+ }
+
+ /**
+ * Gets whether request logging is enabled.
+ */
+ public boolean isLogRequests() {
+ return logRequests;
+ }
+
+ /**
+ * Sets whether to log requests sent to the MCP server.
+ */
+ public void setLogRequests(boolean logRequests) {
+ this.logRequests = logRequests;
+ }
+
+ /**
+ * Gets whether response logging is enabled.
+ */
+ public boolean isLogResponses() {
+ return logResponses;
+ }
+
+ /**
+ * Sets whether to log responses received from the MCP server.
+ */
+ public void setLogResponses(boolean logResponses) {
+ this.logResponses = logResponses;
+ }
+
+ /**
+ * Builds an {@link McpClient} from this definition.
+ *
+ * @return a configured and ready-to-use McpClient
+ * @throws IllegalArgumentException if required configuration is missing
+ */
+ public McpClient buildClient() {
+ McpTransport transport = buildTransport();
+
+ DefaultMcpClient.Builder clientBuilder = new DefaultMcpClient.Builder()
+ .transport(transport)
+ .toolExecutionTimeout(timeout);
+
+ if (serverName != null) {
+ clientBuilder.key(serverName);
+ }
+
+ return clientBuilder.build();
+ }
+
+ @SuppressWarnings("deprecation")
+ private McpTransport buildTransport() {
+ if ("http".equalsIgnoreCase(transportType)) {
+ if (url == null || url.trim().isEmpty()) {
+ throw new IllegalArgumentException("URL is required for HTTP
MCP transport");
+ }
+ return new StreamableHttpMcpTransport.Builder()
+ .url(url)
+ .logRequests(logRequests)
+ .logResponses(logResponses)
+ .timeout(timeout)
+ .build();
+ } else if ("sse".equalsIgnoreCase(transportType)) {
+ if (url == null || url.trim().isEmpty()) {
+ throw new IllegalArgumentException("URL is required for SSE
MCP transport");
+ }
+ return new HttpMcpTransport.Builder()
+ .sseUrl(url)
+ .logRequests(logRequests)
+ .logResponses(logResponses)
+ .timeout(timeout)
+ .build();
+ } else if ("stdio".equalsIgnoreCase(transportType)) {
+ if (command == null || command.isEmpty()) {
+ throw new IllegalArgumentException("Command is required for
stdio MCP transport");
+ }
+ StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
+ .command(command)
+ .logEvents(logRequests || logResponses);
+ if (environment != null) {
+ builder.environment(environment);
+ }
+ return builder.build();
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported MCP transport type: " + transportType
+ + ". Supported values: stdio,
http, sse");
+ }
+ }
+}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMcpAndCamelToolsIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMcpAndCamelToolsIT.java
new file mode 100644
index 000000000000..6f06db05c5fa
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMcpAndCamelToolsIT.java
@@ -0,0 +1,330 @@
+/*
+ * 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.integration;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import dev.langchain4j.mcp.client.DefaultMcpClient;
+import dev.langchain4j.mcp.client.McpClient;
+import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
+import dev.langchain4j.model.chat.ChatModel;
+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.Headers;
+import org.apache.camel.component.mock.MockEndpoint;
+import
org.apache.camel.test.infra.mcp.everything.services.McpEverythingService;
+import
org.apache.camel.test.infra.mcp.everything.services.McpEverythingServiceFactory;
+import org.apache.camel.test.infra.ollama.services.OllamaService;
+import org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration test for the LangChain4j Agent component verifying that
internal Camel route tools (via
+ * camel-langchain4j-tools with tags) and external MCP tools can coexist
together in a single agent.
+ *
+ * <p>
+ * This test demonstrates two MCP configuration approaches working
simultaneously:
+ * <ul>
+ * <li><strong>Endpoint URI configuration</strong>: the mcp/everything server
(Streamable HTTP) is configured
+ * declaratively via {@code
mcpServer.everything.transportType=http&mcpServer.everything.url=...}</li>
+ * <li><strong>Bean reference configuration</strong>: the mcp/time server
(Docker stdio) is configured as a pre-built
+ * {@link McpClient} bean and referenced via {@code
mcpClients=#timeMcpClient}</li>
+ * </ul>
+ *
+ * <p>
+ * Additionally, Camel route tools (user database and weather service) are
configured via the {@code tags} parameter.
+ * All tool sources are composed via {@code CompositeToolProvider}.
+ */
+@DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
disabledReason = "Requires too much network resources")
+public class LangChain4jAgentMcpAndCamelToolsIT extends CamelTestSupport {
+
+ private static final String USER_DATABASE = """
+ {"id": "42", "name": "Alice Johnson", "role": "Engineer"}
+ """;
+
+ private static final String WEATHER_INFO = "cloudy, 18C";
+
+ protected ChatModel chatModel;
+ private McpClient timeClient;
+
+ @RegisterExtension
+ static OllamaService OLLAMA =
OllamaServiceFactory.createSingletonService();
+
+ @RegisterExtension
+ static McpEverythingService MCP_EVERYTHING =
McpEverythingServiceFactory.createService();
+
+ @Override
+ protected void setupResources() throws Exception {
+ super.setupResources();
+ chatModel = ModelHelper.loadChatModel(OLLAMA);
+
+ // MCP Time Server via Docker stdio transport - configured as
pre-built McpClient bean
+ timeClient = new DefaultMcpClient.Builder()
+ .key("time")
+ .transport(new StdioMcpTransport.Builder()
+ .command(Arrays.asList("docker", "run", "-i", "--rm",
"mcp/time"))
+ .logEvents(true)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected void cleanupResources() throws Exception {
+ if (timeClient != null) {
+ timeClient.close();
+ }
+ super.cleanupResources();
+ }
+
+ /**
+ * Tests that the agent can use a Camel route tool (user database lookup)
while MCP tools are also available.
+ */
+ @Test
+ void testCamelRouteToolWithMcpToolsPresent() throws InterruptedException {
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:response");
+ mockEndpoint.expectedMessageCount(1);
+
+ String response = template.requestBody("direct:chat",
+ "What is the name of user with ID 42? Use the user database
tool.", String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(response);
+ assertTrue(response.contains("Alice Johnson"),
+ "Response should contain the user name from the Camel route
tool but was: " + response);
+ }
+
+ /**
+ * Tests that the agent can use an MCP tool (time server via Docker stdio,
configured as bean reference).
+ */
+ @Test
+ void testMcpStdioToolAsBeanReference() throws InterruptedException {
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:response");
+ mockEndpoint.expectedMessageCount(1);
+
+ String response = template.requestBody("direct:chat",
+ "What is the current time? Use your available tools to find
out.", String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(response);
+ assertTrue(response.matches("(?si).*\\d{1,2}[:\\.]\\d{2}.*") ||
response.toLowerCase().contains("time"),
+ "Response should contain time information from MCP time tool
but was: " + response);
+ }
+
+ /**
+ * Tests that the agent can use an MCP tool from the Streamable HTTP
server configured inline on the endpoint URI.
+ * The echo tool is part of everything.
+ */
+ @Test
+ void testMcpHttpToolConfiguredOnEndpoint() throws InterruptedException {
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:response");
+ mockEndpoint.expectedMessageCount(1);
+
+ String response = template.requestBody("direct:chat",
+ "Use the echo tool to echo the message 'Hello from Camel'.",
String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(response);
+ assertTrue(response.toLowerCase().contains("hello") ||
response.toLowerCase().contains("camel"),
+ "Response should contain echoed message from MCP Streamable
HTTP tool but was: " + response);
+ }
+
+ /**
+ * Tests the add tool from the MCP Everything Server (configured via
endpoint URI, Streamable HTTP transport). The
+ * add tool is part of everything.
+ */
+ @Test
+ void testMcpHttpToolAdd() throws InterruptedException {
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:response");
+ mockEndpoint.expectedMessageCount(1);
+
+ String response = template.requestBody("direct:chat",
+ "Use the add tool to add 17 and 25. What is the result?",
String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(response);
+ assertTrue(response.contains("42"),
+ "Response should contain the sum 42 from MCP add tool but was:
" + response);
+ }
+
+ /**
+ * Tests that both Camel route tools and MCP tools work together in a
single interaction.
+ */
+ @Test
+ void testMixedCamelAndMcpTools() throws InterruptedException {
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:response");
+ mockEndpoint.expectedMessageCount(1);
+
+ String response = template.requestBody("direct:chat",
+ "First, tell me the name of user ID 42 using the user database
tool, "
+ + "then tell me
the current time using the time tool.",
+ String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(response);
+ assertTrue(response.contains("Alice Johnson"),
+ "Response should contain user info from Camel route tool but
was: " + response);
+ }
+
+ // ---- Tool Exclusion Tests ----
+
+ /**
+ * Tests that excluding a Camel tool tag via header prevents those tools
from being used. First verifies the tool
+ * works, then excludes the "users" tag and verifies the agent can no
longer query the user database.
+ */
+ @Test
+ void testExcludeCamelToolTag() throws InterruptedException {
+ // First: verify the Camel tool works without exclusion
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:response");
+ mockEndpoint.expectedMessageCount(1);
+
+ String responseWithTool = template.requestBody("direct:chat",
+ "What is the name of user with ID 42? Use the user database
tool.", String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(responseWithTool);
+ assertTrue(responseWithTool.contains("Alice Johnson"),
+ "Without exclusion, response should contain user name but was:
" + responseWithTool);
+
+ // Then: exclude the "users" tag and verify the agent cannot use the
user database tool
+ mockEndpoint.reset();
+ mockEndpoint.expectedMessageCount(1);
+
+ String responseWithoutTool =
template.requestBodyAndHeader("direct:chat",
+ "What is the name of user with ID 42? Use the user database
tool.",
+ Headers.EXCLUDE_TAGS, "users",
+ String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(responseWithoutTool);
+ assertFalse(responseWithoutTool.contains("Alice Johnson"),
+ "With 'users' tag excluded, response should NOT contain user
name but was: " + responseWithoutTool);
+ }
+
+ /**
+ * Tests that excluding an MCP server by name via header prevents those
tools from being used. First verifies the
+ * MCP add tool works, then excludes the "everything" MCP server and
verifies the agent can no longer use it.
+ */
+ @Test
+ void testExcludeMcpServer() throws InterruptedException {
+ // First: verify the MCP tool works without exclusion
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:response");
+ mockEndpoint.expectedMessageCount(1);
+
+ String responseWithTool = template.requestBody("direct:chat",
+ "Use the add tool to add 17 and 25. What is the result?",
String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(responseWithTool);
+ assertTrue(responseWithTool.contains("42"),
+ "Without exclusion, response should contain 42 but was: " +
responseWithTool);
+
+ // Then: exclude the "everything" MCP server and verify the add tool
is no longer available
+ mockEndpoint.reset();
+ mockEndpoint.expectedMessageCount(1);
+
+ String responseWithoutTool =
template.requestBodyAndHeader("direct:chat",
+ "Use the add tool to add 17 and 25. What is the result?",
+ Headers.EXCLUDE_MCP_SERVERS, "everything",
+ String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(responseWithoutTool);
+ assertFalse(responseWithoutTool.contains("42"),
+ "With 'everything' MCP server excluded, response should NOT
contain 42 but was: " + responseWithoutTool);
+ }
+
+ /**
+ * Tests that excluding both Camel tool tags and MCP servers
simultaneously works. The agent should have no tools
+ * available and respond based only on its own knowledge.
+ */
+ @Test
+ void testExcludeBothCamelTagsAndMcpServers() throws InterruptedException {
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:response");
+ mockEndpoint.expectedMessageCount(1);
+
+ // Exclude all Camel tool tags and all MCP servers
+ String response = template.requestBodyAndHeaders("direct:chat",
+ "What tools do you have available? List them all.",
+ Map.of(
+ Headers.EXCLUDE_TAGS, "users,weather",
+ Headers.EXCLUDE_MCP_SERVERS, "everything,time"),
+ String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(response);
+ // With all tools excluded, the agent should not be able to use any
tools
+ String lowerResponse = response.toLowerCase();
+ assertTrue(lowerResponse.contains("no tool") ||
lowerResponse.contains("don't have")
+ || lowerResponse.contains("do not have") ||
lowerResponse.contains("not available")
+ || lowerResponse.contains("cannot") ||
!lowerResponse.contains("add")
+ || !lowerResponse.contains("echo"),
+ "With all tools excluded, agent should indicate no tools are
available but was: " + response);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ // Create agent with no tools at the agent level
+ AgentConfiguration config = new AgentConfiguration()
+ .withChatModel(chatModel);
+ Agent agent = new AgentWithoutMemory(config);
+
+ // Register agent and the pre-built MCP client bean in the registry
+ this.context.getRegistry().bind("myAgent", agent);
+ this.context.getRegistry().bind("timeMcpClient", timeClient);
+
+ // Get the MCP Everything Server Streamable HTTP URL from the
test-infra service
+ String everythingUrl = MCP_EVERYTHING.url();
+
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ // Agent route combining 3 tool sources:
+ // 1. Camel route tools via tags (users, weather)
+ // 2. MCP Everything Server via endpoint URI config
(mcpServer.everything.*)
+ // 3. MCP Time Server via pre-built bean reference
(mcpClients=#timeMcpClient)
+ from("direct:chat")
+
.toF("langchain4j-agent:assistant?agent=#myAgent&tags=users,weather"
+ + "&mcpServer.everything.transportType=http"
+ + "&mcpServer.everything.url=%s"
+ + "&mcpClients=#timeMcpClient",
+ everythingUrl)
+ .to("mock:response");
+
+ // Camel route tools (internal tools via
camel-langchain4j-tools)
+ from("langchain4j-tools:userDb?tags=users"
+ + "&description=Query user database by user ID"
+ + "¶meter.userId=string")
+ .setBody(constant(USER_DATABASE));
+
+ from("langchain4j-tools:weatherService?tags=weather"
+ + "&description=Get current weather for a location"
+ + "¶meter.location=string")
+ .setBody(constant("{\"weather\": \"" + WEATHER_INFO +
"\", \"location\": \"Current Location\"}"));
+ }
+ };
+ }
+}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/resources/log4j2.properties
b/components/camel-ai/camel-langchain4j-agent/src/test/resources/log4j2.properties
new file mode 100644
index 000000000000..500141398566
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/resources/log4j2.properties
@@ -0,0 +1,30 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+appender.file.type = File
+appender.file.name = file
+appender.file.fileName = target/camel-langchain4j-agent-test.log
+appender.file.layout.type = PatternLayout
+appender.file.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m%n
+
+appender.out.type = Console
+appender.out.name = out
+appender.out.layout.type = PatternLayout
+appender.out.layout.pattern = [%30.30t] %-30.30c{1} %-5p %m%n
+
+rootLogger.level = INFO
+rootLogger.appenderRef.file.ref = file
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Langchain4jAgentComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Langchain4jAgentComponentBuilderFactory.java
index 543885e06108..b4437aac5aaf 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Langchain4jAgentComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Langchain4jAgentComponentBuilderFactory.java
@@ -159,6 +159,44 @@ public interface Langchain4jAgentComponentBuilderFactory {
doSetProperty("autowiredEnabled", autowiredEnabled);
return this;
}
+
+ /**
+ * Pre-built MCP (Model Context Protocol) client instances for external
+ * tool integration. Reference beans from the registry, e.g.,
+ * #myMcpClient1,#myMcpClient2.
+ *
+ * The option is a:
+ *
<code>java.util.List&lt;dev.langchain4j.mcp.client.McpClient&gt;</code>
type.
+ *
+ * Group: advanced
+ *
+ * @param mcpClients the value to set
+ * @return the dsl builder
+ */
+ default Langchain4jAgentComponentBuilder
mcpClients(java.util.List<dev.langchain4j.mcp.client.McpClient> mcpClients) {
+ doSetProperty("mcpClients", mcpClients);
+ return this;
+ }
+
+ /**
+ * MCP server definitions in the form of mcpServer..=. Supported
+ * properties: transportType (stdio or http, default: stdio), command
+ * (comma-separated, for stdio), url (for http), environment.= (for
+ * stdio), timeout (in seconds, default: 60), logRequests,
logResponses.
+ * 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.
+ *
+ * Group: advanced
+ *
+ * @param mcpServer the value to set
+ * @return the dsl builder
+ */
+ default Langchain4jAgentComponentBuilder
mcpServer(java.util.Map<java.lang.String, java.lang.Object> mcpServer) {
+ doSetProperty("mcpServer", mcpServer);
+ return this;
+ }
}
class Langchain4jAgentComponentBuilderImpl
@@ -186,6 +224,8 @@ public interface Langchain4jAgentComponentBuilderFactory {
case "lazyStartProducer": ((LangChain4jAgentComponent)
component).setLazyStartProducer((boolean) value); return true;
case "tags": getOrCreateConfiguration((LangChain4jAgentComponent)
component).setTags((java.lang.String) value); return true;
case "autowiredEnabled": ((LangChain4jAgentComponent)
component).setAutowiredEnabled((boolean) value); return true;
+ case "mcpClients":
getOrCreateConfiguration((LangChain4jAgentComponent)
component).setMcpClients((java.util.List) value); return true;
+ case "mcpServer":
getOrCreateConfiguration((LangChain4jAgentComponent)
component).setMcpServer((java.util.Map) value); return true;
default: return false;
}
}
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java
index 5bf843a02cf4..b49ecec2a956 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java
@@ -178,6 +178,85 @@ public interface LangChain4jAgentEndpointBuilderFactory {
doSetProperty("lazyStartProducer", lazyStartProducer);
return this;
}
+ /**
+ * Pre-built MCP (Model Context Protocol) client instances for external
+ * tool integration. Reference beans from the registry, e.g.,
+ * #myMcpClient1,#myMcpClient2.
+ *
+ * The option is a:
+ *
<code>java.util.List<dev.langchain4j.mcp.client.McpClient></code> type.
+ *
+ * Group: advanced
+ *
+ * @param mcpClients the value to set
+ * @return the dsl builder
+ */
+ default AdvancedLangChain4jAgentEndpointBuilder
mcpClients(List<dev.langchain4j.mcp.client.McpClient> mcpClients) {
+ doSetProperty("mcpClients", mcpClients);
+ return this;
+ }
+ /**
+ * Pre-built MCP (Model Context Protocol) client instances for external
+ * tool integration. Reference beans from the registry, e.g.,
+ * #myMcpClient1,#myMcpClient2.
+ *
+ * The option will be converted to a
+ *
<code>java.util.List<dev.langchain4j.mcp.client.McpClient></code> type.
+ *
+ * Group: advanced
+ *
+ * @param mcpClients the value to set
+ * @return the dsl builder
+ */
+ default AdvancedLangChain4jAgentEndpointBuilder mcpClients(String
mcpClients) {
+ doSetProperty("mcpClients", mcpClients);
+ return this;
+ }
+ /**
+ * MCP server definitions in the form of mcpServer..=. Supported
+ * properties: transportType (stdio or http, default: stdio), command
+ * (comma-separated, for stdio), url (for http), environment.= (for
+ * stdio), timeout (in seconds, default: 60), logRequests,
logResponses.
+ * 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.
+ * The option is multivalued, and you can use the mcpServer(String,
+ * Object) method to add a value (call the method multiple times to set
+ * more values).
+ *
+ * Group: advanced
+ *
+ * @param key the option key
+ * @param value the option value
+ * @return the dsl builder
+ */
+ default AdvancedLangChain4jAgentEndpointBuilder mcpServer(String key,
Object value) {
+ doSetMultiValueProperty("mcpServer", "mcpServer." + key, value);
+ return this;
+ }
+ /**
+ * MCP server definitions in the form of mcpServer..=. Supported
+ * properties: transportType (stdio or http, default: stdio), command
+ * (comma-separated, for stdio), url (for http), environment.= (for
+ * stdio), timeout (in seconds, default: 60), logRequests,
logResponses.
+ * 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.
+ * The option is multivalued, and you can use the mcpServer(String,
+ * Object) method to add a value (call the method multiple times to set
+ * more values).
+ *
+ * Group: advanced
+ *
+ * @param values the values
+ * @return the dsl builder
+ */
+ default AdvancedLangChain4jAgentEndpointBuilder mcpServer(Map values) {
+ doSetMultiValueProperties("mcpServer", "mcpServer.", values);
+ return this;
+ }
}
public interface LangChain4jAgentBuilders {
@@ -296,6 +375,33 @@ public interface LangChain4jAgentEndpointBuilderFactory {
public String langChain4jAgentMediaType() {
return "CamelLangChain4jAgentMediaType";
}
+ /**
+ * Comma-separated list of Camel tool tags to exclude from this agent
+ * invocation.
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code LangChain4jAgentExcludeTags}.
+ */
+ public String langChain4jAgentExcludeTags() {
+ return "CamelLangChain4jAgentExcludeTags";
+ }
+ /**
+ * Comma-separated list of MCP server names (keys) to exclude from this
+ * agent invocation.
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code
+ * LangChain4jAgentExcludeMcpServers}.
+ */
+ public String langChain4jAgentExcludeMcpServers() {
+ return "CamelLangChain4jAgentExcludeMcpServers";
+ }
}
static LangChain4jAgentEndpointBuilder endpointBuilder(String
componentName, String path) {
class LangChain4jAgentEndpointBuilderImpl extends
AbstractEndpointBuilder implements LangChain4jAgentEndpointBuilder,
AdvancedLangChain4jAgentEndpointBuilder {
diff --git a/test-infra/camel-test-infra-mcp-everything/pom.xml
b/test-infra/camel-test-infra-mcp-everything/pom.xml
new file mode 100644
index 000000000000..a852f9d7955d
--- /dev/null
+++ b/test-infra/camel-test-infra-mcp-everything/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>camel-test-infra-parent</artifactId>
+ <groupId>org.apache.camel</groupId>
+ <relativePath>../camel-test-infra-parent/pom.xml</relativePath>
+ <version>4.19.0-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>camel-test-infra-mcp-everything</artifactId>
+ <name>Camel :: Test Infra :: MCP Everything</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-infra-common</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <!-- logging -->
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-slf4j2-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git
a/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/common/McpEverythingProperties.java
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/common/McpEverythingProperties.java
new file mode 100644
index 000000000000..e096bdf2cb1f
--- /dev/null
+++
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/common/McpEverythingProperties.java
@@ -0,0 +1,28 @@
+/*
+ * 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.test.infra.mcp.everything.common;
+
+public final class McpEverythingProperties {
+ public static final String MCP_EVERYTHING_URL = "mcp.everything.url";
+ public static final String MCP_EVERYTHING_HOST = "mcp.everything.host";
+ public static final String MCP_EVERYTHING_PORT = "mcp.everything.port";
+ public static final String MCP_EVERYTHING_CONTAINER =
"mcp.everything.container";
+ public static final int DEFAULT_PORT = 3001;
+
+ private McpEverythingProperties() {
+ }
+}
diff --git
a/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingInfraService.java
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingInfraService.java
new file mode 100644
index 000000000000..0ac69476249d
--- /dev/null
+++
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingInfraService.java
@@ -0,0 +1,36 @@
+/*
+ * 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.test.infra.mcp.everything.services;
+
+import org.apache.camel.test.infra.common.services.InfrastructureService;
+
+/**
+ * Test infra service for the MCP Everything Server, which provides MCP tools
via Streamable HTTP transport.
+ */
+public interface McpEverythingInfraService extends InfrastructureService {
+
+ String host();
+
+ int port();
+
+ /**
+ * Returns the Streamable HTTP endpoint URL for connecting an MCP client.
+ */
+ default String url() {
+ return String.format("http://%s:%d/mcp", host(), port());
+ }
+}
diff --git
a/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingLocalContainerInfraService.java
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingLocalContainerInfraService.java
new file mode 100644
index 000000000000..6e409693a977
--- /dev/null
+++
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingLocalContainerInfraService.java
@@ -0,0 +1,121 @@
+/*
+ * 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.test.infra.mcp.everything.services;
+
+import java.time.Duration;
+
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerEnvironmentUtil;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import
org.apache.camel.test.infra.mcp.everything.common.McpEverythingProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Runs the MCP Everything Server as a Docker container in Streamable HTTP
transport mode, exposing port 3001.
+ */
+@InfraService(service = McpEverythingInfraService.class,
+ description = "MCP Everything Server (Streamable HTTP
transport)",
+ serviceAlias = { "mcp-everything" })
+public class McpEverythingLocalContainerInfraService
+ implements McpEverythingInfraService,
ContainerService<GenericContainer<?>> {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(McpEverythingLocalContainerInfraService.class);
+
+ // Using tzolov/mcp-everything-server instead of the official
mcp/everything image
+ // because the official image does not properly support HTTP/SSE transport
mode
+ // (it only works reliably with stdio transport).
+ private static final String DEFAULT_CONTAINER =
"tzolov/mcp-everything-server:v3";
+
+ private final GenericContainer<?> container;
+
+ public McpEverythingLocalContainerInfraService() {
+
this(LocalPropertyResolver.getProperty(McpEverythingLocalContainerInfraService.class,
+ McpEverythingProperties.MCP_EVERYTHING_CONTAINER));
+ }
+
+ public McpEverythingLocalContainerInfraService(String imageName) {
+ container = initContainer(imageName);
+ String name = ContainerEnvironmentUtil.containerName(this.getClass());
+ if (name != null) {
+ container.withCreateContainerCmdModifier(cmd ->
cmd.withName(name));
+ }
+ }
+
+ public McpEverythingLocalContainerInfraService(GenericContainer<?>
container) {
+ this.container = container;
+ }
+
+ protected GenericContainer<?> initContainer(String imageName) {
+ String image = imageName != null ? imageName : DEFAULT_CONTAINER;
+
+ class TestInfraMcpEverythingContainer extends
GenericContainer<TestInfraMcpEverythingContainer> {
+ public TestInfraMcpEverythingContainer(boolean fixedPort) {
+ super(DockerImageName.parse(image));
+
+ withCommand("node", "dist/index.js", "streamableHttp");
+ waitingFor(Wait.forLogMessage(".*listening on port.*\\n", 1))
+ .withStartupTimeout(Duration.ofMinutes(2L));
+
+ ContainerEnvironmentUtil.configurePort(this, fixedPort,
McpEverythingProperties.DEFAULT_PORT);
+ }
+ }
+
+ return new
TestInfraMcpEverythingContainer(ContainerEnvironmentUtil.isFixedPort(this.getClass()));
+ }
+
+ @Override
+ public void registerProperties() {
+ System.setProperty(McpEverythingProperties.MCP_EVERYTHING_URL, url());
+ System.setProperty(McpEverythingProperties.MCP_EVERYTHING_HOST,
host());
+ System.setProperty(McpEverythingProperties.MCP_EVERYTHING_PORT,
String.valueOf(port()));
+ }
+
+ @Override
+ public void initialize() {
+ LOG.info("Trying to start the MCP Everything Server container");
+ container.start();
+
+ registerProperties();
+ LOG.info("MCP Everything Server running at {}", url());
+ }
+
+ @Override
+ public void shutdown() {
+ LOG.info("Stopping the MCP Everything Server container");
+ container.stop();
+ }
+
+ @Override
+ public GenericContainer<?> getContainer() {
+ return container;
+ }
+
+ @Override
+ public String host() {
+ return container.getHost();
+ }
+
+ @Override
+ public int port() {
+ return container.getMappedPort(McpEverythingProperties.DEFAULT_PORT);
+ }
+}
diff --git
a/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingService.java
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingService.java
new file mode 100644
index 000000000000..42ddfdd816c8
--- /dev/null
+++
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingService.java
@@ -0,0 +1,26 @@
+/*
+ * 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.test.infra.mcp.everything.services;
+
+import org.apache.camel.test.infra.common.services.ContainerTestService;
+import org.apache.camel.test.infra.common.services.TestService;
+
+/**
+ * Test infra service for the MCP Everything Server.
+ */
+public interface McpEverythingService extends TestService,
McpEverythingInfraService, ContainerTestService {
+}
diff --git
a/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingServiceFactory.java
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingServiceFactory.java
new file mode 100644
index 000000000000..597b46d53c20
--- /dev/null
+++
b/test-infra/camel-test-infra-mcp-everything/src/main/java/org/apache/camel/test/infra/mcp/everything/services/McpEverythingServiceFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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.test.infra.mcp.everything.services;
+
+import org.apache.camel.test.infra.common.services.SimpleTestServiceBuilder;
+
+public final class McpEverythingServiceFactory {
+
+ private McpEverythingServiceFactory() {
+ }
+
+ public static SimpleTestServiceBuilder<McpEverythingService> builder() {
+ return new SimpleTestServiceBuilder<>("mcp-everything");
+ }
+
+ public static McpEverythingService createService() {
+ return builder()
+ .addLocalMapping(McpEverythingLocalContainerService::new)
+ .build();
+ }
+
+ public static class McpEverythingLocalContainerService extends
McpEverythingLocalContainerInfraService
+ implements McpEverythingService {
+ }
+}
diff --git
a/test-infra/camel-test-infra-mcp-everything/src/main/resources/org/apache/camel/test/infra/mcp/everything/services/container.properties
b/test-infra/camel-test-infra-mcp-everything/src/main/resources/org/apache/camel/test/infra/mcp/everything/services/container.properties
new file mode 100644
index 000000000000..fab448187c43
--- /dev/null
+++
b/test-infra/camel-test-infra-mcp-everything/src/main/resources/org/apache/camel/test/infra/mcp/everything/services/container.properties
@@ -0,0 +1,18 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+mcp.everything.container=tzolov/mcp-everything-server:v3
diff --git a/test-infra/pom.xml b/test-infra/pom.xml
index a506707a54de..7d0f4ab17fe2 100644
--- a/test-infra/pom.xml
+++ b/test-infra/pom.xml
@@ -98,6 +98,7 @@
<module>camel-test-infra-ibmmq</module>
<module>camel-test-infra-docling</module>
<module>camel-test-infra-iggy</module>
+ <module>camel-test-infra-mcp-everything</module>
<module>camel-test-infra-all</module>
</modules>