You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/10 18:56:25 UTC
(camel) branch main updated: CAMEL-24679: Camel TUI: give the MCP server's tool registry the launcher (#26272)
This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 522d91442b41 CAMEL-24679: Camel TUI: give the MCP server's tool registry the launcher (#26272)
522d91442b41 is described below
commit 522d91442b4131d5457d86f6180166319fec4875
Author: Luigi De Masi <55...@users.noreply.github.com>
AuthorDate: Thu Sep 10 20:56:17 2026 +0200
CAMEL-24679: Camel TUI: give the MCP server's tool registry the launcher (#26272)
The LaunchManager was only ever pushed into the AI panel's TuiToolRegistry, but
TuiMcpServer builds its own registry over the same shared McpFacade. That left
tui_run_example answering "Launching examples is not available in this session"
and tui_infra start/run/restart answering "Error: launching is not available"
for every external MCP client — including the ACP coding agent from CAMEL-24663.
restart was the worst case: it stopped the service and only then failed to start
it again.
Hold the LaunchManager on McpFacade and wire it once in CamelMonitor, so every
registry built over that facade reads the same instance whenever it was built.
AiPanel drops its own field and setter and reads the facade as well, leaving the
launcher with exactly one wiring point. Both registry reads go through a helper
that tolerates a null facade, keeping the graceful "not available" answer.
Co-authored-by: Luigi De Masi <ld...@redhat.com>
Co-authored-by: Claude Opus 5 <no...@anthropic.com>
---
.../modules/ROOT/pages/camel-jbang-tui.adoc | 3 +-
.../camel/dsl/jbang/core/commands/tui/AiPanel.java | 22 ++----
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 2 +-
.../dsl/jbang/core/commands/tui/McpFacade.java | 11 +++
.../jbang/core/commands/tui/TuiToolRegistry.java | 10 +--
.../commands/tui/TuiToolRegistryLaunchTest.java | 91 ++++++++++++++++++++++
6 files changed, 117 insertions(+), 22 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index a1641ee38839..c1726f578883 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -1109,7 +1109,8 @@ The MCP server exposes tools organized by purpose:
list the running infra services (brokers, databases) and read their logs
* *Navigate* -- switch tabs, select integrations, select routes, send keystrokes, apply filters
* *Act* -- send test messages to endpoints, start/stop/restart routes, change log levels,
- start/stop/restart infra services, run any entry of the *F2* actions menu by its label
+ list and launch the bundled examples, start/stop/restart infra services, run any entry of
+ the *F2* actions menu by its label
* *Edit* -- write a source file in the integration's source directory (`tui_write_file`), see below
* *Annotate* -- locate text and diagram nodes by coordinates, draw shapes (boxes, highlights,
arrows, underlines, text labels), show captions with typewriter animation
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index b1603543f97e..72b767051948 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -243,10 +243,6 @@ class AiPanel {
private volatile CompletableFuture<AiCliCommandExecutor.Result> activeCliCommand;
private Runnable exitCallback;
- // Detached launches (/run, /infra run) go through the same LaunchManager the F2 Actions menu uses, so they are
- // spawned as tracked background processes instead of blocking in-process. Null in tests that construct the panel
- // directly; the slash context reports an error in that case.
- private LaunchManager launchManager;
private volatile List<JsonObject> exampleCatalog;
// Provider switch popup
@@ -363,23 +359,18 @@ class AiPanel {
this.ctx = ctx;
}
- void setLaunchManager(LaunchManager launchManager) {
- this.launchManager = launchManager;
- if (toolRegistry != null) {
- toolRegistry.setLaunchManager(launchManager);
- }
+ // Detached launches (/run, /infra run) go through the same LaunchManager the F2 Actions menu uses, so they are
+ // spawned as tracked background processes instead of blocking in-process. It comes from the facade, the one place
+ // the TUI wires it; null in tests that construct the panel directly, where the slash context reports an error.
+ private LaunchManager launchManager() {
+ return mcpFacade != null ? mcpFacade.getLaunchManager() : null;
}
void setMcpFacade(McpFacade mcpFacade) {
this.mcpFacade = mcpFacade;
if (mcpFacade != null) {
mcpFacade.setWriteMode(writeMode);
- }
- if (mcpFacade != null) {
this.toolRegistry = new TuiToolRegistry(mcpFacade);
- if (launchManager != null) {
- toolRegistry.setLaunchManager(launchManager);
- }
}
}
@@ -3739,6 +3730,7 @@ class AiPanel {
@Override
public String launchDetached(AiSlashCommandRegistry.LaunchSpec spec) {
+ LaunchManager launchManager = launchManager();
if (launchManager == null) {
throw new IllegalStateException("Launching commands is not available in this session.");
}
@@ -3772,7 +3764,7 @@ class AiPanel {
*/
private void launchDetachedQuietly(AiSlashCommandRegistry.LaunchSpec spec) {
try {
- launchManager.launchDetached(spec.displayName(), spec.camelArgs());
+ launchManager().launchDetached(spec.displayName(), spec.camelArgs());
} catch (IOException e) {
conversation.add(new ConversationEntry(
AiRole.ERROR, "Failed to start: " + spec.displayName() + " - " + e.getMessage()));
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 7003642b7ac8..ee63146660dc 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -496,7 +496,6 @@ public class CamelMonitor extends CamelCommand {
actionsPopup.setResetStatsAction(this::resetStats);
shellPanel.setContext(ctx);
aiPanel.setContext(ctx);
- aiPanel.setLaunchManager(actionsPopup.getLaunchManager());
actionsPopup.setOpenShellAction(shellPanel::open);
actionsPopup.setOpenAiPromptAction(aiPanel::open);
actionsPopup.setBrowseFilesAction(this::openFilesPopup);
@@ -812,6 +811,7 @@ public class CamelMonitor extends CamelCommand {
}
});
mcpFacade.setSourceValidator(tabRegistry.sourceTab().editAssist()::validateSource);
+ mcpFacade.setLaunchManager(actionsPopup.getLaunchManager());
aiPanel.setMcpFacade(mcpFacade);
aiPanel.setOtelSpans(dataService.otelSpans());
mcpFacade.setAiActivityLog(aiPanel::getActivityLog);
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 65970db6b142..43756094b146 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -172,6 +172,9 @@ class McpFacade {
private volatile StatusFileReader statusFiles = StatusFileReader.defaultReader();
private volatile Supplier<List<TuiMcpServer.LogEntry>> mcpActivityLog;
private volatile Supplier<Integer> mcpToolCallCount;
+ // the F2 menu's launcher: starts examples (tui_run_example) and infra services (tui_infra start). It is held
+ // here, not in the tool registry, so the AI panel's registry and the MCP server's registry both see it.
+ private volatile LaunchManager launchManager;
McpFacade(
MonitorContext ctx,
@@ -211,6 +214,14 @@ class McpFacade {
this.mcpToolCallCount = mcpToolCallCount;
}
+ void setLaunchManager(LaunchManager launchManager) {
+ this.launchManager = launchManager;
+ }
+
+ LaunchManager getLaunchManager() {
+ return launchManager;
+ }
+
List<AiPanel.LogEntry> getAiActivityLog() {
Supplier<List<AiPanel.LogEntry>> s = aiActivityLog;
return s != null ? s.get() : List.of();
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 4b821e16c1fa..34b7450e4655 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -73,15 +73,15 @@ class TuiToolRegistry {
private volatile AnimationState currentAnimation;
private volatile List<ToolDef> cachedTools;
- private volatile LaunchManager launchManager;
private volatile List<JsonObject> exampleCatalog;
TuiToolRegistry(McpFacade facade) {
this.facade = facade;
}
- void setLaunchManager(LaunchManager launchManager) {
- this.launchManager = launchManager;
+ /** The launcher the TUI wired into the facade, or null when there is none to launch with. */
+ private LaunchManager launcher() {
+ return facade != null ? facade.getLaunchManager() : null;
}
/**
@@ -1057,7 +1057,7 @@ class TuiToolRegistry {
}
private String startInfra(String alias) {
- LaunchManager lm = launchManager;
+ LaunchManager lm = launcher();
if (lm == null) {
return "Error: launching is not available";
}
@@ -2329,7 +2329,7 @@ class TuiToolRegistry {
return "{\"error\": \"'name' parameter is required\"}";
}
- LaunchManager lm = launchManager;
+ LaunchManager lm = launcher();
if (lm == null) {
return "{\"error\": \"Launching examples is not available in this session\"}";
}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryLaunchTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryLaunchTest.java
new file mode 100644
index 000000000000..37eaa8aa3847
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryLaunchTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.tui;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The launcher belongs to the facade, not to the tool registry: the AI panel and the MCP server each build their own
+ * TuiToolRegistry over the one shared facade, and only the panel's used to be given a LaunchManager, which left
+ * tui_run_example and tui_infra start dead for every external MCP client (CAMEL-24679).
+ */
+class TuiToolRegistryLaunchTest {
+
+ private static final JsonObject UNKNOWN_EXAMPLE = new JsonObject(Map.of("name", "no-such/example"));
+ private static final JsonObject START_KAFKA = new JsonObject(Map.of("action", "start", "alias", "kafka"));
+
+ /** Records what it was asked to launch, so the wired path can be asserted without spawning anything. */
+ private static final class RecordingLaunchManager extends LaunchManager {
+
+ private String startedInfra;
+
+ RecordingLaunchManager() {
+ super(List::of);
+ }
+
+ @Override
+ void startInfra(String alias) {
+ this.startedInfra = alias;
+ }
+ }
+
+ private static McpFacade bareFacade() {
+ return new McpFacade(null, null, null, null, null, null, null, null, null, null, null, null, null);
+ }
+
+ @Test
+ void launchingToolsSayNothingCanBeLaunchedUntilTheFacadeHasTheLauncher() throws Exception {
+ TuiToolRegistry registry = new TuiToolRegistry(bareFacade());
+
+ assertTrue(registry.execute("tui_run_example", UNKNOWN_EXAMPLE).contains("Launching examples is not available"));
+ assertTrue(registry.execute("tui_infra", START_KAFKA).contains("launching is not available"));
+ }
+
+ @Test
+ void oneLauncherOnTheFacadeReachesEveryRegistryBuiltOverIt() throws Exception {
+ McpFacade facade = bareFacade();
+ // the AI panel builds its registry in setMcpFacade, the MCP server builds its own one in the constructor
+ TuiToolRegistry panelRegistry = new TuiToolRegistry(facade);
+
+ RecordingLaunchManager launcher = new RecordingLaunchManager();
+ facade.setLaunchManager(launcher);
+
+ // built after the wiring, like the MCP server's registry, and never handed the launcher itself
+ TuiToolRegistry mcpServerRegistry = new TuiToolRegistry(facade);
+
+ // the example name is unknown, but the catalog is only reached once there is a launcher
+ assertTrue(panelRegistry.execute("tui_run_example", UNKNOWN_EXAMPLE).contains("Unknown example"));
+ assertTrue(mcpServerRegistry.execute("tui_run_example", UNKNOWN_EXAMPLE).contains("Unknown example"));
+
+ assertTrue(mcpServerRegistry.execute("tui_infra", START_KAFKA).contains("Starting infra service kafka"));
+ assertEquals("kafka", launcher.startedInfra);
+ }
+
+ @Test
+ void aRegistryWithoutAFacadeAnswersInsteadOfFailing() throws Exception {
+ TuiToolRegistry registry = new TuiToolRegistry(null);
+
+ assertTrue(registry.execute("tui_run_example", UNKNOWN_EXAMPLE).contains("Launching examples is not available"));
+ }
+}