Skip to content

CAMEL-26396: camel-jbang - AI tools: the MCP server never takes itself for an integration - #28678

Merged
Croway merged 1 commit into
apache:mainfrom
Croway:CAMEL-26396-mcp-server-not-an-integration
Oct 29, 2026
Merged

Croway merged 1 commit into
apache:mainfrom
Croway:CAMEL-26396-mcp-server-not-an-integration

Conversation

@Croway

@Croway Croway commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Claude Code on behalf of Croway

CAMEL-26396

Problem

A local camel_eval_expression (no integration selected) starts a scratch DefaultCamelContext inside the MCP
server. camel-cli-connector is on the server's classpath, so every scratch context started a CLI connector, which
wrote ~/.camel/<server pid>-status.json while it ran. FileCliConnectorTransport.doStop cancelled its status task
with cancel(false) and deleted the files right away, so a status write already in flight recreated the file after
the delete: the status of a context that had already shut down (phase 4, state Starting, named camel-14 and the
like) stayed behind.

From then on the tool layer took the server for an integration, because RuntimeHelper matches any live pid with a
status file, the current one included:

  • camel_runtime_processes listed the server itself;
  • camel_eval_expression without a name selected "the only running integration", the server, and waited 10 s for an
    answer nobody gives ("No answer from the integration with pid 6 to the action eval within 10 seconds"); this is
    sticky, as no scratch context runs any more that could delete the file;
  • with a real integration next to it, calls without a name were refused as ambiguous;
  • camel_control kill of the server's pid ended as JSON-RPC -32603 (IllegalStateException: destroy of current process not allowed from ProcessHandle.destroyForcibly).

Reproduced in a Linux container (--cpus=1 and --cpus=0.5, 3 of 3 runs): after 11-26 local evaluations the status
file stays behind. On macOS, while a slow local evaluation runs, the server lists itself as camel-3, and a concurrent
evaluation without a name is answered by the scratch context's connector and labelled as evaluated in the running
integration.

Fix

Where Change
ScratchContextHelper (new, camel-jbang-core common) withoutCliConnector(context) binds a disabled CliConnectorFactory in the context's registry (picked up when the context builds) and sets it as the context plugin (for a context built already); disabledCliConnectorFactory() for a Camel Main.
ExpressionEvaluator.evaluateWith The scratch context starts without the CLI connector.
VersionList.downloadReleases The KameletMain it starts only to download (also from the MCP tool camel_version_list, inside the server) runs without the CLI connector.
RuntimeHelper.discoverProcesses / findProcess Never return this process (the CLI, camel ask, the TUI or the MCP server running the tools), also not by its pid. New isThisProcess(nameOrPid). ProcessFinder is unchanged: CLI commands can still match the current process by pid.
ToolContext.selectProcess(String), select_process A name that is this process's pid is refused with a ToolExecutionException: "pid N is the process that runs these tools (the MCP server, camel ask or the TUI), not a Camel integration; list_processes shows the integrations".
IntegrationLauncher.control Refuses this process's pid for every action with a ToolExecutionException, so no action on it ends as -32603 (and restart cannot start a copy of the server).
MCP RuntimeService.findSingleProcess The same for the camel_runtime_* tools: "pid N is this MCP server (the process that runs the tools), not a Camel integration".
FileCliConnectorTransport.doStop (camel-cli-connector) After cancelling the status task, shuts the poll executor down and waits for the poll in flight (at most 5 s) before it deletes the files, so a stopped context cannot leave a status file behind. Not when the stop runs on the poll thread itself. The daemon thread (CAMEL-25230) and the "stop polling first" order are kept.

Audited and left unchanged, as these contexts are never initialised (the CLI connector starts in
AbstractCamelContext.doInit): RouteDslConverter.read, TransformJavaRoutes.dump, SourceHelper (classpath
resolver) and Init (GitHub / Gist resolvers). Checked with a throwaway test: converting YAML to XML and Java, Java
to YAML, and dumping the structure of a Java route create nothing under user.home.

The TUI already skips its own pid in its process list (TuiHelper.findPids) and gives the shared tools only the
integration the user selected. camel ask only targets processes it discovers. Neither needs to select its own JVM.
The one case is an integration that camel ask's cli_exec runs in its own JVM (camel run --max-seconds=N): the call
waits for that run (and interrupts it after 60 s), so no other tool call inspects it meanwhile; a run that ignores
the interrupt and goes on can no longer be selected by its pid.

Testing

New tests, each failing on origin/main (with a no-op ScratchContextHelper for the class that is new):

Test On origin/main
FileCliConnectorTransportStopTest (camel-cli-connector): the status provider blocks the first poll, the transport stops meanwhile, then the poll goes on; the status file must not exist afterwards fails: the poll's write recreates <pid>-status.json after the stop deleted it
ThisProcessIsNoIntegrationTest (camel-jbang-core), with a status file of the test JVM's own pid in a temp camel dir 5 of 5 fail: discoverProcesses() returns the JVM itself; selectProcess("<own pid>") selects it; camel_eval_expression without a name ends with "No answer from the integration with pid N to the action eval within 10 seconds"; a local evaluation creates ~/.camel (the scratch context's connector); control(kill) throws IllegalStateException: destroy of current process not allowed
ScratchContextHelperTest: a context built or not, and a KameletMain, start without a CliConnector and write nothing under user.home 3 of 4 fail with the helper as a no-op (the fourth checks that the connector is on the classpath by default)
RuntimeServiceTest.theServerIsNeverAProcessOfItsOwn (camel-jbang-mcp) fails: the server is listed

IntegrationLauncherGuardTest faked the running integration with the test JVM's own pid; it now starts a small child
JVM (java Wait.java, blocking on stdin) and writes the status file for that pid.

Full module suites, in one reactor (mvnd -P apache-snapshots -T 2 test -pl dsl/camel-cli-connector,dsl/camel-jbang/camel-jbang-core,dsl/camel-jbang/camel-jbang-mcp,dsl/camel-jbang/camel-jbang-plugin-tui):

Module Tests
camel-cli-connector 76, 0 failures
camel-jbang-core 2522 + 244, 0 failures (2 skipped)
camel-jbang-mcp 506, 0 failures
camel-jbang-plugin-tui 2283, 0 failures (1 skipped; MonitorStartupTest.browserOnlyServesUntilTheServerStops flaked once with "Connection refused" and passed on the rerun)

End to end in a Linux container (camel-mcp-repro:21, --cpus=1): a stdio MCP client runs up to 200 local
camel_eval_expression calls and checks for ~/.camel/<server pid>-status.json after each one.

Runner jar Result
origin/main evals run: 17, stale status file: True
this branch, run 1 evals run: 200, stale status file: False
this branch, run 2 evals run: 200, stale status file: False

On this branch the server log shows 200 scratch contexts starting and no "Camel CLI connector enabled" line.

…f for an integration

A local camel_eval_expression starts a scratch CamelContext inside the MCP
server. With camel-cli-connector on the classpath each scratch context started
a CLI connector, which wrote ~/.camel/<server pid>-status.json, and the
connector's stop deleted the file without waiting for a status write in
flight, which could recreate it. The tools then took the server for an
integration: camel_runtime_processes listed it, an evaluation without a name
went to the server and waited 10 s for an answer, a call without a name was
refused as ambiguous next to a real integration, and camel_control kill of
its pid ended as an internal error (the JDK refuses to destroy the current
process).

- The scratch context of ExpressionEvaluator and the Camel Main that camel
  version list (camel_version_list) starts to download run without the CLI
  connector (new ScratchContextHelper).
- RuntimeHelper.discoverProcesses / findProcess never return this process,
  also not by its pid; ProcessFinder is unchanged for the CLI commands.
- A tool call that names this process's pid is refused with a clear error
  (ToolContext.selectProcess, select_process, MCP RuntimeService), and
  IntegrationLauncher.control refuses it for every action.
- FileCliConnectorTransport.doStop waits (at most 5 s) for the poll in
  flight before it deletes the files, so a stopped CamelContext cannot leave
  its status file behind; not when stopped from the poll thread itself.
@Croway
Croway requested review from davsclaus, gnodet and oscerd October 29, 2026 11:04
@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Well-engineered fix for CAMEL-26396.

The two-layer approach is correct: suppressing the CLI connector in scratch contexts prevents the file from being written in the first place, and the awaitPoll() drain in doStop closes the race for the (now-suppressed) connector path as defense in depth. Process discovery and tool routing are all guarded independently — listing, auto-selection, explicit PID selection, and control actions each refuse this process.

Pre-fix test validation: FileCliConnectorTransportStopTest would fail without awaitPoll() (stop returns immediately, the in-flight status write recreates the deleted file); ThisProcessIsNoIntegrationTest would fail without the others() filter. Tests are correctly structured to validate the fix.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@oscerd oscerd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the self-exclusion logic across the connector, the runtime helpers and the launcher. It's a correct and well-layered fix for a real foot-gun: the tools (camel ask, the TUI, the MCP server) run inside a Camel/jbang process that can leave a <pid>-status.json, and without this the AI tools would "discover" their own runner as an integration and try to eval/trace/kill it.

The fix works at two levels, which is the right shape:

  • Don't advertise itself. ScratchContextHelper binds a turned-off CLI connector factory in the registry of the Camel Main the CLI runs for itself, so the tool's own scratch context doesn't publish a status file that looks like an integration — fixing the cause rather than just filtering the symptom.
  • Exclude this pid defensively everywhere it matters. RuntimeHelper.isThisProcess compares against ProcessHandle.current().pid() (bounded \d{1,18} parse), others(...) filters the current pid out of discovery, RuntimeService.findSingleProcess turns a request that names this pid into a clear message ("pid X is this MCP server ... not a Camel integration; camel_runtime_processes shows the integrations"), and IntegrationLauncher refuses an action targeting the current pid up front. That last one matters most for correctness — an action against self would either be refused by the JDK (can't kill yourself) or deadlock, since the tool would be waiting on a response from the very process that's blocked running the tool.

Identifying "self" by the actual OS pid is the robust choice (name matching alone would be fooled by two Camel processes), and the error messages point the model at the right tool instead of just failing. The tests cover the scenarios directly — a self-pid status file not being treated as an integration (theServerIsNeverAProcessOfItsOwn, ThisProcessIsNoIntegration...) and the launcher guard. It's MCP-surface code but this doesn't change what the tools can do — it only stops them acting on their own process — so no trust-boundary impact.

CI was nearly green (one check still running) when I looked — worth confirming it finishes green before merge.

No blockers.

Claude Code on behalf of oscerd

@github-actions

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • docs
  • dsl/camel-cli-connector
  • dsl/camel-jbang/camel-jbang-core
  • dsl/camel-jbang/camel-jbang-mcp

🔁 1 test passed only after a retry on JDK 21 (1 retried attempt)

Recovered flaky tests on JDK 21 (1)
Module Test Failed attempts First failure
dsl/camel-jbang/camel-jbang-plugin-tui MonitorStartupTest.browserOnlyServesUntilTheServerStops 1 Connection refused (connect failed)

ℹ️ These tests did not fail the build. Surefire retried them and they passed.
Retries are enabled project-wide by surefire.rerunFailingTestsCount in parent/pom.xml.


🔬 Scalpel shadow comparison — Scalpel: 7 of 709 tested, 8 compile-only — current: 7 all tested

Maveniverse Scalpel detected 7 affected modules (current approach: 7).

Skip-tests mode would test 7 modules (4 direct + 5 downstream), skip tests for 8 (generated code, meta-modules)

Modules Scalpel would test (7)
  • camel-cli-connector ← dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/FileCliConnectorTransport.java, dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/FileCliConnectorTransportStopTest.java
  • camel-cli-debug ← downstream of org.apache.camel:camel-cli-connector
  • camel-jbang-mcp ← dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeService.java, dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeServiceTest.java
  • camel-jbang-plugin-mcp ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-tui ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-jbang-core
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
Modules with tests skipped (8)
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-launcher
  • coverage

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • dsl/camel-jbang/camel-jbang-core: 2 test(s) disabled on GitHub Actions
  • dsl/camel-jbang/camel-jbang-mcp: 2 test(s) disabled on GitHub Actions

💡 Manual integration tests recommended:

You modified dsl/camel-jbang/camel-jbang-core. The related integration tests in dsl/camel-jbang/camel-jbang-it are excluded from CI. Consider running them manually:

mvn verify -f dsl/camel-jbang/camel-jbang-it -Djbang-it-test
All tested modules (17 modules, 1m 54s total)

Total reactor time: 1m 54s

Module Duration Status
Camel :: JBang :: MCP 37.2s SUCCESS
Camel :: JBang :: Plugin :: Validate 28.2s SUCCESS
Camel :: Docs 16.0s SUCCESS
Camel :: JBang :: Plugin :: Testing 13.0s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 11.2s SUCCESS
Camel :: JBang :: Main 1.4s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.2s SUCCESS
Camel :: Coverage 1.2s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.0s SUCCESS
Camel :: JBang :: Integration tests 1.0s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.9s SUCCESS
Camel :: DSL :: CLI Debug 0.9s SUCCESS
Camel :: Launcher :: Container 0.6s SUCCESS
Camel :: DSL :: CLI Connector n/a
Camel :: JBang :: Core n/a
Camel :: JBang :: Plugin :: TUI n/a
Camel :: Launcher n/a

Top 20 slowest modules:

  • Camel :: JBang :: MCP (37.2s)
  • Camel :: JBang :: Plugin :: Validate (28.2s)
  • Camel :: Docs (16.0s)
  • Camel :: JBang :: Plugin :: Testing (13.0s)
  • Camel :: JBang :: Plugin :: Kubernetes (11.2s)
  • Camel :: JBang :: Main (1.4s)
  • Camel :: JBang :: Plugin :: Generate (1.2s)
  • Camel :: Coverage (1.2s)
  • Camel :: JBang :: Plugin :: Edit (1.0s)
  • Camel :: JBang :: Integration tests (1.0s)
  • Camel :: JBang :: Plugin :: MCP (0.9s)
  • Camel :: DSL :: CLI Debug (0.9s)
  • Camel :: Launcher :: Container (0.6s)

⚙️ View full build and test results

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. The callers of discoverProcesses/findProcess are all AI/MCP code, so camel ps is unaffected. Suggestion: FileCliConnectorTransport.doStop waits up to 5 s for the poll in flight before cancelling pending; cancelling pending first would avoid a 5 s stop if a poll waits on those entries. This conflicts with #28683 in ExpressionEvaluator.evaluateWith; whichever merges second needs a rebase.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

@Croway
Croway merged commit db46ee4 into apache:main Oct 29, 2026
7 checks passed
@Croway Croway added the bug Something isn't working label Oct 29, 2026
@Croway Croway self-assigned this Oct 29, 2026
@Croway Croway added this to the 4.24.0 milestone Oct 29, 2026
@Croway
Croway deleted the CAMEL-26396-mcp-server-not-an-integration branch October 29, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working docs dsl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants