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

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


The following commit(s) were added to refs/heads/main by this push:
     new db46ee4e7920 CAMEL-26396: camel-jbang - AI tools: the MCP server never 
takes itself for an integration (#28678)
db46ee4e7920 is described below

commit db46ee4e7920cf433d643de1b719df65b65020c1
Author: Federico Mariani <[email protected]>
AuthorDate: Thu Oct 29 13:37:05 2026 +0100

    CAMEL-26396: camel-jbang - AI tools: the MCP server never takes itself for 
an integration (#28678)
    
    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.
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   3 +-
 .../cli/connector/FileCliConnectorTransport.java   |  28 ++++
 .../FileCliConnectorTransportStopTest.java         | 146 +++++++++++++++++++++
 .../core/commands/ai/ExpressionEvaluator.java      |   4 +-
 .../core/commands/ai/IntegrationLauncher.java      |   4 +
 .../dsl/jbang/core/commands/ai/ToolContext.java    |  24 +++-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |   1 +
 .../jbang/core/commands/version/VersionList.java   |   5 +
 .../camel/dsl/jbang/core/common/RuntimeHelper.java |  28 +++-
 .../jbang/core/common/ScratchContextHelper.java    |  56 ++++++++
 .../commands/ai/IntegrationLauncherGuardTest.java  |  25 +++-
 .../ai/ThisProcessIsNoIntegrationTest.java         | 137 +++++++++++++++++++
 .../core/common/ScratchContextHelperTest.java      |  97 ++++++++++++++
 .../jbang/core/commands/mcp/RuntimeService.java    |   7 +
 .../core/commands/mcp/RuntimeServiceTest.java      |  31 +++++
 15 files changed, 583 insertions(+), 13 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 62b061c5291c..833be18aab8d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -850,7 +850,8 @@ help validate and scaffold that workflow, following the 
rules of the `rest-opena
 NOTE: Runtime tools require a running Camel application started via `camel 
run`. They communicate with the
 application through the file-based IPC protocol in `~/.camel/`. All tools 
accept an optional `nameOrPid`
 parameter; when omitted, the server auto-discovers the running Camel process 
(this works when exactly one
-process is running).
+process is running). The MCP server itself is never one of them: it is not 
listed, not selected when no name is
+given, and a call that names its pid is refused.
 
 ==== Process Discovery
 
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/FileCliConnectorTransport.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/FileCliConnectorTransport.java
index 4195b13a4723..278ec3d11557 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/FileCliConnectorTransport.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/FileCliConnectorTransport.java
@@ -62,6 +62,7 @@ public class FileCliConnectorTransport extends ServiceSupport 
implements CliConn
     private CliSnapshotProducer snapshots;
     private Runnable shutdown;
     private ScheduledExecutorService executor;
+    private volatile Thread pollThread;
     private ScheduledFuture<?> scheduledFuture;
     private int delay = 1000;
     private long counter;
@@ -69,6 +70,8 @@ public class FileCliConnectorTransport extends ServiceSupport 
implements CliConn
 
     // an action file this young that cannot be read may still be written: the 
Camel CLI before 4.24 writes it in place
     private static final long UNREADABLE_GRACE_MS = 2000;
+    // how long a stop waits for the poll in flight before it removes the files
+    private static final long STOP_POLL_TIMEOUT_MS = 5000;
 
     private record PendingAction(File output, boolean replyAlways, 
CompletableFuture<CliActionOutcome> outcome) {
     }
@@ -108,6 +111,7 @@ public class FileCliConnectorTransport extends 
ServiceSupport implements CliConn
             String threadName = ThreadHelper.resolveThreadName(null, 
"LocalCliConnector");
             Thread thread = new Thread(r, threadName);
             thread.setDaemon(true);
+            pollThread = thread;
             return thread;
         });
 
@@ -436,6 +440,7 @@ public class FileCliConnectorTransport extends 
ServiceSupport implements CliConn
         if (scheduledFuture != null) {
             scheduledFuture.cancel(false);
         }
+        awaitPoll();
         synchronized (this) {
             pending.forEach((request, action) -> {
                 action.outcome().cancel(true);
@@ -480,6 +485,29 @@ public class FileCliConnectorTransport extends 
ServiceSupport implements CliConn
         }
     }
 
+    /**
+     * Waits a little for the poll in flight: a status written after the files 
are removed would stay behind, and the
+     * Camel CLI would list this process as a running integration after its 
Camel has stopped (CAMEL-26396). Not when
+     * the poll itself stops the transport, which would wait for itself.
+     */
+    private void awaitPoll() {
+        if (executor == null) {
+            return;
+        }
+        // the poll is cancelled, so the thread ends once the poll in flight 
is done
+        executor.shutdown();
+        if (Thread.currentThread() == pollThread) {
+            return;
+        }
+        try {
+            if (!executor.awaitTermination(STOP_POLL_TIMEOUT_MS, 
TimeUnit.MILLISECONDS)) {
+                LOG.debug("The poll of the Camel CLI connector has not 
finished within {} millis", STOP_POLL_TIMEOUT_MS);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
     private static String getPid() {
         return String.valueOf(ProcessHandle.current().pid());
     }
diff --git 
a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/FileCliConnectorTransportStopTest.java
 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/FileCliConnectorTransportStopTest.java
new file mode 100644
index 000000000000..459bdad2c5aa
--- /dev/null
+++ 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/FileCliConnectorTransportStopTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.cli.connector;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * CAMEL-26396: a stop waits for the status write in flight, so a stopped 
integration leaves no status file behind. The
+ * Camel CLI lists a live process with a status file as a running integration: 
the MCP server, whose scratch contexts
+ * ran with the connector, listed itself and took itself for the integration 
to evaluate in.
+ * <p>
+ * Isolated because it changes {@code user.home}, and every connector in this 
JVM writes files named after the same pid.
+ */
+@Isolated
+class FileCliConnectorTransportStopTest {
+
+    private static final String PID = 
String.valueOf(ProcessHandle.current().pid());
+
+    @TempDir
+    Path home;
+
+    private String oldHome;
+    private DefaultCamelContext context;
+
+    @BeforeEach
+    void useHome() {
+        oldHome = System.getProperty("user.home");
+        System.setProperty("user.home", home.toString());
+        context = new DefaultCamelContext();
+    }
+
+    @AfterEach
+    void restoreHome() {
+        context.stop();
+        System.setProperty("user.home", oldHome);
+    }
+
+    @Test
+    void aStatusWriteInFlightDoesNotOutliveTheStop() throws Exception {
+        SlowSnapshots snapshots = new SlowSnapshots();
+        FileCliConnectorTransport transport = new FileCliConnectorTransport();
+        transport.configure(context, (action, output) -> false, snapshots, () 
-> {
+        });
+        transport.start();
+        File status = home.resolve(".camel").resolve(PID + 
"-status.json").toFile();
+        // the first poll collects the status, which takes until the test lets 
it go on
+        assertThat(snapshots.collecting.await(10, TimeUnit.SECONDS)).isTrue();
+
+        Thread stopper = new Thread(transport::stop, "test-stop");
+        stopper.start();
+        // the stop is done, as it was at once before the fix, or it waits for 
the poll
+        await().atMost(10, TimeUnit.SECONDS).until(() -> switch 
(stopper.getState()) {
+            case TERMINATED, WAITING, TIMED_WAITING -> true;
+            default -> false;
+        });
+        snapshots.collected.countDown();
+
+        stopper.join(10_000);
+        assertThat(stopper.isAlive()).isFalse();
+        // the poll has written the status, before the stop removed the files 
or after
+        snapshots.poll.join(10_000);
+        assertThat(snapshots.poll.isAlive()).isFalse();
+        assertThat(status).doesNotExist();
+    }
+
+    /**
+     * Snapshots whose status takes until the test counts down {@link 
#collected}.
+     */
+    private static final class SlowSnapshots implements CliSnapshotProducer {
+
+        final CountDownLatch collecting = new CountDownLatch(1);
+        final CountDownLatch collected = new CountDownLatch(1);
+        volatile Thread poll;
+
+        @Override
+        public JsonObject status() throws Exception {
+            poll = Thread.currentThread();
+            collecting.countDown();
+            collected.await(10, TimeUnit.SECONDS);
+            JsonObject context = new JsonObject();
+            context.put("name", "demo");
+            context.put("phase", 4);
+            JsonObject root = new JsonObject();
+            root.put("context", context);
+            return root;
+        }
+
+        @Override
+        public JsonObject trace() {
+            return null;
+        }
+
+        @Override
+        public JsonObject debug() {
+            return null;
+        }
+
+        @Override
+        public JsonObject messageHistory() {
+            return null;
+        }
+
+        @Override
+        public JsonObject errors() {
+            return null;
+        }
+
+        @Override
+        public JsonObject receive() {
+            return null;
+        }
+
+        @Override
+        public JsonObject activity() {
+            return null;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
index 3f3d44227959..1bb3c5dfc3ea 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
@@ -24,6 +24,7 @@ import org.apache.camel.Exchange;
 import org.apache.camel.Expression;
 import org.apache.camel.Predicate;
 import org.apache.camel.catalog.LanguageValidationResult;
+import org.apache.camel.dsl.jbang.core.common.ScratchContextHelper;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.main.download.DependencyDownloaderClassLoader;
 import org.apache.camel.main.download.MavenDependencyDownloader;
@@ -193,7 +194,8 @@ public final class ExpressionEvaluator {
      */
     private static boolean evaluateWith(
             ClassLoader loader, String lang, String expression, String body, 
boolean predicate, JsonObject result) {
-        try (DefaultCamelContext context = new DefaultCamelContext(false)) {
+        // without the CLI connector, which would make this process, such as 
the MCP server, a running integration
+        try (DefaultCamelContext context = 
ScratchContextHelper.withoutCliConnector(new DefaultCamelContext(false))) {
             if (loader != null) {
                 // before the start: the language resolver reads the class 
loader as the context comes up
                 context.setApplicationContextClassLoader(loader);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
index 49972c634899..ed6c16f4549e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
@@ -570,6 +570,10 @@ public final class IntegrationLauncher {
     public static String control(ToolContext ctx, String action) {
         ctx.requireProcess();
         long pid = ctx.pid();
+        if (pid == ProcessHandle.current().pid()) {
+            // the tools run in this process: it cannot kill itself (the JDK 
refuses), nor answer an action it waits for
+            throw ToolContext.thisProcess(pid);
+        }
         return switch (action) {
             case "stop" -> ctx.stopApplication();
             case "kill" -> {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
index 6f3f31f52294..d057db674dfb 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
@@ -222,12 +222,13 @@ public class ToolContext {
      * Selects the process a tool call names ({@code name} is an integration 
name or pid), or keeps the current
      * selection when the call names none.
      *
-     * @throws ToolExecutionException when the name matches no running process
+     * @throws ToolExecutionException when the name matches no running 
process, or is the pid of this process
      */
     public void selectProcess(String nameOrPid) {
         if (nameOrPid == null || nameOrPid.isBlank()) {
             return;
         }
+        refuseThisProcess(nameOrPid);
         RuntimeHelper.ProcessInfo p = RuntimeHelper.findProcess(nameOrPid);
         if (p == null) {
             throw new ToolExecutionException(
@@ -237,6 +238,27 @@ public class ToolContext {
         selectProcess(p.pid());
     }
 
+    /**
+     * Refuses the pid of this process, which runs the tools: a status file of 
its own comes from a CamelContext it ran
+     * for itself, never from an integration, and it would wait for itself to 
answer an action (CAMEL-26396).
+     *
+     * @throws ToolExecutionException when the name or pid is the pid of this 
process
+     */
+    static void refuseThisProcess(String nameOrPid) {
+        if (RuntimeHelper.isThisProcess(nameOrPid)) {
+            throw thisProcess(ProcessHandle.current().pid());
+        }
+    }
+
+    /**
+     * The error for a tool call on the pid of this process (see {@link 
#refuseThisProcess(String)}).
+     */
+    static ToolExecutionException thisProcess(long pid) {
+        return new ToolExecutionException(
+                "pid " + pid + " is the process that runs these tools (the MCP 
server, camel ask or the TUI), not a"
+                                          + " Camel integration; 
list_processes shows the integrations");
+    }
+
     public String readStatus(String section) {
         requireProcess();
         return RuntimeHelper.readStatusSection(pid, section);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 44f8b209153f..ea135305d2e7 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -134,6 +134,7 @@ public final class ToolRegistry {
                     if (name == null || name.isBlank()) {
                         throw new ToolExecutionException("name or PID is 
required");
                     }
+                    ToolContext.refuseThisProcess(name);
                     RuntimeHelper.ProcessInfo found = ctx.findProcess(name);
                     if (found == null) {
                         List<RuntimeHelper.ProcessInfo> processes = 
ctx.discoverProcesses();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
index a0e813d1c67f..b1ee64bf3f3d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
@@ -60,10 +60,12 @@ import org.apache.camel.dsl.jbang.core.common.RowSort;
 import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates;
 import org.apache.camel.dsl.jbang.core.common.RuntimeType;
 import org.apache.camel.dsl.jbang.core.common.RuntimeTypeConverter;
+import org.apache.camel.dsl.jbang.core.common.ScratchContextHelper;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.dsl.jbang.core.model.VersionListDTO;
 import org.apache.camel.main.KameletMain;
 import org.apache.camel.main.download.MavenDependencyDownloader;
+import org.apache.camel.spi.CliConnectorFactory;
 import org.apache.camel.tooling.maven.RepositoryResolver;
 import org.apache.camel.tooling.model.ReleaseModel;
 import org.apache.camel.util.StringHelper;
@@ -400,6 +402,9 @@ public class VersionList extends CamelCommand {
                     mavenResolver.fresh());
         }
         KameletMain main = new KameletMain(CAMEL_INSTANCE_TYPE);
+        // only to download: without the CLI connector, which would list this 
process (such as the MCP server, whose
+        // camel_version_list runs this) as a running integration
+        main.bind(CliConnectorFactory.FACTORY, 
ScratchContextHelper.disabledCliConnectorFactory());
         try {
             main.setFresh(mavenResolver.fresh());
             main.setDownload(mavenResolver.download());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
index e224a882f511..e7a9d35dd7e0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
@@ -61,10 +61,13 @@ public final class RuntimeHelper {
     /**
      * Discovers all running Camel processes by scanning {@code ~/.camel/} for 
{@code {pid}-status.json} files and
      * verifying each PID is still alive.
+     * <p>
+     * This process is never one of them: it runs the tools (camel ask, the 
TUI, the MCP server), and a status file of
+     * its own comes from a CamelContext it ran for itself, not from an 
integration (CAMEL-26396).
      */
     public static List<ProcessInfo> discoverProcesses() {
         List<ProcessInfo> result = new ArrayList<>();
-        for (ProcessFinder.CamelProcess p : ProcessFinder.findAll()) {
+        for (ProcessFinder.CamelProcess p : others(ProcessFinder.findAll())) {
             result.add(new ProcessInfo(p.pid(), p.name(), p.contextName()));
         }
         return result;
@@ -73,13 +76,13 @@ public final class RuntimeHelper {
     /**
      * Finds a single Camel process matching the given name or PID. Returns 
{@code null} if no match is found or if
      * multiple processes match. When {@code nameOrPid} is {@code null} and 
exactly one process is running, returns that
-     * process.
+     * process. This process never matches, also not by its pid (see {@link 
#discoverProcesses()}).
      *
      * @param nameOrPid a process name (with optional wildcard), a numeric PID 
string, or {@code null} for auto-detect
      */
     public static ProcessInfo findProcess(String nameOrPid) {
-        List<ProcessFinder.CamelProcess> found = nameOrPid != null && 
!nameOrPid.isBlank()
-                ? ProcessFinder.find(nameOrPid) : ProcessFinder.findAll();
+        List<ProcessFinder.CamelProcess> found = others(nameOrPid != null && 
!nameOrPid.isBlank()
+                ? ProcessFinder.find(nameOrPid) : ProcessFinder.findAll());
         if (found.size() == 1) {
             ProcessFinder.CamelProcess p = found.get(0);
             return new ProcessInfo(p.pid(), p.name(), p.contextName());
@@ -87,6 +90,23 @@ public final class RuntimeHelper {
         return null;
     }
 
+    /**
+     * Whether the name or pid is the pid of this process, which {@link 
#findProcess(String)} never finds.
+     *
+     * @since 4.24
+     */
+    public static boolean isThisProcess(String nameOrPid) {
+        if (nameOrPid == null || !nameOrPid.matches("\\d{1,18}")) {
+            return false;
+        }
+        return Long.parseLong(nameOrPid) == ProcessHandle.current().pid();
+    }
+
+    private static List<ProcessFinder.CamelProcess> 
others(List<ProcessFinder.CamelProcess> processes) {
+        long self = ProcessHandle.current().pid();
+        return processes.stream().filter(p -> p.pid() != self).toList();
+    }
+
     /**
      * Reads the full status JSON for the given process.
      *
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ScratchContextHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ScratchContextHelper.java
new file mode 100644
index 000000000000..4d0ca0831dc2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ScratchContextHelper.java
@@ -0,0 +1,56 @@
+/*
+ * 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.dsl.jbang.core.common;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.cli.connector.DefaultCliConnectorFactory;
+import org.apache.camel.spi.CliConnectorFactory;
+
+/**
+ * The CamelContexts the Camel CLI runs for itself, such as the scratch 
context that evaluates an expression for the AI
+ * tools: they are not integrations, so they run without the CLI connector. 
With it, such a context writes the status
+ * file of this process ({@code ~/.camel/{pid}-status.json}) while it runs, 
and the MCP server, the TUI or camel ask,
+ * which run for a long time, showed up as a running integration (CAMEL-26396).
+ *
+ * @since 4.24
+ */
+public final class ScratchContextHelper {
+
+    private ScratchContextHelper() {
+    }
+
+    /**
+     * The context, set up to start without the CLI connector, whether it is 
built already or not.
+     */
+    public static <T extends CamelContext> T withoutCliConnector(T context) {
+        CliConnectorFactory disabled = disabledCliConnectorFactory();
+        // a context that is not built yet takes the factory from its registry 
as it builds
+        context.getRegistry().bind(CliConnectorFactory.FACTORY, 
CliConnectorFactory.class, disabled);
+        // a built one has its factory already
+        
context.getCamelContextExtension().addContextPlugin(CliConnectorFactory.class, 
disabled);
+        return context;
+    }
+
+    /**
+     * A CLI connector factory that is turned off, to bind in the registry of 
a Camel Main the CLI runs for itself.
+     */
+    public static CliConnectorFactory disabledCliConnectorFactory() {
+        CliConnectorFactory factory = new DefaultCliConnectorFactory();
+        factory.setEnabled(false);
+        return factory;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherGuardTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherGuardTest.java
index ae6049078887..bd7225aabdbe 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherGuardTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherGuardTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
@@ -32,7 +33,8 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * CAMEL-25364: camel_run of a directory an integration already runs from 
returns that one instead of starting a second
- * copy. The running integration is this test's own JVM, made visible by a 
status file that names the directory.
+ * copy. The running integration is a JVM the test starts, made visible by a 
status file that names the directory: not
+ * this JVM, which runs the tools and is never taken for an integration 
(CAMEL-26396).
  */
 @Isolated
 class IntegrationLauncherGuardTest {
@@ -44,6 +46,7 @@ class IntegrationLauncherGuardTest {
     Path project;
 
     private String originalHome;
+    private Process integration;
 
     @BeforeEach
     void runningFromTheProject() throws Exception {
@@ -60,23 +63,33 @@ class IntegrationLauncherGuardTest {
         JsonObject status = new JsonObject();
         status.put("runtime", runtime);
         status.put("context", context);
-        Files.writeString(camelDir.resolve(ProcessHandle.current().pid() + 
"-status.json"), status.toJson());
+        integration = startJvm();
+        Files.writeString(camelDir.resolve(integration.pid() + 
"-status.json"), status.toJson());
     }
 
     @AfterEach
-    void restoreHome() {
+    void restoreHome() throws Exception {
+        integration.destroyForcibly().waitFor();
         CommandLineHelper.useHomeDir(originalHome);
     }
 
+    /** A JVM that waits until it is destroyed. */
+    private Process startJvm() throws IOException {
+        Path main = home.resolve("Wait.java");
+        Files.writeString(main, "class Wait { public static void main(String[] 
args) throws Exception { System.in.read(); } }");
+        String java = Path.of(System.getProperty("java.home"), "bin", 
"java").toString();
+        return new ProcessBuilder(java, main.toString()).start();
+    }
+
     @Test
     void theRunningIntegrationIsReturned() {
         JsonObject result = IntegrationLauncher.run(project, List.of(), null, 
true, List.of());
         assertThat(result.getString("status")).isEqualTo("running");
-        
assertThat(result.getLong("pid")).isEqualTo(ProcessHandle.current().pid());
+        assertThat(result.getLong("pid")).isEqualTo(integration.pid());
         assertThat(result.getString("name")).isEqualTo("demo");
         assertThat(result.getString("log")).isNotBlank();
         assertThat(result.containsKey("command")).isFalse();
-        // this JVM was not started with --source-dir, so it is not said to 
reload an added file
+        // the JVM was not started with --source-dir, so it is not said to 
reload an added file
         assertThat(result.getString("message")).contains("a second copy was 
not started")
                 .contains("started with its own files").doesNotContain("is 
reloaded");
     }
@@ -86,6 +99,6 @@ class IntegrationLauncherGuardTest {
         Path link = Files.createSymbolicLink(home.resolve("project-link"), 
project);
         JsonObject result = IntegrationLauncher.run(link, List.of(), null, 
true, List.of());
         assertThat(result.getString("status")).isEqualTo("running");
-        
assertThat(result.getLong("pid")).isEqualTo(ProcessHandle.current().pid());
+        assertThat(result.getLong("pid")).isEqualTo(integration.pid());
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ThisProcessIsNoIntegrationTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ThisProcessIsNoIntegrationTest.java
new file mode 100644
index 000000000000..8435952dbabb
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ThisProcessIsNoIntegrationTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * CAMEL-26396: the process that runs the tools (the MCP server, camel ask, 
the TUI) is never taken for an integration.
+ * The scratch context of a local evaluation ran with the CLI connector, which 
wrote the status file of this process and
+ * sometimes left it behind: the MCP server then listed itself, evaluated the 
next expression in itself (waiting 10 s
+ * for its own answer), and camel_control kill of its pid failed with an 
internal error.
+ */
+class ThisProcessIsNoIntegrationTest {
+
+    private static final long PID = ProcessHandle.current().pid();
+    private static final String THIS_PROCESS = "pid " + PID + " is the process 
that runs these tools";
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+
+    @BeforeEach
+    void aStatusFileOfThisProcess() throws Exception {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        Path camelDir = CommandLineHelper.getCamelDir();
+        Files.createDirectories(camelDir);
+        // as the scratch context left it: named after the context, which had 
not stopped yet
+        JsonObject context = new JsonObject();
+        context.put("name", "camel-14");
+        context.put("phase", 4);
+        context.put("state", "Starting");
+        JsonObject status = new JsonObject();
+        status.put("context", context);
+        Files.writeString(camelDir.resolve(PID + "-status.json"), 
status.toJson());
+    }
+
+    @AfterEach
+    void restoreHome() {
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void thisProcessIsNotListed() {
+        assertThat(RuntimeHelper.discoverProcesses()).isEmpty();
+        assertThat(RuntimeHelper.findProcess(null)).isNull();
+        assertThat(RuntimeHelper.findProcess(String.valueOf(PID))).isNull();
+        assertThat(ToolRegistry.execute("list_processes", new ToolContext(), 
Map.of()))
+                .asString().startsWith("No running Camel processes found");
+
+        ToolContext ctx = new ToolContext();
+        assertThat(ctx.selectSingleProcessIfNone()).isFalse();
+        assertThat(ctx.hasProcess()).isFalse();
+    }
+
+    @Test
+    void aCallThatNamesThisProcessIsRefused() {
+        ToolContext ctx = new ToolContext();
+        assertThatThrownBy(() -> ctx.selectProcess(String.valueOf(PID)))
+                
.isInstanceOf(ToolExecutionException.class).hasMessageStartingWith(THIS_PROCESS);
+        assertThat(ctx.hasProcess()).isFalse();
+
+        assertThatThrownBy(() -> ToolRegistry.execute("select_process", ctx, 
Map.of("name", String.valueOf(PID))))
+                
.isInstanceOf(ToolExecutionException.class).hasMessageStartingWith(THIS_PROCESS);
+        assertThatThrownBy(() -> ToolRegistry.execute("camel_eval_expression", 
ctx,
+                Map.of("expression", "${body}", "name", String.valueOf(PID))))
+                
.isInstanceOf(ToolExecutionException.class).hasMessageStartingWith(THIS_PROCESS);
+        assertThatThrownBy(() -> ToolRegistry.execute("camel_control", ctx,
+                Map.of("action", "kill", "name", String.valueOf(PID))))
+                
.isInstanceOf(ToolExecutionException.class).hasMessageStartingWith(THIS_PROCESS);
+    }
+
+    @Test
+    void anExpressionWithoutANameIsEvaluatedLocally() {
+        JsonObject result = 
Jsoner.deserialize(String.valueOf(ToolRegistry.execute("camel_eval_expression",
+                new ToolContext(), Map.of("expression", "Hello ${body}", 
"body", "Camel"))), (JsonObject) null);
+        assertThat(result.getString("evaluatedIn")).isEqualTo("a local scratch 
context (no integration selected)");
+        assertThat(result.getString("status")).isEqualTo("ok");
+        assertThat(result.getString("result")).isEqualTo("Hello Camel");
+    }
+
+    @Test
+    void theScratchContextRunsWithoutTheCliConnector(@TempDir Path userHome) {
+        String previousUserHome = System.getProperty("user.home");
+        // where the CLI connector writes the files of this process while a 
context runs
+        System.setProperty("user.home", userHome.toString());
+        try {
+            JsonObject result = ExpressionEvaluator.evaluate(new 
ToolContext(), "simple", "Hello ${body}", "Camel");
+            assertThat(result.getString("status")).isEqualTo("ok");
+        } finally {
+            System.setProperty("user.home", previousUserHome);
+        }
+        assertThat(userHome.resolve(".camel")).doesNotExist();
+    }
+
+    @Test
+    void noActionControlsThisProcess() {
+        ToolContext ctx = new ToolContext();
+        // selected anyway, as a caller that has a pid of its own can
+        ctx.selectProcess(PID);
+        // kill first: the JDK refuses to kill this process, and restart would 
start a copy of it
+        for (String action : List.of("kill", "stop", "restart", "reload", 
"stop-routes", "start-routes", "reset-stats")) {
+            assertThatThrownBy(() -> IntegrationLauncher.control(ctx, 
action)).as(action)
+                    
.isInstanceOf(ToolExecutionException.class).hasMessageStartingWith(THIS_PROCESS);
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ScratchContextHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ScratchContextHelperTest.java
new file mode 100644
index 000000000000..1d5e19485d64
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ScratchContextHelperTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.dsl.jbang.core.common;
+
+import java.nio.file.Path;
+
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.main.KameletMain;
+import org.apache.camel.spi.CliConnector;
+import org.apache.camel.spi.CliConnectorFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26396: the CamelContexts the CLI runs for itself start without the 
CLI connector, which would write the status
+ * file of this process in {@code ~/.camel} (under {@code user.home}, a 
directory of the test here).
+ */
+class ScratchContextHelperTest {
+
+    @TempDir
+    Path userHome;
+
+    private String previousUserHome;
+
+    @BeforeEach
+    void useHome() {
+        previousUserHome = System.getProperty("user.home");
+        System.setProperty("user.home", userHome.toString());
+    }
+
+    @AfterEach
+    void restoreHome() {
+        System.setProperty("user.home", previousUserHome);
+    }
+
+    @Test
+    void theConnectorIsOnTheClasspath() throws Exception {
+        // what the helper turns off: a context the CLI runs is managed by the 
CLI connector by default
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            CliConnectorFactory factory = 
context.getCamelContextExtension().getContextPlugin(CliConnectorFactory.class);
+            assertThat(factory).isNotNull();
+            assertThat(factory.isEnabled()).isTrue();
+        }
+    }
+
+    @Test
+    void aContextThatIsNotBuiltYetStartsWithoutTheConnector() throws Exception 
{
+        try (DefaultCamelContext context = 
ScratchContextHelper.withoutCliConnector(new DefaultCamelContext(false))) {
+            context.start();
+            assertThat(context.hasService(CliConnector.class)).isNull();
+            
assertThat(context.getCamelContextExtension().getContextPlugin(CliConnectorFactory.class).isEnabled()).isFalse();
+        }
+        assertThat(userHome.resolve(".camel")).doesNotExist();
+    }
+
+    @Test
+    void aBuiltContextStartsWithoutTheConnector() throws Exception {
+        try (DefaultCamelContext context = 
ScratchContextHelper.withoutCliConnector(new DefaultCamelContext())) {
+            context.start();
+            assertThat(context.hasService(CliConnector.class)).isNull();
+        }
+        assertThat(userHome.resolve(".camel")).doesNotExist();
+    }
+
+    @Test
+    void aCamelMainStartsWithoutTheConnector() throws Exception {
+        // as camel version list downloads with Camel Main
+        KameletMain main = new KameletMain("camel.jbang");
+        main.bind(CliConnectorFactory.FACTORY, 
ScratchContextHelper.disabledCliConnectorFactory());
+        main.setDownload(false);
+        try {
+            main.start();
+            
assertThat(main.getCamelContext().hasService(CliConnector.class)).isNull();
+        } finally {
+            main.stop();
+        }
+        assertThat(userHome.resolve(".camel")).doesNotExist();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeService.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeService.java
index aeafaeca701c..3adb1c2c2ad3 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeService.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeService.java
@@ -61,6 +61,13 @@ public class RuntimeService {
     }
 
     public ProcessInfo findSingleProcess(String nameOrPid) {
+        if (RuntimeHelper.isThisProcess(nameOrPid)) {
+            // never an integration, see RuntimeHelper.discoverProcesses()
+            throw new ToolCallException(
+                    "pid " + nameOrPid + " is this MCP server (the process 
that runs the tools), not a Camel integration;"
+                                        + " camel_runtime_processes shows the 
integrations",
+                    null);
+        }
         List<RuntimeHelper.ProcessInfo> processes = 
RuntimeHelper.discoverProcesses();
 
         if (processes.isEmpty()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeServiceTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeServiceTest.java
index 3581934ede23..36b0a5e66b22 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeServiceTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeServiceTest.java
@@ -16,10 +16,14 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 
 import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -54,6 +58,33 @@ class RuntimeServiceTest {
                 .hasMessageContaining("Camel process");
     }
 
+    /**
+     * CAMEL-26396: a status file of the server's own pid, as a scratch 
context left it, does not make the server an
+     * integration.
+     */
+    @Test
+    void theServerIsNeverAProcessOfItsOwn(@TempDir Path home) throws Exception 
{
+        Path previous = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        try {
+            long pid = ProcessHandle.current().pid();
+            Files.createDirectories(CommandLineHelper.getCamelDir());
+            Files.writeString(CommandLineHelper.getCamelDir().resolve(pid + 
"-status.json"),
+                    
"{\"context\":{\"name\":\"camel-14\",\"phase\":4,\"state\":\"Starting\"}}");
+            RuntimeService service = new RuntimeService();
+
+            assertThat(service.discoverProcesses()).isEmpty();
+            assertThatThrownBy(() -> service.findSingleProcess(null))
+                    .isInstanceOf(ToolCallException.class)
+                    .hasMessage("No running Camel processes found");
+            assertThatThrownBy(() -> 
service.findSingleProcess(String.valueOf(pid)))
+                    .isInstanceOf(ToolCallException.class)
+                    .hasMessageStartingWith("pid " + pid + " is this MCP 
server");
+        } finally {
+            CommandLineHelper.useHomeDir(previous.toString());
+        }
+    }
+
     @Test
     void readStatusReturnsNullForNonExistentPid() {
         RuntimeService service = new RuntimeService();

Reply via email to