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 82891fc357fa chore: camel-jbang monitor - the MCP client stays
connected while idle (#27567)
82891fc357fa is described below
commit 82891fc357fa502a9a5dfd4732ae4654dee86110
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 8 15:40:21 2026 +0200
chore: camel-jbang monitor - the MCP client stays connected while idle
(#27567)
The footer showed the MCP client as gone (red, "F2 → Setup MCP") a
minute after its last request. An MCP client over HTTP only sends a
request when it uses a tool, so an idle client looked disconnected. A
client that said initialize now stays connected, shown idle, until it
closes its session (an HTTP DELETE, which the server now accepts); a
client that never says initialize is still connected for a minute after
a request.
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-ai-agents.adoc | 4 +-
.../dsl/jbang/core/commands/tui/TuiMcpServer.java | 23 +++++-
.../core/commands/tui/TuiMcpServerClientTest.java | 86 ++++++++++++++++++++++
3 files changed, 111 insertions(+), 2 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
index 284f92771308..af88d7ee0468 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
@@ -42,7 +42,9 @@ camel tui --mcp
This starts an MCP server on `localhost:8123` (configurable with `--mcp-port`).
The server is bound to `127.0.0.1` only -- it never listens on external
interfaces.
-When MCP is active, the TUI footer shows the connection status.
+When MCP is active, the TUI footer shows the connection status: the port and
the name of the connected client,
+with ● while it calls tools and ○ while it is idle (it stays connected while
idle, as an MCP client only sends a
+request when it uses a tool), or ✗ when no client has connected yet or the
client closed its session.
Use *F2* -> _MCP Info_ to see server details and _MCP Log_ to view the tool
call history.
== Status document resources
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
index 8f1ea05e4d5e..82b3e279469d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
@@ -77,6 +77,9 @@ class TuiMcpServer {
private HttpServer server;
private volatile String clientName;
private volatile long lastActivity;
+ // a client said initialize and has not closed its session (an HTTP
DELETE); it stays connected while idle, as an
+ // MCP client over HTTP only sends a request when it uses a tool
+ private volatile boolean sessionOpen;
private volatile long lastToolCallTime;
private final AtomicInteger toolCallCount = new AtomicInteger();
private final List<LogEntry> activityLog = new ArrayList<>();
@@ -138,17 +141,34 @@ class TuiMcpServer {
return toolCallCount.get();
}
+ /**
+ * The client connected to the server: one that said initialize, until it
closes its session, idle or not; or, for a
+ * client that never says initialize, one that sent a request in the last
minute. Null when there is none.
+ */
String getConnectedClient() {
- if (System.currentTimeMillis() - lastActivity < CLIENT_TIMEOUT_MS) {
+ if (sessionOpen || System.currentTimeMillis() - lastActivity <
CLIENT_TIMEOUT_MS) {
return clientName != null ? clientName : "connected";
}
return null;
}
+ /** For tests: as if the last request came that many milliseconds ago. */
+ void idleFor(long millis) {
+ lastActivity = System.currentTimeMillis() - millis;
+ }
+
private void handleMcp(HttpExchange exchange) throws IOException {
try {
lastActivity = System.currentTimeMillis();
String method = exchange.getRequestMethod();
+ if ("DELETE".equals(method)) {
+ // the client closes its session: it is gone
+ sessionOpen = false;
+ clientName = null;
+ lastActivity = 0;
+ exchange.sendResponseHeaders(200, -1);
+ return;
+ }
if (!"POST".equals(method)) {
exchange.sendResponseHeaders(405, -1);
return;
@@ -257,6 +277,7 @@ class TuiMcpServer {
clientName = (String) clientInfo.get("name");
}
}
+ sessionOpen = true;
repeatedCalls.reset();
JsonObject result = new JsonObject();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServerClientTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServerClientTest.java
new file mode 100644
index 000000000000..133a23f62341
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServerClientTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Map;
+
+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 static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The client of the MCP server stays connected while it is idle: an MCP
client over HTTP only sends a request when it
+ * uses a tool, so a minute without one is not a disconnect. It is gone when
it closes its session.
+ */
+class TuiMcpServerClientTest {
+
+ private final HttpClient http = HttpClient.newHttpClient();
+ private TuiMcpServer server;
+
+ @BeforeEach
+ void start() throws Exception {
+ server = new TuiMcpServer(0, null);
+ server.start();
+ }
+
+ @AfterEach
+ void stop() {
+ server.stop();
+ }
+
+ @Test
+ void aClientStaysConnectedWhileIdleAndGoesWhenItClosesItsSession() throws
Exception {
+ assertThat(server.getConnectedClient()).isNull();
+
+ JsonObject params = new JsonObject(Map.of("clientInfo", new
JsonObject(Map.of("name", "claude-code"))));
+
post("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":" +
params.toJson() + "}");
+ assertThat(server.getConnectedClient()).isEqualTo("claude-code");
+
+ // ten minutes without a tool call
+ server.idleFor(10 * 60_000);
+ assertThat(server.getConnectedClient()).isEqualTo("claude-code");
+
+ HttpRequest delete =
HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + server.getPort() +
"/mcp"))
+ .DELETE().build();
+ assertThat(http.send(delete,
HttpResponse.BodyHandlers.discarding()).statusCode()).isEqualTo(200);
+ assertThat(server.getConnectedClient()).isNull();
+ }
+
+ @Test
+ void aClientThatNeverSaysInitializeIsConnectedForAMinute() throws
Exception {
+
post("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}");
+ assertThat(server.getConnectedClient()).isEqualTo("connected");
+
+ server.idleFor(2 * 60_000);
+ assertThat(server.getConnectedClient()).isNull();
+ }
+
+ private void post(String body) throws Exception {
+ HttpRequest post =
HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + server.getPort() +
"/mcp"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+ http.send(post, HttpResponse.BodyHandlers.ofString());
+ }
+}