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

gnodet 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 c954f0635c46 Extract RuntimeHelper for shared IPC code between CLI and 
MCP server
c954f0635c46 is described below

commit c954f0635c46fedb37e6ea68d74946c672800866
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri May 22 11:25:20 2026 +0200

    Extract RuntimeHelper for shared IPC code between CLI and MCP server
    
    - Extract RuntimeHelper utility class in camel-jbang-core/common/ with 
shared IPC methods:
      process discovery, status reading, action execution (multi-client 
protocol), graceful stop
    - Refactor MCP RuntimeService to delegate to RuntimeHelper
    - Add Javadoc with @since 4.21 tags
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../camel/dsl/jbang/core/common/RuntimeHelper.java | 247 +++++++++++++++++++++
 .../jbang/core/commands/mcp/RuntimeService.java    | 115 ++++++++++
 2 files changed, 362 insertions(+)

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
new file mode 100644
index 000000000000..94897399442a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
@@ -0,0 +1,247 @@
+/*
+ * 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.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Shared helper for discovering running Camel processes and communicating 
with them via the file-based IPC protocol.
+ * <p>
+ * Camel JBang applications write status snapshots to {@code 
~/.camel/{pid}-status.json}. Actions are requested by
+ * writing to {@code {pid}-action-{requestId}.json} and reading the response 
from {@code {pid}-output-{requestId}.json}.
+ * Each request gets a unique ID so concurrent callers (CLI, MCP server, etc.) 
don't interfere with each other.
+ * <p>
+ * This class is used by both the {@code camel ask} CLI command and the MCP 
server's {@code RuntimeService}.
+ *
+ * @since 4.21
+ */
+public final class RuntimeHelper {
+
+    private static final long ACTION_TIMEOUT_MS = 10_000;
+    private static final long POLL_INTERVAL_MS = 100;
+
+    /**
+     * Information about a discovered running Camel process.
+     *
+     * @param pid         the OS process ID
+     * @param name        the application name (extracted from status or 
process info)
+     * @param contextName the Camel context name, or {@code null} if not 
available
+     *
+     * @since             4.21
+     */
+    public record ProcessInfo(long pid, String name, String contextName) {
+    }
+
+    private RuntimeHelper() {
+    }
+
+    /**
+     * Discovers all running Camel processes by scanning {@code ~/.camel/} for 
{@code {pid}-status.json} files and
+     * verifying each PID is still alive.
+     */
+    public static List<ProcessInfo> discoverProcesses() {
+        List<ProcessInfo> result = new ArrayList<>();
+        Path camelDir = CommandLineHelper.getCamelDir();
+        File dir = camelDir.toFile();
+        if (!dir.isDirectory()) {
+            return result;
+        }
+
+        File[] statusFiles = dir.listFiles((d, name) -> 
name.matches("\\d+-status\\.json"));
+        if (statusFiles == null) {
+            return result;
+        }
+
+        for (File sf : statusFiles) {
+            String fileName = sf.getName();
+            long pid = Long.parseLong(fileName.substring(0, 
fileName.indexOf('-')));
+            if 
(!ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false)) {
+                continue;
+            }
+            try {
+                JsonObject root = readStatusFromFile(sf.toPath());
+                if (root != null) {
+                    String name = ProcessHelper.extractName(root, 
ProcessHandle.of(pid).orElse(null));
+                    String contextName = null;
+                    JsonObject context = (JsonObject) root.get("context");
+                    if (context != null) {
+                        contextName = context.getString("name");
+                    }
+                    result.add(new ProcessInfo(pid, name, contextName));
+                }
+            } catch (Exception e) {
+                // skip
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 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.
+     *
+     * @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<ProcessInfo> processes = discoverProcesses();
+        if (processes.isEmpty()) {
+            return null;
+        }
+
+        if (nameOrPid != null && !nameOrPid.isBlank()) {
+            if (nameOrPid.matches("\\d+")) {
+                long pid = Long.parseLong(nameOrPid);
+                return processes.stream()
+                        .filter(p -> p.pid == pid)
+                        .findFirst()
+                        .orElse(null);
+            }
+            String pattern = nameOrPid.endsWith("*") ? nameOrPid : nameOrPid + 
"*";
+            List<ProcessInfo> matched = processes.stream()
+                    .filter(p -> (p.name != null && 
PatternHelper.matchPattern(FileUtil.onlyName(p.name), pattern))
+                            || (p.contextName != null && 
PatternHelper.matchPattern(p.contextName, pattern)))
+                    .toList();
+            if (matched.size() == 1) {
+                return matched.get(0);
+            }
+            return null;
+        }
+
+        if (processes.size() == 1) {
+            return processes.get(0);
+        }
+        return null;
+    }
+
+    /**
+     * Reads the full status JSON for the given process.
+     *
+     * @return the parsed status object, or {@code null} if the status file 
does not exist or is unreadable
+     */
+    public static JsonObject readStatus(long pid) {
+        Path statusFile = CommandLineHelper.getCamelDir().resolve(pid + 
"-status.json");
+        return readStatusFromFile(statusFile);
+    }
+
+    /**
+     * Reads a specific section from the status JSON.
+     *
+     * @param  section the top-level key to extract (e.g., "context", 
"routes", "health")
+     * @return         the section value as a JSON string, or {@code "{}"} if 
absent
+     */
+    public static String readStatusSection(long pid, String section) {
+        JsonObject root = readStatus(pid);
+        if (root == null) {
+            return "No status available for PID " + pid;
+        }
+        Object value = root.get(section);
+        if (value instanceof JsonObject jo) {
+            return jo.toJson();
+        }
+        if (value != null) {
+            JsonObject wrapper = new JsonObject();
+            wrapper.put(section, value);
+            return wrapper.toJson();
+        }
+        return "{}";
+    }
+
+    /**
+     * Executes an action against a running Camel process using the file-based 
IPC protocol. Writes an action request
+     * file and polls for the output file within a timeout.
+     *
+     * @param  pid       the target process ID
+     * @param  action    the action name (e.g., "route", "top", "source")
+     * @param  configure optional callback to add extra fields to the request 
JSON
+     * @return           the raw response string, or a timeout message if no 
response was received
+     */
+    public static String executeAction(long pid, String action, 
Consumer<JsonObject> configure) {
+        String requestId = UUID.randomUUID().toString().substring(0, 8);
+        Path camelDir = CommandLineHelper.getCamelDir();
+        Path outputFile = camelDir.resolve(pid + "-output-" + requestId + 
".json");
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", action);
+        if (configure != null) {
+            configure.accept(root);
+        }
+
+        Path actionFile = camelDir.resolve(pid + "-action-" + requestId + 
".json");
+        PathUtils.writeTextSafely(root.toJson(), actionFile);
+
+        try {
+            StopWatch watch = new StopWatch();
+            while (watch.taken() < ACTION_TIMEOUT_MS) {
+                try {
+                    Thread.sleep(POLL_INTERVAL_MS);
+                    if (Files.exists(outputFile) && 
outputFile.toFile().length() > 0) {
+                        return Files.readString(outputFile);
+                    }
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    break;
+                } catch (Exception e) {
+                    // retry
+                }
+            }
+            return "Timeout waiting for response from PID " + pid + " for 
action: " + action;
+        } finally {
+            PathUtils.deleteFile(outputFile);
+            PathUtils.deleteFile(actionFile);
+        }
+    }
+
+    /**
+     * Initiates a graceful shutdown of a running Camel application by 
deleting its PID file.
+     */
+    public static String stopApplication(long pid) {
+        Path pidFile = 
CommandLineHelper.getCamelDir().resolve(Long.toString(pid));
+        if (Files.exists(pidFile)) {
+            PathUtils.deleteFile(pidFile);
+            return "Graceful shutdown initiated for PID " + pid
+                   + ". The application will finish processing in-flight 
exchanges and shut down.";
+        } else {
+            return "PID file not found for " + pid + ". The process may 
already be stopping.";
+        }
+    }
+
+    public static JsonObject readStatusFromFile(Path path) {
+        try {
+            if (Files.exists(path) && path.toFile().length() > 0) {
+                String text = Files.readString(path);
+                return (JsonObject) Jsoner.deserialize(text);
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return null;
+    }
+}
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
new file mode 100644
index 000000000000..7d349268a836
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeService.java
@@ -0,0 +1,115 @@
+/*
+ * 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.mcp;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * CDI service for discovering running Camel processes and communicating with 
them via the file-based IPC protocol.
+ * <p>
+ * This is a thin wrapper around {@link RuntimeHelper} that translates null 
returns and error conditions into
+ * {@link ToolCallException} instances suitable for MCP tool responses.
+ *
+ * @since 4.21
+ */
+@ApplicationScoped
+public class RuntimeService {
+
+    /**
+     * Process information exposed to MCP tools.
+     *
+     * @since 4.21
+     */
+    public record ProcessInfo(long pid, String name, String contextName) {
+    }
+
+    public List<ProcessInfo> discoverProcesses() {
+        return RuntimeHelper.discoverProcesses().stream()
+                .map(p -> new ProcessInfo(p.pid(), p.name(), p.contextName()))
+                .toList();
+    }
+
+    public ProcessInfo findSingleProcess(String nameOrPid) {
+        List<RuntimeHelper.ProcessInfo> processes = 
RuntimeHelper.discoverProcesses();
+
+        if (processes.isEmpty()) {
+            throw new ToolCallException("No running Camel processes found", 
null);
+        }
+
+        RuntimeHelper.ProcessInfo found = RuntimeHelper.findProcess(nameOrPid);
+        if (found != null) {
+            return new ProcessInfo(found.pid(), found.name(), 
found.contextName());
+        }
+
+        if (nameOrPid != null && !nameOrPid.isBlank()) {
+            throw new ToolCallException(
+                    "No unique Camel process found matching '" + nameOrPid + 
"': "
+                                        + processes.stream().map(p -> p.name() 
+ " (PID " + p.pid() + ")").toList()
+                                        + ". Specify a more specific name or 
PID.",
+                    null);
+        }
+
+        throw new ToolCallException(
+                "Multiple Camel processes running: "
+                                    + processes.stream().map(p -> p.name() + " 
(PID " + p.pid() + ")").toList()
+                                    + ". Specify nameOrPid to select one.",
+                null);
+    }
+
+    public JsonObject readStatus(long pid) {
+        return RuntimeHelper.readStatus(pid);
+    }
+
+    public JsonObject readStatusSection(long pid, String section) {
+        JsonObject root = RuntimeHelper.readStatus(pid);
+        if (root == null) {
+            throw new ToolCallException("No status available for PID " + pid, 
null);
+        }
+        Object value = root.get(section);
+        if (value instanceof JsonObject jo) {
+            return jo;
+        }
+        if (value != null) {
+            JsonObject wrapper = new JsonObject();
+            wrapper.put(section, value);
+            return wrapper;
+        }
+        return new JsonObject();
+    }
+
+    public JsonObject executeAction(long pid, String action, 
Consumer<JsonObject> configure) {
+        String result = RuntimeHelper.executeAction(pid, action, configure);
+        if (result != null && result.startsWith("Timeout")) {
+            throw new ToolCallException(result, null);
+        }
+        try {
+            return (JsonObject) Jsoner.deserialize(result);
+        } catch (Exception e) {
+            JsonObject wrapper = new JsonObject();
+            wrapper.put("result", result);
+            return wrapper;
+        }
+    }
+}

Reply via email to