This is an automated email from the ASF dual-hosted git repository. Croway pushed a commit to branch CAMEL-23078-openai-mcp-parallel-tools-and-refresh in repository https://gitbox.apache.org/repos/asf/camel.git
commit 14ff3721dff874d96d8bce037af7b25e9bc36cca Author: croway <[email protected]> AuthorDate: Fri Jul 31 09:56:50 2026 +0200 CAMEL-23078: camel-openai - Refresh MCP tools on tools/list_changed MCP servers announce tool additions, removals and changes with a tools/list_changed notification, but the component ignored it: the advertised tool list was fixed to whatever was listed when the endpoint started, and was only ever re-listed as a side effect of a transport reconnect. A long-running route therefore kept offering the model a stale set of tools. Register a toolsChangeConsumer on each MCP client and republish the tool state when it fires. The MCP SDK already re-lists the tools before invoking the consumer, so no extra round trip is needed, but the list it passes is unfiltered and the per-server `toolNames` include list is re-applied before publishing, so a server cannot widen its exposed tool set by announcing new tools. The new `mcpToolRefresh` option (default true, matching what the MCP specification expects of a client) pins the tool list to endpoint startup when set to false. The per-server state rebuild was inlined in doReconnectMcpServer; it is extracted into republishServerTools and shared with the refresh path so that duplicate name handling, the toolNames filter and returnDirect detection cannot drift between the two. A notification arriving from a client that a reconnect has already superseded is discarded, so a stale in-flight notification cannot map tools back to a dead client. Also fixes returnDirect flags set programmatically through addReturnDirectTool and removeReturnDirectTool being silently discarded whenever the tool state was rebuilt. They are now remembered and re-applied, except for tools that no longer exist, which stay fully pruned. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> --- .../apache/camel/catalog/components/openai.json | 67 +++--- .../org/apache/camel/catalog/docs/openai-mcp.adoc | 131 ++++++++++ .../jbang/camel-jbang-configuration-metadata.json | 6 +- .../component/openai/OpenAIEndpointConfigurer.java | 6 + .../component/openai/OpenAIEndpointUriFactory.java | 3 +- .../org/apache/camel/component/openai/openai.json | 67 +++--- .../camel-openai/src/main/docs/openai-mcp.adoc | 62 +++++ .../component/openai/OpenAIConfiguration.java | 14 ++ .../camel/component/openai/OpenAIEndpoint.java | 187 ++++++++++++--- .../openai/OpenAIEndpointMcpToolRefreshTest.java | 263 +++++++++++++++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 14 ++ .../endpoint/dsl/OpenAIEndpointBuilderFactory.java | 120 ++++++++++ 12 files changed, 830 insertions(+), 110 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json index c1c640d00adc..3ded34a943e7 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json @@ -120,38 +120,39 @@ "mcpReconnect": { "index": 31, "kind": "parameter", "displayName": "Mcp Reconnect", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Automatically reconnect to MCP servers when a tool call fails due to a t [...] "mcpServer": { "index": 32, "kind": "parameter", "displayName": "Mcp Server", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Cont [...] "mcpTimeout": { "index": 33, "kind": "parameter", "displayName": "Mcp Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 20, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in seconds for MCP tool call requests. Applies to all MCP operations inclu [...] - "model": { "index": 34, "kind": "parameter", "displayName": "Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for chat completion" }, - "outputClass": { "index": 35, "kind": "parameter", "displayName": "Output Class", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Fully qualified class name for structured output using response format" }, - "parallelToolExecution": { "index": 36, "kind": "parameter", "displayName": "Parallel Tool Execution", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Execute the tool calls returned by the model in a si [...] - "parallelToolTimeout": { "index": 37, "kind": "parameter", "displayName": "Parallel Tool Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in milliseconds for a batch of parallel tool calls, so [...] - "previousResponseId": { "index": 38, "kind": "parameter", "displayName": "Previous Response Id", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Previous response id for OpenAI server-side conversation state (Responses [...] - "requestTimeout": { "index": 39, "kind": "parameter", "displayName": "Request Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "HTTP request timeout in milliseconds for the OpenAI SDK client. When 0 or [...] - "speechInstructions": { "index": 40, "kind": "parameter", "displayName": "Speech Instructions", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Optional instructions to control the voice of the generated audio. Does no [...] - "speechModel": { "index": 41, "kind": "parameter", "displayName": "Speech Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)" }, - "speechResponseFormat": { "index": 42, "kind": "parameter", "displayName": "Speech Response Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "mp3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "descrip [...] - "speechSpeed": { "index": 43, "kind": "parameter", "displayName": "Speech Speed", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The speed of the generated audio, from 0.25 to 4.0 where 1.0 is normal speed" }, - "speechVoice": { "index": 44, "kind": "parameter", "displayName": "Speech Voice", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "alloy", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The voice to use for text-to-speech (e.g., alloy, echo, fable, [...] - "storeFullResponse": { "index": 45, "kind": "parameter", "displayName": "Store Full Response", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Store the full SDK response in non-streaming mode: chat-comp [...] - "streaming": { "index": 46, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, - "stripThinking": { "index": 47, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like [...] - "systemMessage": { "index": 48, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversat [...] - "temperature": { "index": 49, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, - "toolExecutionErrorStrategy": { "index": 50, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfigur [...] - "topP": { "index": 51, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, - "userMessage": { "index": 52, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, - "lazyStartProducer": { "index": 53, "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 produ [...] - "oauthProfile": { "index": 54, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Cred [...] - "sslContextParameters": { "index": 55, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use [...] - "sslEndpointAlgorithm": { "index": 56, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", [...] - "sslKeymanagerAlgorithm": { "index": 57, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key [...] - "sslKeyPassword": { "index": 58, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, - "sslKeystoreLocation": { "index": 59, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be [...] - "sslKeystorePassword": { "index": 60, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, - "sslKeystoreType": { "index": 61, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, - "sslProtocol": { "index": 62, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, - "sslTrustmanagerAlgorithm": { "index": 63, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the tru [...] - "sslTruststoreLocation": { "index": 64, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the [...] - "sslTruststorePassword": { "index": 65, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. [...] - "sslTruststoreType": { "index": 66, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } + "mcpToolRefresh": { "index": 34, "kind": "parameter", "displayName": "Mcp Tool Refresh", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Refresh the advertised tool list when an MCP server notifies that i [...] + "model": { "index": 35, "kind": "parameter", "displayName": "Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for chat completion" }, + "outputClass": { "index": 36, "kind": "parameter", "displayName": "Output Class", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Fully qualified class name for structured output using response format" }, + "parallelToolExecution": { "index": 37, "kind": "parameter", "displayName": "Parallel Tool Execution", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Execute the tool calls returned by the model in a si [...] + "parallelToolTimeout": { "index": 38, "kind": "parameter", "displayName": "Parallel Tool Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in milliseconds for a batch of parallel tool calls, so [...] + "previousResponseId": { "index": 39, "kind": "parameter", "displayName": "Previous Response Id", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Previous response id for OpenAI server-side conversation state (Responses [...] + "requestTimeout": { "index": 40, "kind": "parameter", "displayName": "Request Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "HTTP request timeout in milliseconds for the OpenAI SDK client. When 0 or [...] + "speechInstructions": { "index": 41, "kind": "parameter", "displayName": "Speech Instructions", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Optional instructions to control the voice of the generated audio. Does no [...] + "speechModel": { "index": 42, "kind": "parameter", "displayName": "Speech Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)" }, + "speechResponseFormat": { "index": 43, "kind": "parameter", "displayName": "Speech Response Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "mp3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "descrip [...] + "speechSpeed": { "index": 44, "kind": "parameter", "displayName": "Speech Speed", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The speed of the generated audio, from 0.25 to 4.0 where 1.0 is normal speed" }, + "speechVoice": { "index": 45, "kind": "parameter", "displayName": "Speech Voice", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "alloy", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The voice to use for text-to-speech (e.g., alloy, echo, fable, [...] + "storeFullResponse": { "index": 46, "kind": "parameter", "displayName": "Store Full Response", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Store the full SDK response in non-streaming mode: chat-comp [...] + "streaming": { "index": 47, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, + "stripThinking": { "index": 48, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like [...] + "systemMessage": { "index": 49, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversat [...] + "temperature": { "index": 50, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, + "toolExecutionErrorStrategy": { "index": 51, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfigur [...] + "topP": { "index": 52, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, + "userMessage": { "index": 53, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, + "lazyStartProducer": { "index": 54, "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 produ [...] + "oauthProfile": { "index": 55, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Cred [...] + "sslContextParameters": { "index": 56, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use [...] + "sslEndpointAlgorithm": { "index": 57, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", [...] + "sslKeymanagerAlgorithm": { "index": 58, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key [...] + "sslKeyPassword": { "index": 59, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, + "sslKeystoreLocation": { "index": 60, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be [...] + "sslKeystorePassword": { "index": 61, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, + "sslKeystoreType": { "index": 62, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, + "sslProtocol": { "index": 63, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, + "sslTrustmanagerAlgorithm": { "index": 64, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the tru [...] + "sslTruststoreLocation": { "index": 65, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the [...] + "sslTruststorePassword": { "index": 66, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. [...] + "sslTruststoreType": { "index": 67, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } } } 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 4b8160b3bf2e..ec12fccde32a 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 @@ -300,6 +300,75 @@ YAML:: ---- ==== +=== Parallel Tool Execution + +A model can request several tool calls in a single response. By default, the component executes them one after another, +so the batch takes as long as the sum of its tool calls. Because tool calls emitted in the same assistant message are +independent by design, `parallelToolExecution=true` dispatches them concurrently instead, reducing the batch to the +duration of its slowest tool: + +[tabs] +==== +Java:: ++ +[source,java] +---- +from("direct:chat") + .to("openai:chat-completion?model=gpt-4" + + "¶llelToolExecution=true" + + "¶llelToolTimeout=30000" + + "&mcpServer.api.transportType=streamableHttp" + + "&mcpServer.api.url=http://localhost:9090/mcp"); +---- + +XML:: ++ +[source,xml] +---- +<route> + <from uri="direct:chat"/> + <to uri="openai:chat-completion?model=gpt-4&parallelToolExecution=true&parallelToolTimeout=30000&mcpServer.api.transportType=streamableHttp&mcpServer.api.url=http://localhost:9090/mcp"/> +</route> +---- + +YAML:: ++ +[source,yaml] +---- +- route: + from: + uri: direct:chat + steps: + - to: + uri: openai:chat-completion + parameters: + model: gpt-4 + parallelToolExecution: true + parallelToolTimeout: 30000 + mcpServer.api.transportType: streamableHttp + mcpServer.api.url: http://localhost:9090/mcp +---- +==== + +The option applies to both the agentic loop and the `openai:tool-execution` operation. + +Notes: + +* Results are always fed back to the model in the order the model requested the tools, as required by the OpenAI API, + regardless of the order in which the tool calls complete. +* A batch containing a single tool call is executed inline, without a thread hand-off. +* `parallelToolTimeout` (milliseconds, default `0` = disabled) bounds the batch as a whole, so that one slow tool cannot + block it. A tool call that exceeds it is cancelled and then handled according to `toolExecutionErrorStrategy`. Leaving + it disabled is reasonable, because `mcpTimeout` already bounds each individual MCP request. +* With `toolExecutionErrorStrategy=failExchange`, the sibling tool calls that were already dispatched are allowed to + complete before the exchange fails. Sequential execution instead abandons the remaining calls at the first failure. +* The thread pool comes from Camel's `ExecutorServiceManager` and follows the default thread pool profile, so it is + visible over JMX and shut down with the `CamelContext`. When virtual threads are enabled + (`camel.main.virtualThreadsEnabled=true`), the pool automatically becomes a thread-per-task virtual thread executor and + the profile's pool sizes no longer apply — concurrency is then bounded by the profile's `maxQueueSize` instead. +* `stdio` MCP servers multiplex all calls over a single child process, so the achievable parallelism there depends on + the server implementation. HTTP-based transports (`streamableHttp`, `sse`) benefit the most. + === Manual Tool Loop with `tool-execution` Operation When `autoToolExecution=false`, you can implement your own tool loop using the `openai:tool-execution` operation and Camel's `loopDoWhile` EIP. This gives you full control to add logging, filtering, retry logic, or custom routing between tool calls — without writing any Java code. @@ -524,6 +593,68 @@ YAML:: ---- ==== +=== Runtime Tool Refresh + +MCP servers may add, remove or change tools while they are running, and announce it with a `tools/list_changed` +notification. When `mcpToolRefresh=true` (the default), the component subscribes to that notification and refreshes the +tool list it advertises to the model, so a long-running route picks up server-side changes without being restarted. + +Set `mcpToolRefresh=false` to pin the tool list to whatever was listed when the endpoint started, for deployments that +require a deterministic set of tools: + +[tabs] +==== +Java:: ++ +[source,java] +---- +from("direct:chat") + .to("openai:chat-completion?model=gpt-4" + + "&mcpToolRefresh=false" + + "&mcpServer.api.transportType=streamableHttp" + + "&mcpServer.api.url=http://localhost:9090/mcp"); +---- + +XML:: ++ +[source,xml] +---- +<route> + <from uri="direct:chat"/> + <to uri="openai:chat-completion?model=gpt-4&mcpToolRefresh=false&mcpServer.api.transportType=streamableHttp&mcpServer.api.url=http://localhost:9090/mcp"/> +</route> +---- + +YAML:: ++ +[source,yaml] +---- +- route: + from: + uri: direct:chat + steps: + - to: + uri: openai:chat-completion + parameters: + model: gpt-4 + mcpToolRefresh: false + mcpServer.api.transportType: streamableHttp + mcpServer.api.url: http://localhost:9090/mcp +---- +==== + +Notes: + +* A refresh replaces only the tools of the server that sent the notification. Tools from the other configured servers are + left untouched. +* The per-server `toolNames` include list is re-applied on every refresh, so a server cannot widen its exposed tool set + by announcing new tools. +* Duplicate tool names are resolved the same way as at startup: the server that registered the name first keeps it. +* A tool call already in flight when a refresh lands runs to completion. If the model then calls a tool that the refresh + removed, it is handled as a hallucinated tool name according to `hallucinatedToolNameStrategy`. +* `returnDirect` flags set programmatically with `addReturnDirectTool` / `removeReturnDirectTool` survive a refresh and a + reconnect. + === MCP Connection Recovery When `mcpReconnect=true` (the default), the component automatically recovers from MCP server connection failures. If a tool call fails with a transport error, the component: diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/jbang/camel-jbang-configuration-metadata.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/jbang/camel-jbang-configuration-metadata.json index 4f02be84a402..822581e20943 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/jbang/camel-jbang-configuration-metadata.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/jbang/camel-jbang-configuration-metadata.json @@ -23,8 +23,8 @@ { "name": "camel.jbang.javaVersion", "required": false, "description": "Java version", "type": "enum", "javaType": "String", "defaultValue": "21", "secret": false, "enum": [ "21" ] }, { "name": "camel.jbang.jfr", "required": false, "description": "Enables Java Flight Recorder saving recording to disk on exit", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, { "name": "camel.jbang.jfr-profile", "required": false, "description": "Java Flight Recorder profile to use (such as default or profile)", "type": "string", "javaType": "String", "defaultValue": "default", "secret": false }, - { "name": "camel.jbang.jib-maven-plugin-version", "required": false, "description": "Version to use for jib-maven-plugin when exporting the application", "label": "maven", "type": "string", "javaType": "String", "defaultValue": "3.5.2", "secret": false }, - { "name": "camel.jbang.jkube-maven-plugin-version", "required": false, "description": "Version to use for jkube-maven-plugin if exporting to camel-main and have Kubernetes enabled (jkube.xxx options)", "label": "kubernetes", "type": "string", "javaType": "String", "defaultValue": "1.20.0", "secret": false }, + { "name": "camel.jbang.jib-maven-plugin-version", "required": false, "description": "Version to use for jib-maven-plugin when exporting the application", "label": "maven", "type": "string", "javaType": "String", "defaultValue": "3.5.1", "secret": false }, + { "name": "camel.jbang.jkube-maven-plugin-version", "required": false, "description": "Version to use for jkube-maven-plugin if exporting to camel-main and have Kubernetes enabled (jkube.xxx options)", "label": "kubernetes", "type": "string", "javaType": "String", "defaultValue": "1.19.0", "secret": false }, { "name": "camel.jbang.jkubeFiles", "required": false, "description": "Resource YAML fragments for Kubernetes using Eclipse JKube tool (Use commas to separate multiple files).", "label": "kubernetes", "type": "string", "javaType": "String", "secret": false }, { "name": "camel.jbang.kameletsVersion", "required": false, "description": "Apache Camel Kamelets version. By default the Kamelets are the same version as Camel.", "type": "string", "javaType": "String", "secret": false }, { "name": "camel.jbang.lazyBean", "required": false, "description": "Whether to use lazy bean initialization (can help with complex classloading issues)", "label": "advanced", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, @@ -37,7 +37,6 @@ { "name": "camel.jbang.metrics", "required": false, "description": "Metrics (Micrometer and Prometheus) at \/observe\/metrics on local HTTP server (port 8080 by default) when running standalone Camel", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false, "deprecated": true }, { "name": "camel.jbang.openApi", "required": false, "description": "File name of open-api spec file (JSON or YAML) to generate routes from the swagger\/openapi API spec file.", "type": "string", "javaType": "String", "secret": false }, { "name": "camel.jbang.packageScanJars", "required": false, "description": "Whether to automatic package scan JARs for custom Spring or Quarkus beans making them available for Camel CLI", "label": "advanced", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, - { "name": "camel.jbang.parentPom", "required": false, "description": "Parent POM coordinate (groupId:artifactId:version) to use in the exported project", "type": "string", "javaType": "String", "secret": false }, { "name": "camel.jbang.prompt", "required": false, "description": "Allow user to type in required parameters in prompt if not present in application", "label": "advanced", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, { "name": "camel.jbang.quarkusArtifactId", "required": false, "description": "artifactId of Quarkus Platform BOM", "label": "quarkus", "type": "string", "javaType": "String", "secret": false, "deprecated": true }, { "name": "camel.jbang.quarkusExtensionRegistryBaseUri", "required": false, "description": "The base URI of Quarkus Extension Registry", "label": "quarkus", "type": "string", "javaType": "String", "secret": false }, @@ -45,7 +44,6 @@ { "name": "camel.jbang.quarkusVersion", "required": false, "description": "version of Quarkus Platform BOM", "label": "quarkus", "type": "string", "javaType": "String", "secret": false }, { "name": "camel.jbang.readmeFiles", "required": false, "description": "README files included with the integration (Use commas to separate multiple files)", "type": "string", "javaType": "String", "secret": false }, { "name": "camel.jbang.repos", "required": false, "description": "Additional Maven repositories for download on-demand (Use commas to separate multiple repositories)", "label": "maven", "type": "string", "javaType": "String", "secret": false }, - { "name": "camel.jbang.resourceDirs", "required": false, "description": "Additional resource directories to include recursively, preserving directory structure (Use commas to separate multiple directories)", "type": "string", "javaType": "String", "secret": false }, { "name": "camel.jbang.runtime", "required": false, "description": "Which runtime to use (camel-main, spring-boot, quarkus)", "type": "enum", "javaType": "String", "secret": false, "enum": [ "camel-main", "spring-boot", "quarkus" ] }, { "name": "camel.jbang.scriptFiles", "required": false, "description": "Additional shell script files to export to src\/main\/scripts directory", "type": "string", "javaType": "String", "secret": false }, { "name": "camel.jbang.sourceDir", "required": false, "description": "Source directory for dynamically loading Camel file(s) to run. When using this, then files cannot be specified at the same time.", "label": "advanced", "type": "string", "javaType": "String", "secret": false }, diff --git a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java index 9519713469f1..41665230d82d 100644 --- a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java +++ b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java @@ -90,6 +90,8 @@ public class OpenAIEndpointConfigurer extends PropertyConfigurerSupport implemen case "mcpServer": target.getConfiguration().setMcpServer(property(camelContext, java.util.Map.class, value)); return true; case "mcptimeout": case "mcpTimeout": target.getConfiguration().setMcpTimeout(property(camelContext, int.class, value)); return true; + case "mcptoolrefresh": + case "mcpToolRefresh": target.getConfiguration().setMcpToolRefresh(property(camelContext, boolean.class, value)); return true; case "model": target.getConfiguration().setModel(property(camelContext, java.lang.String.class, value)); return true; case "oauthprofile": case "oauthProfile": target.getConfiguration().setOauthProfile(property(camelContext, java.lang.String.class, value)); return true; @@ -225,6 +227,8 @@ public class OpenAIEndpointConfigurer extends PropertyConfigurerSupport implemen case "mcpServer": return java.util.Map.class; case "mcptimeout": case "mcpTimeout": return int.class; + case "mcptoolrefresh": + case "mcpToolRefresh": return boolean.class; case "model": return java.lang.String.class; case "oauthprofile": case "oauthProfile": return java.lang.String.class; @@ -361,6 +365,8 @@ public class OpenAIEndpointConfigurer extends PropertyConfigurerSupport implemen case "mcpServer": return target.getConfiguration().getMcpServer(); case "mcptimeout": case "mcpTimeout": return target.getConfiguration().getMcpTimeout(); + case "mcptoolrefresh": + case "mcpToolRefresh": return target.getConfiguration().isMcpToolRefresh(); case "model": return target.getConfiguration().getModel(); case "oauthprofile": case "oauthProfile": return target.getConfiguration().getOauthProfile(); diff --git a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java index 5e751f585b10..f09adda48780 100644 --- a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java +++ b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java @@ -24,7 +24,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map<String, String> MULTI_VALUE_PREFIXES; static { - Set<String> props = new HashSet<>(67); + Set<String> props = new HashSet<>(68); props.add("additionalBodyProperty"); props.add("additionalHeader"); props.add("additionalResponseHeader"); @@ -59,6 +59,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component props.add("mcpReconnect"); props.add("mcpServer"); props.add("mcpTimeout"); + props.add("mcpToolRefresh"); props.add("model"); props.add("oauthProfile"); props.add("operation"); diff --git a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json index c1c640d00adc..3ded34a943e7 100644 --- a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json +++ b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json @@ -120,38 +120,39 @@ "mcpReconnect": { "index": 31, "kind": "parameter", "displayName": "Mcp Reconnect", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Automatically reconnect to MCP servers when a tool call fails due to a t [...] "mcpServer": { "index": 32, "kind": "parameter", "displayName": "Mcp Server", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Cont [...] "mcpTimeout": { "index": 33, "kind": "parameter", "displayName": "Mcp Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 20, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in seconds for MCP tool call requests. Applies to all MCP operations inclu [...] - "model": { "index": 34, "kind": "parameter", "displayName": "Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for chat completion" }, - "outputClass": { "index": 35, "kind": "parameter", "displayName": "Output Class", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Fully qualified class name for structured output using response format" }, - "parallelToolExecution": { "index": 36, "kind": "parameter", "displayName": "Parallel Tool Execution", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Execute the tool calls returned by the model in a si [...] - "parallelToolTimeout": { "index": 37, "kind": "parameter", "displayName": "Parallel Tool Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in milliseconds for a batch of parallel tool calls, so [...] - "previousResponseId": { "index": 38, "kind": "parameter", "displayName": "Previous Response Id", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Previous response id for OpenAI server-side conversation state (Responses [...] - "requestTimeout": { "index": 39, "kind": "parameter", "displayName": "Request Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "HTTP request timeout in milliseconds for the OpenAI SDK client. When 0 or [...] - "speechInstructions": { "index": 40, "kind": "parameter", "displayName": "Speech Instructions", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Optional instructions to control the voice of the generated audio. Does no [...] - "speechModel": { "index": 41, "kind": "parameter", "displayName": "Speech Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)" }, - "speechResponseFormat": { "index": 42, "kind": "parameter", "displayName": "Speech Response Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "mp3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "descrip [...] - "speechSpeed": { "index": 43, "kind": "parameter", "displayName": "Speech Speed", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The speed of the generated audio, from 0.25 to 4.0 where 1.0 is normal speed" }, - "speechVoice": { "index": 44, "kind": "parameter", "displayName": "Speech Voice", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "alloy", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The voice to use for text-to-speech (e.g., alloy, echo, fable, [...] - "storeFullResponse": { "index": 45, "kind": "parameter", "displayName": "Store Full Response", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Store the full SDK response in non-streaming mode: chat-comp [...] - "streaming": { "index": 46, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, - "stripThinking": { "index": 47, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like [...] - "systemMessage": { "index": 48, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversat [...] - "temperature": { "index": 49, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, - "toolExecutionErrorStrategy": { "index": 50, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfigur [...] - "topP": { "index": 51, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, - "userMessage": { "index": 52, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, - "lazyStartProducer": { "index": 53, "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 produ [...] - "oauthProfile": { "index": 54, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Cred [...] - "sslContextParameters": { "index": 55, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use [...] - "sslEndpointAlgorithm": { "index": 56, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", [...] - "sslKeymanagerAlgorithm": { "index": 57, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key [...] - "sslKeyPassword": { "index": 58, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, - "sslKeystoreLocation": { "index": 59, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be [...] - "sslKeystorePassword": { "index": 60, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, - "sslKeystoreType": { "index": 61, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, - "sslProtocol": { "index": 62, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, - "sslTrustmanagerAlgorithm": { "index": 63, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the tru [...] - "sslTruststoreLocation": { "index": 64, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the [...] - "sslTruststorePassword": { "index": 65, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. [...] - "sslTruststoreType": { "index": 66, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } + "mcpToolRefresh": { "index": 34, "kind": "parameter", "displayName": "Mcp Tool Refresh", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Refresh the advertised tool list when an MCP server notifies that i [...] + "model": { "index": 35, "kind": "parameter", "displayName": "Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for chat completion" }, + "outputClass": { "index": 36, "kind": "parameter", "displayName": "Output Class", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Fully qualified class name for structured output using response format" }, + "parallelToolExecution": { "index": 37, "kind": "parameter", "displayName": "Parallel Tool Execution", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Execute the tool calls returned by the model in a si [...] + "parallelToolTimeout": { "index": 38, "kind": "parameter", "displayName": "Parallel Tool Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in milliseconds for a batch of parallel tool calls, so [...] + "previousResponseId": { "index": 39, "kind": "parameter", "displayName": "Previous Response Id", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Previous response id for OpenAI server-side conversation state (Responses [...] + "requestTimeout": { "index": 40, "kind": "parameter", "displayName": "Request Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "HTTP request timeout in milliseconds for the OpenAI SDK client. When 0 or [...] + "speechInstructions": { "index": 41, "kind": "parameter", "displayName": "Speech Instructions", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Optional instructions to control the voice of the generated audio. Does no [...] + "speechModel": { "index": 42, "kind": "parameter", "displayName": "Speech Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)" }, + "speechResponseFormat": { "index": 43, "kind": "parameter", "displayName": "Speech Response Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "mp3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "descrip [...] + "speechSpeed": { "index": 44, "kind": "parameter", "displayName": "Speech Speed", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The speed of the generated audio, from 0.25 to 4.0 where 1.0 is normal speed" }, + "speechVoice": { "index": 45, "kind": "parameter", "displayName": "Speech Voice", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "alloy", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The voice to use for text-to-speech (e.g., alloy, echo, fable, [...] + "storeFullResponse": { "index": 46, "kind": "parameter", "displayName": "Store Full Response", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Store the full SDK response in non-streaming mode: chat-comp [...] + "streaming": { "index": 47, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, + "stripThinking": { "index": 48, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like [...] + "systemMessage": { "index": 49, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversat [...] + "temperature": { "index": 50, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, + "toolExecutionErrorStrategy": { "index": 51, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfigur [...] + "topP": { "index": 52, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, + "userMessage": { "index": 53, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, + "lazyStartProducer": { "index": 54, "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 produ [...] + "oauthProfile": { "index": 55, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Cred [...] + "sslContextParameters": { "index": 56, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use [...] + "sslEndpointAlgorithm": { "index": 57, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", [...] + "sslKeymanagerAlgorithm": { "index": 58, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key [...] + "sslKeyPassword": { "index": 59, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, + "sslKeystoreLocation": { "index": 60, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be [...] + "sslKeystorePassword": { "index": 61, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, + "sslKeystoreType": { "index": 62, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, + "sslProtocol": { "index": 63, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, + "sslTrustmanagerAlgorithm": { "index": 64, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the tru [...] + "sslTruststoreLocation": { "index": 65, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the [...] + "sslTruststorePassword": { "index": 66, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. [...] + "sslTruststoreType": { "index": 67, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } } } 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 8f5fb2e6accb..ec12fccde32a 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 @@ -593,6 +593,68 @@ YAML:: ---- ==== +=== Runtime Tool Refresh + +MCP servers may add, remove or change tools while they are running, and announce it with a `tools/list_changed` +notification. When `mcpToolRefresh=true` (the default), the component subscribes to that notification and refreshes the +tool list it advertises to the model, so a long-running route picks up server-side changes without being restarted. + +Set `mcpToolRefresh=false` to pin the tool list to whatever was listed when the endpoint started, for deployments that +require a deterministic set of tools: + +[tabs] +==== +Java:: ++ +[source,java] +---- +from("direct:chat") + .to("openai:chat-completion?model=gpt-4" + + "&mcpToolRefresh=false" + + "&mcpServer.api.transportType=streamableHttp" + + "&mcpServer.api.url=http://localhost:9090/mcp"); +---- + +XML:: ++ +[source,xml] +---- +<route> + <from uri="direct:chat"/> + <to uri="openai:chat-completion?model=gpt-4&mcpToolRefresh=false&mcpServer.api.transportType=streamableHttp&mcpServer.api.url=http://localhost:9090/mcp"/> +</route> +---- + +YAML:: ++ +[source,yaml] +---- +- route: + from: + uri: direct:chat + steps: + - to: + uri: openai:chat-completion + parameters: + model: gpt-4 + mcpToolRefresh: false + mcpServer.api.transportType: streamableHttp + mcpServer.api.url: http://localhost:9090/mcp +---- +==== + +Notes: + +* A refresh replaces only the tools of the server that sent the notification. Tools from the other configured servers are + left untouched. +* The per-server `toolNames` include list is re-applied on every refresh, so a server cannot widen its exposed tool set + by announcing new tools. +* Duplicate tool names are resolved the same way as at startup: the server that registered the name first keeps it. +* A tool call already in flight when a refresh lands runs to completion. If the model then calls a tool that the refresh + removed, it is handled as a hallucinated tool name according to `hallucinatedToolNameStrategy`. +* `returnDirect` flags set programmatically with `addReturnDirectTool` / `removeReturnDirectTool` survive a refresh and a + reconnect. + === MCP Connection Recovery When `mcpReconnect=true` (the default), the component automatically recovers from MCP server connection failures. If a tool call fails with a transport error, the component: diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java index 9df4fc72c21a..4afd1b77879c 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java @@ -209,6 +209,12 @@ public class OpenAIConfiguration implements Cloneable { + "and retry the call once.") private boolean mcpReconnect = true; + @UriParam(defaultValue = "true") + @Metadata(description = "Refresh the advertised tool list when an MCP server notifies that its tools changed. " + + "Set to false to keep the tool list fixed to what was listed when the endpoint started, " + + "for deployments that require a deterministic set of tools.") + private boolean mcpToolRefresh = true; + @UriParam(enums = "failExchange,repromptModel", defaultValue = "failExchange") @Metadata(description = "Strategy for handling exceptions thrown during MCP tool execution. " + "'failExchange' (default) propagates the exception to the Camel exchange so that standard Camel " @@ -754,6 +760,14 @@ public class OpenAIConfiguration implements Cloneable { this.mcpReconnect = mcpReconnect; } + public boolean isMcpToolRefresh() { + return mcpToolRefresh; + } + + public void setMcpToolRefresh(boolean mcpToolRefresh) { + this.mcpToolRefresh = mcpToolRefresh; + } + public ToolExecutionErrorStrategy getToolExecutionErrorStrategy() { return toolExecutionErrorStrategy; } 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 e33369776aa3..094a5a9e7afe 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 @@ -25,7 +25,10 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @@ -93,6 +96,9 @@ public class OpenAIEndpoint extends DefaultEndpoint { private final ReentrantLock globalMcpLock = new ReentrantLock(); private Map<String, ReentrantLock> mcpClientLocks; + private final Set<String> manualReturnDirectAdded = ConcurrentHashMap.newKeySet(); + private final Set<String> manualReturnDirectRemoved = ConcurrentHashMap.newKeySet(); + private volatile McpToolState mcpToolState = McpToolState.empty(); private volatile boolean mcpStopped; private Map<String, Map<String, String>> serverConfigs; @@ -285,10 +291,19 @@ public class OpenAIEndpoint extends DefaultEndpoint { LOG.debug("Creating MCP transport for server '{}' with type '{}'", serverName, transportType); McpClientTransport transport = createMcpTransport(serverName, transportType, props); Duration timeout = Duration.ofSeconds(configuration.getMcpTimeout()); - McpSyncClient mcpClient = McpClient.sync(transport) + McpClient.SyncSpec spec = McpClient.sync(transport) .requestTimeout(timeout) - .initializationTimeout(timeout) - .build(); + .initializationTimeout(timeout); + + // The callback needs the client, which does not exist until build(), so hand it a holder that is + // populated below. Notifications can only arrive once the transport is connected by initialize(). + AtomicReference<McpSyncClient> clientHolder = new AtomicReference<>(); + if (configuration.isMcpToolRefresh()) { + spec.toolsChangeConsumer(tools -> onToolsChanged(serverName, clientHolder.get(), tools)); + } + + McpSyncClient mcpClient = spec.build(); + clientHolder.set(mcpClient); mcpClient.initialize(); return mcpClient; } @@ -365,42 +380,9 @@ public class OpenAIEndpoint extends DefaultEndpoint { List<McpSchema.Tool> tools = filterTools(newClient.listTools().tools(), serverName, props); - globalMcpLock.lock(); - try { - if (mcpStopped) { - newClient.closeGracefully(); - return null; - } - // Rebuild shared tool state, replacing this server's old tools with the freshly listed ones - Set<String> oldServerTools = toolsForServer(serverName); - List<ChatCompletionFunctionTool> newTools = new ArrayList<>(mcpToolState.tools()); - Map<String, McpSyncClient> newClientMap = new HashMap<>(mcpToolState.toolClientMap()); - Map<String, String> newToolToServer = new HashMap<>(mcpToolState.toolToServerName()); - Set<String> newReturnDirect = new HashSet<>(mcpToolState.returnDirectTools()); - - newTools.removeIf(t -> oldServerTools.contains(t.function().name())); - newClientMap.keySet().removeIf(oldServerTools::contains); - newToolToServer.keySet().removeIf(oldServerTools::contains); - newReturnDirect.removeIf(oldServerTools::contains); - - for (McpSchema.Tool tool : tools) { - if (newClientMap.containsKey(tool.name())) { - LOG.warn("Duplicate MCP tool name '{}' from server '{}', using first registered", tool.name(), - serverName); - } else { - newTools.addAll(McpToolConverter.convert(List.of(tool))); - newClientMap.put(tool.name(), newClient); - newToolToServer.put(tool.name(), serverName); - if (isReturnDirect(tool)) { - newReturnDirect.add(tool.name()); - } - } - } - - // Publish immutable snapshots for lock-free readers - mcpToolState = new McpToolState(newTools, newClientMap, newToolToServer, newReturnDirect); - } finally { - globalMcpLock.unlock(); + if (!republishServerTools(serverName, newClient, tools)) { + newClient.closeGracefully(); + return null; } LOG.info("Reconnected MCP server '{}' with {} tools: {}", serverName, tools.size(), @@ -412,6 +394,125 @@ public class OpenAIEndpoint extends DefaultEndpoint { } } + /** + * Rebuilds and publishes the shared tool state, replacing the tools currently registered for {@code serverName} + * with the given ones. Shared by the reconnect and the runtime tool refresh paths so that duplicate name handling, + * {@code returnDirect} detection and the manual {@code returnDirect} overrides stay identical between the two. + * + * @param serverName the MCP server whose tools are being replaced + * @param client the client owning the given tools + * @param tools the tools to register, already filtered by the per-server {@code toolNames} include list + * @return {@code false} when the endpoint is stopping and the state was left untouched + */ + private boolean republishServerTools(String serverName, McpSyncClient client, List<McpSchema.Tool> tools) { + globalMcpLock.lock(); + try { + if (mcpStopped) { + return false; + } + + Set<String> oldServerTools = toolsForServer(serverName); + List<ChatCompletionFunctionTool> newTools = new ArrayList<>(mcpToolState.tools()); + Map<String, McpSyncClient> newClientMap = new HashMap<>(mcpToolState.toolClientMap()); + Map<String, String> newToolToServer = new HashMap<>(mcpToolState.toolToServerName()); + Set<String> newReturnDirect = new HashSet<>(mcpToolState.returnDirectTools()); + + newTools.removeIf(t -> oldServerTools.contains(t.function().name())); + newClientMap.keySet().removeIf(oldServerTools::contains); + newToolToServer.keySet().removeIf(oldServerTools::contains); + newReturnDirect.removeIf(oldServerTools::contains); + + for (McpSchema.Tool tool : tools) { + if (newClientMap.containsKey(tool.name())) { + LOG.warn("Duplicate MCP tool name '{}' from server '{}', using first registered", tool.name(), + serverName); + } else { + newTools.addAll(McpToolConverter.convert(List.of(tool))); + newClientMap.put(tool.name(), client); + newToolToServer.put(tool.name(), serverName); + if (isReturnDirect(tool)) { + newReturnDirect.add(tool.name()); + } + } + } + + // Publish immutable snapshots for lock-free readers + mcpToolState = new McpToolState( + newTools, newClientMap, newToolToServer, + applyManualReturnDirectOverrides(newReturnDirect, newClientMap.keySet())); + return true; + } finally { + globalMcpLock.unlock(); + } + } + + /** + * Handles a {@code tools/list_changed} notification from an MCP server. The SDK has already re-listed the tools and + * passes them here, but without the per-server {@code toolNames} filter, which is re-applied before publishing. + * + * @param serverName the server that reported the change + * @param client the client that received the notification + * @param tools the unfiltered tool list as re-listed by the SDK + */ + void onToolsChanged(String serverName, McpSyncClient client, List<McpSchema.Tool> tools) { + if (!configuration.isMcpToolRefresh()) { + return; + } + + if (client == null) { + // Notifications only flow after initialize(), which runs once the holder is populated + LOG.debug("Ignoring tools list change from MCP server '{}' received before the client was ready", serverName); + return; + } + + Map<String, String> props = serverConfigs != null ? serverConfigs.get(serverName) : null; + if (props == null) { + LOG.warn("Cannot refresh tools: no configuration found for MCP server '{}'", serverName); + return; + } + + globalMcpLock.lock(); + try { + McpSyncClient current = currentClientForServer(serverName); + if (current != null && current != client) { + // A reconnect already replaced this client, so its view of the tools is stale + LOG.debug("Ignoring tools list change from a superseded client of MCP server '{}'", serverName); + return; + } + + List<McpSchema.Tool> filtered = filterTools(tools, serverName, props); + if (republishServerTools(serverName, client, filtered)) { + LOG.info("Refreshed MCP server '{}' with {} tools: {}", serverName, filtered.size(), + filtered.stream().map(McpSchema.Tool::name).toList()); + } + } finally { + globalMcpLock.unlock(); + } + } + + private McpSyncClient currentClientForServer(String serverName) { + return toolsForServer(serverName).stream() + .map(toolName -> mcpToolState.toolClientMap().get(toolName)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** + * Re-applies the {@code returnDirect} flags set programmatically through {@link #addReturnDirectTool(String)} and + * {@link #removeReturnDirectTool(String)}, so that a reconnect or a tool refresh does not discard them. Overrides + * for tools that no longer exist are not reinstated, so that a tool which vanished server-side leaves no stale + * entry behind. + * + * @param returnDirectTools the flags rebuilt from the tool annotations, modified in place + * @param knownTools the names of the tools registered in the new state + */ + private Set<String> applyManualReturnDirectOverrides(Set<String> returnDirectTools, Set<String> knownTools) { + manualReturnDirectAdded.stream().filter(knownTools::contains).forEach(returnDirectTools::add); + returnDirectTools.removeAll(manualReturnDirectRemoved); + return returnDirectTools; + } + /** * Filters the tools listed from an MCP server according to the per-server {@code toolNames} include list. When no * {@code toolNames} property is configured, all tools are returned unchanged. @@ -647,6 +748,11 @@ public class OpenAIEndpoint extends DefaultEndpoint { public void addReturnDirectTool(String toolName) { globalMcpLock.lock(); try { + // Remembered so a reconnect or a tool refresh, which rebuild the flags from the tool annotations, + // does not silently discard the override + manualReturnDirectAdded.add(toolName); + manualReturnDirectRemoved.remove(toolName); + Set<String> newReturnDirect = new HashSet<>(mcpToolState.returnDirectTools()); newReturnDirect.add(toolName); mcpToolState = new McpToolState( @@ -660,6 +766,9 @@ public class OpenAIEndpoint extends DefaultEndpoint { public void removeReturnDirectTool(String toolName) { globalMcpLock.lock(); try { + manualReturnDirectRemoved.add(toolName); + manualReturnDirectAdded.remove(toolName); + Set<String> newReturnDirect = new HashSet<>(mcpToolState.returnDirectTools()); newReturnDirect.remove(toolName); mcpToolState = new McpToolState( diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEndpointMcpToolRefreshTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEndpointMcpToolRefreshTest.java new file mode 100644 index 000000000000..e65c3d47641f --- /dev/null +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEndpointMcpToolRefreshTest.java @@ -0,0 +1,263 @@ +/* + * 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +import com.openai.models.chat.completions.ChatCompletionFunctionTool; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for CAMEL-23078: handling of the MCP {@code tools/list_changed} notification, which refreshes the advertised + * tool list at runtime instead of keeping whatever was listed when the endpoint started. + */ +class OpenAIEndpointMcpToolRefreshTest { + + private static final String SERVER = "test-server"; + private static final String OTHER_SERVER = "other-server"; + + @Test + void refreshRegistersToolsAddedByTheServer() { + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather")); + + endpoint.onToolsChanged(SERVER, client, tools("get_weather", "get_traffic")); + + assertThat(endpoint.getMcpToolState().toolClientMap()).containsOnlyKeys("get_weather", "get_traffic"); + assertThat(toolNames(endpoint)).containsExactlyInAnyOrder("get_weather", "get_traffic"); + assertThat(endpoint.getMcpToolState().toolToServerName()) + .containsEntry("get_traffic", SERVER); + } + + @Test + void refreshDropsToolsRemovedByTheServer() { + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather", "get_traffic")); + + endpoint.onToolsChanged(SERVER, client, tools("get_weather")); + + assertThat(endpoint.getMcpToolState().toolClientMap()).containsOnlyKeys("get_weather"); + assertThat(toolNames(endpoint)).containsExactly("get_weather"); + assertThat(endpoint.getMcpToolState().toolToServerName()).doesNotContainKey("get_traffic"); + } + + @Test + void refreshLeavesOtherServersUntouched() { + McpSyncClient client = mock(McpSyncClient.class); + McpSyncClient otherClient = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather")); + registerServer(endpoint, OTHER_SERVER, otherClient, List.of("send_email")); + + endpoint.onToolsChanged(SERVER, client, tools("get_traffic")); + + assertThat(endpoint.getMcpToolState().toolClientMap()) + .containsEntry("send_email", otherClient) + .containsEntry("get_traffic", client) + .doesNotContainKey("get_weather"); + } + + @Test + void refreshReappliesThePerServerToolNamesFilter() { + // the SDK hands the consumer the unfiltered tool list, so the include list must be applied again + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather")); + endpoint.setServerConfigs(new ConcurrentHashMap<>( + Map.of( + SERVER, Map.of("transportType", "stdio", "toolNames", "get_weather,get_traffic")))); + + endpoint.onToolsChanged(SERVER, client, tools("get_weather", "get_traffic", "delete_everything")); + + assertThat(endpoint.getMcpToolState().toolClientMap()) + .containsOnlyKeys("get_weather", "get_traffic"); + } + + @Test + void refreshSkipsToolNamesAlreadyRegisteredByAnotherServer() { + McpSyncClient client = mock(McpSyncClient.class); + McpSyncClient otherClient = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather")); + registerServer(endpoint, OTHER_SERVER, otherClient, List.of("get_traffic")); + + endpoint.onToolsChanged(SERVER, client, tools("get_weather", "get_traffic")); + + // the duplicate stays with the server that registered it first + assertThat(endpoint.getMcpToolState().toolClientMap()).containsEntry("get_traffic", otherClient); + } + + @Test + void refreshTracksReturnDirectAnnotations() { + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather")); + + endpoint.onToolsChanged(SERVER, client, List.of( + tool("get_weather"), returnDirectTool("fetch_report"))); + + assertThat(endpoint.getMcpToolState().returnDirectTools()).containsExactly("fetch_report"); + } + + @Test + void refreshPreservesManualReturnDirectOverrides() { + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather")); + endpoint.addReturnDirectTool("get_weather"); + + endpoint.onToolsChanged(SERVER, client, tools("get_weather", "get_traffic")); + + assertThat(endpoint.getMcpToolState().returnDirectTools()).contains("get_weather"); + } + + @Test + void refreshPreservesManualReturnDirectRemovals() { + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("fetch_report")); + endpoint.removeReturnDirectTool("fetch_report"); + + endpoint.onToolsChanged(SERVER, client, List.of(returnDirectTool("fetch_report"))); + + assertThat(endpoint.getMcpToolState().returnDirectTools()).doesNotContain("fetch_report"); + } + + @Test + void refreshDoesNotReinstateAnOverrideForAToolThatVanished() { + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather", "get_traffic")); + endpoint.addReturnDirectTool("get_traffic"); + + endpoint.onToolsChanged(SERVER, client, tools("get_weather")); + + assertThat(endpoint.getMcpToolState().returnDirectTools()).doesNotContain("get_traffic"); + } + + @Test + void refreshIsIgnoredWhenDisabled() { + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather")); + endpoint.getConfiguration().setMcpToolRefresh(false); + + endpoint.onToolsChanged(SERVER, client, tools("get_weather", "get_traffic")); + + assertThat(endpoint.getMcpToolState().toolClientMap()).containsOnlyKeys("get_weather"); + } + + @Test + void refreshFromASupersededClientIsIgnored() { + // a reconnect already replaced the client, so a notification still in flight from the old one is stale + McpSyncClient reconnected = mock(McpSyncClient.class); + McpSyncClient superseded = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(reconnected, List.of("get_weather")); + + endpoint.onToolsChanged(SERVER, superseded, tools("get_weather", "get_traffic")); + + assertThat(endpoint.getMcpToolState().toolClientMap()) + .containsOnlyKeys("get_weather") + .containsEntry("get_weather", reconnected); + } + + @Test + void refreshForAnUnknownServerIsIgnored() { + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather")); + + endpoint.onToolsChanged("never-configured", client, tools("get_traffic")); + + assertThat(endpoint.getMcpToolState().toolClientMap()).containsOnlyKeys("get_weather"); + } + + @Test + void refreshBeforeTheClientIsReadyIsIgnored() { + McpSyncClient client = mock(McpSyncClient.class); + OpenAIEndpoint endpoint = newEndpoint(client, List.of("get_weather")); + + endpoint.onToolsChanged(SERVER, null, tools("get_traffic")); + + assertThat(endpoint.getMcpToolState().toolClientMap()).containsOnlyKeys("get_weather"); + } + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + private OpenAIEndpoint newEndpoint(McpSyncClient client, List<String> initialTools) { + DefaultCamelContext ctx = new DefaultCamelContext(); + OpenAIComponent component = new OpenAIComponent(); + component.setCamelContext(ctx); + + OpenAIEndpoint endpoint = new OpenAIEndpoint("openai:chat-completion", component, new OpenAIConfiguration()); + endpoint.setCamelContext(ctx); + + endpoint.setMcpToolState(McpToolState.empty()); + endpoint.setServerConfigs(new ConcurrentHashMap<>(Map.of(SERVER, Map.of("transportType", "stdio")))); + endpoint.setMcpClientLocks(new ConcurrentHashMap<>(Map.of(SERVER, new ReentrantLock()))); + + registerServer(endpoint, SERVER, client, initialTools); + return endpoint; + } + + /** + * Seeds the shared tool state with the tools of one server, mirroring what endpoint startup would have published. + */ + private void registerServer( + OpenAIEndpoint endpoint, String serverName, McpSyncClient client, List<String> toolNames) { + McpToolState current = endpoint.getMcpToolState(); + + List<ChatCompletionFunctionTool> allTools = new ArrayList<>(current.tools()); + Map<String, McpSyncClient> clientMap = new HashMap<>(current.toolClientMap()); + Map<String, String> toolToServer = new HashMap<>(current.toolToServerName()); + + List<McpSchema.Tool> mcpTools = toolNames.stream().map(OpenAIEndpointMcpToolRefreshTest::tool).toList(); + allTools.addAll(McpToolConverter.convert(mcpTools)); + toolNames.forEach(name -> { + clientMap.put(name, client); + toolToServer.put(name, serverName); + }); + + endpoint.setMcpToolState( + new McpToolState(allTools, clientMap, toolToServer, current.returnDirectTools())); + } + + private static List<String> toolNames(OpenAIEndpoint endpoint) { + return endpoint.getMcpToolState().tools().stream() + .map(t -> t.function().name()) + .toList(); + } + + private static List<McpSchema.Tool> tools(String... names) { + return List.of(names).stream().map(OpenAIEndpointMcpToolRefreshTest::tool).toList(); + } + + private static McpSchema.Tool tool(String name) { + return McpSchema.Tool.builder(name, Map.of("type", "object")).description("mock " + name).build(); + } + + private static McpSchema.Tool returnDirectTool(String name) { + return McpSchema.Tool.builder(name, Map.of("type", "object")) + .description("mock " + name) + .annotations(McpSchema.ToolAnnotations.builder().returnDirect(true).build()) + .build(); + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index f56588c184c8..306a680e343a 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -414,6 +414,20 @@ tool calls that were already dispatched complete before the exchange fails, wher abandons the remaining calls at the first failure. Routes whose tools have side effects and that rely on a failure preventing later tool calls in the same batch should keep the sequential default. +==== MCP tool list is refreshed at runtime by default + +MCP servers announce tool additions, removals and changes with a `tools/list_changed` notification. That +notification was previously ignored, so the tool list advertised to the model was fixed to whatever was +listed when the endpoint started (and was only re-listed after a transport reconnect). The component now +subscribes to it and refreshes the tool list, which is the behavior the MCP specification expects. + +Set the new `mcpToolRefresh` option to `false` to restore the previous behavior and pin the tool list to +endpoint startup, for deployments that require a deterministic set of tools. + +Related fix: `returnDirect` flags set programmatically through `addReturnDirectTool` and +`removeReturnDirectTool` on `OpenAIEndpoint` are now remembered and re-applied when the tool state is +rebuilt. Previously a reconnect silently discarded them. + === camel-management - Throughput MBean attribute uses EWMA smoothing The `Throughput` attribute on the `ManagedPerformanceCounter` JMX MBean now reports an EWMA diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java index ce0c3e15890a..794ee7972c42 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java @@ -919,6 +919,42 @@ public interface OpenAIEndpointBuilderFactory { doSetProperty("mcpTimeout", mcpTimeout); return this; } + /** + * Refresh the advertised tool list when an MCP server notifies that its + * tools changed. Set to false to keep the tool list fixed to what was + * listed when the endpoint started, for deployments that require a + * deterministic set of tools. + * + * The option is a: <code>boolean</code> type. + * + * Default: true + * Group: producer + * + * @param mcpToolRefresh the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder mcpToolRefresh(boolean mcpToolRefresh) { + doSetProperty("mcpToolRefresh", mcpToolRefresh); + return this; + } + /** + * Refresh the advertised tool list when an MCP server notifies that its + * tools changed. Set to false to keep the tool list fixed to what was + * listed when the endpoint started, for deployments that require a + * deterministic set of tools. + * + * The option will be converted to a <code>boolean</code> type. + * + * Default: true + * Group: producer + * + * @param mcpToolRefresh the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder mcpToolRefresh(String mcpToolRefresh) { + doSetProperty("mcpToolRefresh", mcpToolRefresh); + return this; + } /** * The model to use for chat completion. * @@ -948,6 +984,90 @@ public interface OpenAIEndpointBuilderFactory { doSetProperty("outputClass", outputClass); return this; } + /** + * Execute the tool calls returned by the model in a single response + * concurrently instead of sequentially. Tool calls in the same batch + * are independent by design, so this reduces the latency of a batch to + * that of its slowest tool. Results are always fed back to the model in + * the original tool call order. Note that with + * toolExecutionErrorStrategy=failExchange the sibling tool calls + * already dispatched complete before the exchange fails. + * + * The option is a: <code>boolean</code> type. + * + * Default: false + * Group: producer + * + * @param parallelToolExecution the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder parallelToolExecution(boolean parallelToolExecution) { + doSetProperty("parallelToolExecution", parallelToolExecution); + return this; + } + /** + * Execute the tool calls returned by the model in a single response + * concurrently instead of sequentially. Tool calls in the same batch + * are independent by design, so this reduces the latency of a batch to + * that of its slowest tool. Results are always fed back to the model in + * the original tool call order. Note that with + * toolExecutionErrorStrategy=failExchange the sibling tool calls + * already dispatched complete before the exchange fails. + * + * The option will be converted to a <code>boolean</code> type. + * + * Default: false + * Group: producer + * + * @param parallelToolExecution the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder parallelToolExecution(String parallelToolExecution) { + doSetProperty("parallelToolExecution", parallelToolExecution); + return this; + } + /** + * Timeout in milliseconds for a batch of parallel tool calls, so that + * one slow tool cannot block the whole batch. The timeout applies to + * the batch as a whole, not per tool call. A tool call that exceeds it + * is cancelled and handled according to toolExecutionErrorStrategy. The + * default of 0 disables the batch timeout and relies on mcpTimeout, + * which already bounds each individual MCP request. Only used when + * parallelToolExecution=true. + * + * The option is a: <code>long</code> type. + * + * Default: 0 + * Group: producer + * + * @param parallelToolTimeout the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder parallelToolTimeout(long parallelToolTimeout) { + doSetProperty("parallelToolTimeout", parallelToolTimeout); + return this; + } + /** + * Timeout in milliseconds for a batch of parallel tool calls, so that + * one slow tool cannot block the whole batch. The timeout applies to + * the batch as a whole, not per tool call. A tool call that exceeds it + * is cancelled and handled according to toolExecutionErrorStrategy. The + * default of 0 disables the batch timeout and relies on mcpTimeout, + * which already bounds each individual MCP request. Only used when + * parallelToolExecution=true. + * + * The option will be converted to a <code>long</code> type. + * + * Default: 0 + * Group: producer + * + * @param parallelToolTimeout the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder parallelToolTimeout(String parallelToolTimeout) { + doSetProperty("parallelToolTimeout", parallelToolTimeout); + return this; + } /** * Previous response id for OpenAI server-side conversation state * (Responses API only).
