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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new aa8c9d327034 CAMEL-24369: camel-mcp-server jackson-annotations 
alignment + camel-openai deferred MCP client init
aa8c9d327034 is described below

commit aa8c9d327034332b4b0bb8f7cc148645c4da3c82
Author: Federico Mariani <[email protected]>
AuthorDate: Fri Aug 7 12:04:30 2026 +0200

    CAMEL-24369: camel-mcp-server jackson-annotations alignment + camel-openai 
deferred MCP client init
    
    Add a direct jackson-annotations dependency to camel-mcp-server so consumers
    resolving without the jackson-bom get a version aligned with 
jackson-databind
    (the MCP SDK's older jackson-annotations otherwise wins by nearest-wins and
    breaks databind at runtime with NoClassDefFoundError: JsonSerializeAs).
    
    Defer initialization of MCP servers that are unreachable when the 
camel-openai
    endpoint starts: the server is recorded with a WARN and retried lazily on 
first
    use via the existing reconnect path and per-server locks. This lets an
    application consume its own MCP endpoint on runtimes where the HTTP server 
only
    accepts connections after the application has started. Invalid configuration
    (e.g. missing transportType) still fails fast.
    
    Includes a new unit test (4 scenarios), the openai-mcp error-table update, 
and
    a fix for the stale ai-tool See Also section.
    
    Closes #25404
    
    Co-Authored-By: Claude <[email protected]>
---
 .../camel/catalog/docs/ai-tool-component.adoc      |   5 +-
 .../org/apache/camel/catalog/docs/openai-mcp.adoc  |   2 +-
 .../src/main/docs/ai-tool-component.adoc           |   5 +-
 components/camel-ai/camel-mcp-server/pom.xml       |   8 ++
 .../camel-openai/src/main/docs/openai-mcp.adoc     |   2 +-
 .../camel/component/openai/OpenAIEndpoint.java     |  56 +++++++-
 .../openai/OpenAIEndpointMcpDeferredInitTest.java  | 155 +++++++++++++++++++++
 7 files changed, 223 insertions(+), 10 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
index 7b305ed88951..defb3f0a929c 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
@@ -311,5 +311,6 @@ YAML::
 
 == See Also
 
-* xref:langchain4j-agent-component.adoc[LangChain4j Agent Component] — will 
discover ai-tool tools in a future release
-* xref:spring-ai-chat-component.adoc[Spring AI Chat Component] — will discover 
ai-tool tools in a future release
+* xref:langchain4j-agent-component.adoc[LangChain4j Agent Component] — 
discovers ai-tool tools via the `tags` option
+* xref:spring-ai-chat-component.adoc[Spring AI Chat Component] — discovers 
ai-tool tools via the `tags` option
+* xref:others:mcp-server.adoc[MCP Server] — exposes ai-tool routes as MCP 
tools over streamable HTTP
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-mcp.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-mcp.adoc
index ec12fccde32a..7123962058e5 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-mcp.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-mcp.adoc
@@ -732,7 +732,7 @@ Two endpoint options control what happens when a tool call 
fails:
 |===
 | Scenario | Behavior
 
-| MCP client initialization failure | Route fails to start (`RuntimeException` 
during `doStart()`)
+| MCP server unreachable at startup | Route starts anyway: a WARN is logged 
and initialization of that server is deferred and retried on first use. This 
lets an application consume its own MCP endpoint (or one of a service starting 
concurrently) on runtimes where the HTTP server only accepts connections after 
the application has started. Invalid configuration (for example a missing 
`transportType`) still fails the route at startup.
 | Tool execution throws an exception | Governed by 
`toolExecutionErrorStrategy`: `failExchange` (default) propagates the exception 
to the exchange; `repromptModel` catches the error, logs it as WARN, and sends 
it as tool result text to the model
 | MCP transport error (`mcpReconnect=true`) | Automatic reconnection and retry 
(once). If retry fails, behavior follows `toolExecutionErrorStrategy`
 | MCP `CallToolResult.isError()` is true | Error content is sent as tool 
result text to the model
diff --git 
a/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc 
b/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
index 7b305ed88951..defb3f0a929c 100644
--- a/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
+++ b/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
@@ -311,5 +311,6 @@ YAML::
 
 == See Also
 
-* xref:langchain4j-agent-component.adoc[LangChain4j Agent Component] — will 
discover ai-tool tools in a future release
-* xref:spring-ai-chat-component.adoc[Spring AI Chat Component] — will discover 
ai-tool tools in a future release
+* xref:langchain4j-agent-component.adoc[LangChain4j Agent Component] — 
discovers ai-tool tools via the `tags` option
+* xref:spring-ai-chat-component.adoc[Spring AI Chat Component] — discovers 
ai-tool tools via the `tags` option
+* xref:others:mcp-server.adoc[MCP Server] — exposes ai-tool routes as MCP 
tools over streamable HTTP
diff --git a/components/camel-ai/camel-mcp-server/pom.xml 
b/components/camel-ai/camel-mcp-server/pom.xml
index feb84234815f..4e37d50c31f0 100644
--- a/components/camel-ai/camel-mcp-server/pom.xml
+++ b/components/camel-ai/camel-mcp-server/pom.xml
@@ -63,6 +63,14 @@
             <artifactId>mcp-json-jackson2</artifactId>
             <version>${mcp-java-sdk-version}</version>
         </dependency>
+        <!-- direct dependency so consumers resolving without the jackson-bom 
get a
+             jackson-annotations aligned with jackson-databind: mcp-core 
declares an older
+             jackson-annotations that otherwise wins by nearest-wins and 
breaks databind
+             at runtime (NoClassDefFoundError: JsonSerializeAs) -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
 
         <!-- test dependencies -->
         <dependency>
diff --git a/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc 
b/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc
index ec12fccde32a..7123962058e5 100644
--- a/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc
+++ b/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc
@@ -732,7 +732,7 @@ Two endpoint options control what happens when a tool call 
fails:
 |===
 | Scenario | Behavior
 
-| MCP client initialization failure | Route fails to start (`RuntimeException` 
during `doStart()`)
+| MCP server unreachable at startup | Route starts anyway: a WARN is logged 
and initialization of that server is deferred and retried on first use. This 
lets an application consume its own MCP endpoint (or one of a service starting 
concurrently) on runtimes where the HTTP server only accepts connections after 
the application has started. Invalid configuration (for example a missing 
`transportType`) still fails the route at startup.
 | Tool execution throws an exception | Governed by 
`toolExecutionErrorStrategy`: `failExchange` (default) propagates the exception 
to the exchange; `repromptModel` catches the error, logs it as WARN, and sends 
it as tool result text to the model
 | MCP transport error (`mcpReconnect=true`) | Automatic reconnection and retry 
(once). If retry fails, behavior follows `toolExecutionErrorStrategy`
 | MCP `CallToolResult.isError()` is true | Error content is sent as tool 
result text to the model
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
index 094a5a9e7afe..63f872b3745a 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
@@ -99,6 +99,13 @@ public class OpenAIEndpoint extends DefaultEndpoint {
     private final Set<String> manualReturnDirectAdded = 
ConcurrentHashMap.newKeySet();
     private final Set<String> manualReturnDirectRemoved = 
ConcurrentHashMap.newKeySet();
 
+    /**
+     * MCP servers that were unreachable when the endpoint started. 
Initialization is retried lazily on first use so
+     * that an application acting as MCP client of itself (or of another 
service starting concurrently) does not fail
+     * route startup on runtimes where the HTTP server only accepts 
connections after the application has started.
+     */
+    private final Set<String> pendingMcpServers = 
ConcurrentHashMap.newKeySet();
+
     private volatile McpToolState mcpToolState = McpToolState.empty();
     private volatile boolean mcpStopped;
     private Map<String, Map<String, String>> serverConfigs;
@@ -140,6 +147,7 @@ public class OpenAIEndpoint extends DefaultEndpoint {
         globalMcpLock.lock();
         try {
             mcpStopped = true;
+            pendingMcpServers.clear();
             toClose = new HashSet<>(mcpToolState.toolClientMap().values());
             mcpToolState = McpToolState.empty();
         } finally {
@@ -198,11 +206,24 @@ public class OpenAIEndpoint extends DefaultEndpoint {
                 throw new IllegalArgumentException("mcpServer." + serverName + 
".transportType is required");
             }
 
-            McpSyncClient mcpClient = createMcpClient(serverName, props);
-            LOG.debug("MCP server '{}' initialized, listing tools", 
serverName);
+            McpSyncClient mcpClient;
+            List<McpSchema.Tool> serverTools;
+            try {
+                mcpClient = createMcpClient(serverName, props);
+                LOG.debug("MCP server '{}' initialized, listing tools", 
serverName);
 
-            McpSchema.ListToolsResult toolsResult = mcpClient.listTools();
-            List<McpSchema.Tool> serverTools = 
filterTools(toolsResult.tools(), serverName, props);
+                McpSchema.ListToolsResult toolsResult = mcpClient.listTools();
+                serverTools = filterTools(toolsResult.tools(), serverName, 
props);
+            } catch (Exception e) {
+                // do not fail route startup on an unreachable server: the 
server may simply not be
+                // accepting connections yet (e.g. this application's own MCP 
endpoint on runtimes
+                // where the HTTP server starts after the CamelContext); retry 
lazily on first use
+                pendingMcpServers.add(serverName);
+                LOG.warn("MCP server '{}' is unreachable at endpoint startup 
({}); "
+                         + "initialization deferred to first use",
+                        serverName, e.getMessage());
+                continue;
+            }
 
             for (McpSchema.Tool tool : serverTools) {
                 if (toolClientMap.putIfAbsent(tool.name(), mcpClient) != null) 
{
@@ -780,9 +801,36 @@ public class OpenAIEndpoint extends DefaultEndpoint {
     }
 
     McpToolState getMcpToolState() {
+        if (!pendingMcpServers.isEmpty()) {
+            initializePendingMcpServers();
+        }
         return mcpToolState;
     }
 
+    /**
+     * Retries the initialization of MCP servers that were unreachable when 
the endpoint started. Runs at most one
+     * initialization attempt per server at a time via the per-server locks; a 
thread that finds a server being
+     * initialized by another thread simply skips it and serves the current 
tool state.
+     */
+    private void initializePendingMcpServers() {
+        for (String serverName : pendingMcpServers) {
+            ReentrantLock lock = mcpClientLocks.get(serverName);
+            if (lock == null || !lock.tryLock()) {
+                continue;
+            }
+            try {
+                if (mcpStopped || !pendingMcpServers.contains(serverName)) {
+                    continue;
+                }
+                if (doReconnectMcpServer(null, serverName) != null) {
+                    pendingMcpServers.remove(serverName);
+                }
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
     // Package-private setters for testing
 
     void setMcpToolState(McpToolState state) {
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEndpointMcpDeferredInitTest.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEndpointMcpDeferredInitTest.java
new file mode 100644
index 000000000000..5d76f7bc5851
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEndpointMcpDeferredInitTest.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import com.openai.client.OpenAIClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests that an MCP server that is unreachable when the endpoint starts does 
not fail the route: initialization is
+ * deferred and retried on first use. This is the self-referential agent 
scenario, where an application consumes its own
+ * MCP endpoint and the HTTP server only accepts connections after the 
application has started.
+ */
+class OpenAIEndpointMcpDeferredInitTest {
+
+    private static final String SERVER = "orderDesk";
+    private static final List<String> SERVER_TOOLS = List.of("say_hello", 
"list_orders");
+
+    /**
+     * Overrides the client-creation seam: throws while {@code serverUp} is 
false, simulating an MCP server that is not
+     * yet accepting connections, and returns a mock client once the server is 
"up".
+     */
+    private static class TestEndpoint extends OpenAIEndpoint {
+        volatile boolean serverUp;
+        final AtomicInteger connectionAttempts = new AtomicInteger();
+
+        TestEndpoint(OpenAIComponent component, OpenAIConfiguration config) {
+            super("openai:chat-completion", component, config);
+        }
+
+        @Override
+        McpSyncClient createMcpClient(String serverName, Map<String, String> 
props) {
+            connectionAttempts.incrementAndGet();
+            if (!serverUp) {
+                throw new RuntimeException("Client failed to initialize by 
explicit API call");
+            }
+            McpSyncClient client = mock(McpSyncClient.class);
+            List<McpSchema.Tool> tools = SERVER_TOOLS.stream()
+                    .map(n -> McpSchema.Tool.builder(n, Map.of("type", 
"object")).description("mock " + n).build())
+                    .toList();
+            
when(client.listTools()).thenReturn(McpSchema.ListToolsResult.builder(tools).build());
+            return client;
+        }
+
+        @Override
+        protected OpenAIClient createClient() {
+            return mock(OpenAIClient.class);
+        }
+    }
+
+    private TestEndpoint createEndpoint() {
+        DefaultCamelContext ctx = new DefaultCamelContext();
+        OpenAIComponent component = new OpenAIComponent();
+        component.setCamelContext(ctx);
+
+        Map<String, Object> flatConfig = new HashMap<>();
+        flatConfig.put(SERVER + ".transportType", "streamableHttp");
+        flatConfig.put(SERVER + ".url", "http://localhost:1/mcp";);
+
+        OpenAIConfiguration config = new OpenAIConfiguration();
+        config.setMcpServer(flatConfig);
+
+        TestEndpoint endpoint = new TestEndpoint(component, config);
+        endpoint.setCamelContext(ctx);
+        endpoint.setOperation(OpenAIOperations.chatCompletion);
+        return endpoint;
+    }
+
+    private static Set<String> toolNames(McpToolState state) {
+        return state.tools().stream().map(t -> 
t.function().name()).collect(Collectors.toSet());
+    }
+
+    @Test
+    void unreachableServerDoesNotFailEndpointStart() throws Exception {
+        TestEndpoint endpoint = createEndpoint();
+
+        assertThatCode(endpoint::doStart).doesNotThrowAnyException();
+        assertThat(endpoint.connectionAttempts).hasValue(1);
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void toolsAppearOnFirstUseOnceServerIsReachable() throws Exception {
+        TestEndpoint endpoint = createEndpoint();
+        endpoint.doStart();
+
+        // still down on first use: no tools, but no failure either
+        assertThat(toolNames(endpoint.getMcpToolState())).isEmpty();
+
+        endpoint.serverUp = true;
+        McpToolState state = endpoint.getMcpToolState();
+        
assertThat(toolNames(state)).containsExactlyInAnyOrderElementsOf(SERVER_TOOLS);
+        assertThat(state.toolToServerName()).containsValues(SERVER);
+
+        // once initialized, further uses do not attempt new connections
+        int attempts = endpoint.connectionAttempts.get();
+        endpoint.getMcpToolState();
+        assertThat(endpoint.connectionAttempts).hasValue(attempts);
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void reachableServerInitializesEagerlyAtStart() throws Exception {
+        TestEndpoint endpoint = createEndpoint();
+        endpoint.serverUp = true;
+
+        endpoint.doStart();
+        assertThat(endpoint.connectionAttempts).hasValue(1);
+        
assertThat(toolNames(endpoint.getMcpToolState())).containsExactlyInAnyOrderElementsOf(SERVER_TOOLS);
+        assertThat(endpoint.connectionAttempts).hasValue(1);
+
+        endpoint.doStop();
+    }
+
+    @Test
+    void stoppedEndpointDoesNotRetryPendingServers() throws Exception {
+        TestEndpoint endpoint = createEndpoint();
+        endpoint.doStart();
+        endpoint.doStop();
+
+        endpoint.serverUp = true;
+        assertThat(toolNames(endpoint.getMcpToolState())).isEmpty();
+        assertThat(endpoint.connectionAttempts).hasValue(1);
+    }
+}

Reply via email to