Federico Mariani created CAMEL-26396:
----------------------------------------
Summary: camel-jbang-mcp - the server lists itself as a running
integration, so camel_eval_expression times out on it and camel_control kill
fails with an internal error
Key: CAMEL-26396
URL: https://issues.apache.org/jira/browse/CAMEL-26396
Project: Camel
Issue Type: Bug
Components: camel-jbang, camel-jbang-mcp
Affects Versions: 4.23.0
Reporter: Federico Mariani
Assignee: Federico Mariani
Found on main (4.24.0-SNAPSHOT, commit ffef175654d1); the line numbers below
are from that commit. The same code is in the 4.23.0 release.
h3. Reproduction
An evaluation of coding agents on legacy-to-Camel migrations ran
camel-jbang-mcp 4.24.0-SNAPSHOT (built at ffef175654d1) over stdio in a Linux
container, with OpenCode + GLM 5.3. After a few {{camel_eval_expression}} calls
that ran in the local scratch context ({{"evaluatedIn":"a local scratch context
(no integration selected)"}}), the server's own JVM shows up in the process
list:
{code}
camel_runtime_processes {}
-> {"pid":50,"name":"io.quarkus.runner.GeneratedMain","contextName":"camel-9"}
{code}
{{ps}} in the same container shows that pid 50 is the camel-jbang-mcp runner
(pid 51 is another MCP server, 55 a third one):
{noformat}
agent 50 ... /opt/java/openjdk/bin/java
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.nio=ALL-UNNAMED ... -Dquarkus.log.lev...
{noformat}
>From then on a tool called without a name picks that "only running
>integration", the server itself:
{code}
camel_eval_expression {"body":"\"abc\"","expression":". |
length","language":"jq"}
-> No answer from the integration with pid 50 to the action eval within 10
seconds
camel_control {"action":"kill","name":"50"}
-> MCP error -32603: Internal error
{code}
The same jq calls answered at once a few calls earlier, in the scratch context.
With a real integration running next to it, the server makes two processes, so
a call without a name is refused instead:
{code}
camel_control {"action":"stop"}
-> No integration selected: pass name (an integration name or pid);
list_processes shows what runs, camel_run starts one
camel_runtime_processes {}
-> {"pid":50,"name":"io.quarkus.runner.GeneratedMain","contextName":"camel-12"}
{"pid":2325,"name":"alert-system","contextName":"alert-system"}
{code}
Eval evidence (200 trials on 4.24.0-SNAPSHOT, 3,421 MCP calls): 8 failed calls,
all GLM 5.3, in 3 trials:
* 6 x {{camel_eval_expression}} "No answer from the integration with pid 50/58
... within 10 seconds" (60 s spent waiting). In one trial the next identical
call worked locally, so the server's status file can also be there for a moment
only.
* 1 x {{camel_control kill}} of the server's pid: {{MCP error -32603: Internal
error}}.
* 1 x {{camel_control stop}} without a name refused because the server counts
as a second integration.
* Worst case: after the internal error the agent ran {{kill -9 50}} in its
shell, which killed camel-jbang-mcp. The trial went on for 73 more tool calls
without any Camel MCP tool.
Not seen with 4.22.1 (no scratch-context evaluation and no {{camel_control}}
there).
h3. Cause
# {{ExpressionEvaluator.evaluateWith}}
({{camel-jbang-core/.../commands/ai/ExpressionEvaluator.java:196}}) starts a
{{new DefaultCamelContext(false)}} inside the MCP server for every local
evaluation. camel-cli-connector is on the server's classpath (dependency of
camel-jbang-core, {{camel-jbang-core/pom.xml:104-107}}), and
{{AbstractCamelContext}} starts a CLI connector for every context whose factory
is enabled ({{core/camel-base-engine/.../AbstractCamelContext.java:2671-2680}};
{{DefaultCliConnectorFactory}} is enabled by default). So each scratch context
writes {{~/.camel/<server pid>-status.json}} while it runs, which makes the
server a "running integration" ({{ProcessFinder}} counts any live pid with a
status file). The context name ({{camel-9}}, {{camel-12}}) is the counter of
contexts this JVM has created.
# The file outlived the context in two of three trials.
{{FileCliConnectorTransport.doStop}} cancels its status task with
{{cancel(false)}} and then deletes the files
({{dsl/camel-cli-connector/.../FileCliConnectorTransport.java:433-451}}), so a
status write already running can recreate the file after the delete, and the
daemon executor of each context writes to the same {{<pid>-*}} files. (Likely
cause, not proven: the files show that the status file stayed, with the context
not in a stopped phase.)
# Nothing keeps the server from selecting itself: {{ProcessFinder.findAll()}}
scans with {{skipThisProcess=false}}
({{camel-jbang-core/.../common/ProcessFinder.java:86-88, 146-160}}), and a pid
is matched even when it is the current one. {{RuntimeHelper.findProcess(null)}}
({{common/RuntimeHelper.java:80-89}}) and
{{ToolContext.selectSingleProcessIfNone()}}
({{commands/ai/ToolContext.java:89}}) use it, which {{camel_eval_expression}}
calls when no name is given ({{commands/ai/AuthoringTools.java:400-404}}).
# {{IntegrationLauncher.control}} "kill" calls
{{ProcessHandle.destroyForcibly()}} on the selected pid
({{commands/ai/IntegrationLauncher.java:575-581}}); for the current process the
JDK throws {{IllegalStateException("destroy of current process not allowed")}},
which is not a {{ToolExecutionException}}, so the client gets -32603.
h3. Fix
* Start the scratch context without the CLI connector: bind a disabled
{{CliConnectorFactory}} (or a no-op one) in the scratch context's registry
before {{context.start()}} in {{ExpressionEvaluator.evaluateWith}}, and the
same for any other short-lived context the tools start inside the server.
* Never treat this JVM as an integration in the tool layer: skip
{{ProcessHandle.current()}} in {{RuntimeHelper.discoverProcesses}} /
{{findProcess}} (or in {{ProcessFinder.findAll}} and the pid match), so a stale
file cannot make the server pick itself.
* {{camel_control}} refuses the server's own pid with a
{{ToolExecutionException}} ("pid N is this MCP server, not an integration"), so
no action on it ends as -32603.
* Optionally, in camel-cli-connector: stop the status task and wait for it
before deleting the files in {{FileCliConnectorTransport.doStop}}, so a stopped
context cannot leave a status file behind.
* Test: evaluate a jq or jsonpath expression locally, then assert
{{camel_runtime_processes}} is empty and {{camel_eval_expression}} without a
name still evaluates locally.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)