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());
+    }
+}

Reply via email to