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 87050476ec1c CAMEL-24327: Add MCP session keep-alive and idle TTL 
eviction
87050476ec1c is described below

commit 87050476ec1cf656bbb6018b519973b38699e21c
Author: Omar Atie <[email protected]>
AuthorDate: Thu Aug 6 13:02:50 2026 -0700

    CAMEL-24327: Add MCP session keep-alive and idle TTL eviction
    
    Evict orphaned MCP streamable HTTP sessions on the Vert.x transport using
    periodic keep-alive pings (default 30s, evict after 3 consecutive failures)
    and a configurable idle TTL (default 5 minutes). POST-only sessions without
    an SSE stream are not pinged; idle TTL handles those. Configuration via
    camel.server.mcp-session-keep-alive-interval and
    camel.server.mcp-session-idle-ttl properties (set to 0 to disable).
    
    Closes #25397
    
    Co-Authored-By: Cursor <[email protected]>
---
 .../org/apache/camel/catalog/docs/main.adoc        |   4 +-
 .../org/apache/camel/catalog/docs/mcp-server.adoc  |  13 ++
 .../main/camel-main-configuration-metadata.json    |   2 +
 .../component/mcp/server/McpServerBridge.java      |   8 +-
 .../mcp/server/McpServerConfiguration.java         |  27 +++
 .../component/mcp/server/McpServerConstants.java   |  12 ++
 .../camel/component/mcp/server/McpServerInfo.java  |  34 ++-
 .../camel-mcp-server/src/main/docs/mcp-server.adoc |  13 ++
 .../mcp/server/main/DefaultMcpServerFactory.java   |   2 +
 .../mcp/server/vertx/VertxMcpServerEngine.java     |   8 +-
 .../VertxMcpStreamableServerTransportProvider.java | 230 ++++++++++++++++++---
 .../McpServerSessionEvictionPropertiesTest.java    |  90 ++++++++
 .../server/vertx/VertxMcpSessionEvictionTest.java  | 158 ++++++++++++++
 ...ttpServerConfigurationPropertiesConfigurer.java |  14 ++
 .../camel-main-configuration-metadata.json         |   2 +
 core/camel-main/src/main/docs/main.adoc            |   4 +-
 .../main/HttpServerConfigurationProperties.java    |  44 ++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |   8 +
 18 files changed, 641 insertions(+), 32 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
index e3d33c3295b0..db7bd0eb0a19 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
@@ -195,7 +195,7 @@ The camel.routecontroller supports 12 options, which are 
listed below.
 
 
 === Camel Embedded HTTP Server (only for standalone; not Spring Boot or 
Quarkus) configurations
-The camel.server supports 26 options, which are listed below.
+The camel.server supports 28 options, which are listed below.
 
 [width="100%",cols="2,5,^1,2",options="header"]
 |===
@@ -218,6 +218,8 @@ The camel.server supports 26 options, which are listed 
below.
 | *camel.server.mcpEnabled* | Whether to expose ai-tool routes as MCP tools 
over streamable HTTP. Requires camel-mcp-server on the classpath. By default, 
the MCP server is not enabled. | false | boolean
 | *camel.server.mcpPath* | HTTP path where the MCP endpoint is served. | /mcp 
| String
 | *camel.server.mcpServerName* | MCP server name advertised to clients. 
Defaults to the CamelContext name. |  | String
+| *camel.server.mcpSessionIdleTtl* | Idle TTL in milliseconds for MCP sessions 
on the Vert.x streamable transport. Sessions with no activity for longer than 
this interval are evicted. 0 disables idle eviction. | 300000 | long
+| *camel.server.mcpSessionKeepAliveInterval* | Keep-alive ping interval in 
milliseconds for MCP sessions on the Vert.x streamable transport. Dead sessions 
are evicted after consecutive ping failures. 0 disables keep-alive pings. | 
30000 | long
 | *camel.server.mcpTags* | Comma-separated list of ai-tool tags to expose as 
MCP tools. Only tools registered under one of these tags are exposed; the 
untagged default pool is never exposed. When not set, no tools are exposed. |  
| String
 | *camel.server.mcpToolTimeout* | Per-call MCP tool execution timeout in 
milliseconds. A call exceeding the timeout returns an error result to the MCP 
client; the underlying route keeps running until it completes on its own. | 
20000 | long
 | *camel.server.path* | Context-path to use for embedded HTTP server | / | 
String
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
index ffe21a708a75..fdc8fdef6e3a 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
@@ -169,6 +169,14 @@ JBang (see the xref:main.adoc[camel-main] options) or on
   `/mcp` | engine
 | `camel.server.mcp-server-name` | MCP server name advertised to clients. |
   CamelContext name | engine
+| `camel.server.mcp-session-keep-alive-interval` | Keep-alive ping interval in
+  milliseconds for MCP sessions on the Vert.x streamable transport. Dead 
sessions
+  are evicted after consecutive ping failures. `0` disables keep-alive
+  pings. | `30000` | engine
+| `camel.server.mcp-session-idle-ttl` | Idle TTL in milliseconds for MCP
+  sessions on the Vert.x streamable transport. Sessions with no activity for
+  longer than this interval are evicted. `0` disables idle eviction. |
+  `300000` | engine
 |===
 
 Bridge-owned options are honored identically on every runtime. Engine-owned
@@ -192,6 +200,11 @@ The Vert.x engine implements the MCP streamable HTTP 
transport:
   `Last-Event-ID` replay,
 * session management via the `Mcp-Session-Id` header and `DELETE /mcp` for
   session termination.
+* active session eviction: keep-alive pings (default every 30 seconds) remove
+  sessions whose ping fails repeatedly, and an idle TTL (default 5 minutes)
+  removes sessions with no traffic. Configure with
+  `camel.server.mcp-session-keep-alive-interval` and
+  `camel.server.mcp-session-idle-ttl` (set either to `0` to disable).
 
 Tools appearing or disappearing (routes starting and stopping) emit
 `notifications/tools/list_changed` to connected clients.
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
index 3d8ec1317988..c03dd1557f00 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
@@ -442,6 +442,8 @@
     { "name": "camel.server.mcpEnabled", "required": false, "description": 
"Whether to expose ai-tool routes as MCP tools over streamable HTTP. Requires 
camel-mcp-server on the classpath. By default, the MCP server is not enabled.", 
"sourceType": "org.apache.camel.main.HttpServerConfigurationProperties", 
"type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": 
false },
     { "name": "camel.server.mcpPath", "required": false, "description": "HTTP 
path where the MCP endpoint is served.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "string", 
"javaType": "java.lang.String", "defaultValue": "\/mcp", "secret": false },
     { "name": "camel.server.mcpServerName", "required": false, "description": 
"MCP server name advertised to clients. Defaults to the CamelContext name.", 
"sourceType": "org.apache.camel.main.HttpServerConfigurationProperties", 
"type": "string", "javaType": "java.lang.String", "secret": false },
+    { "name": "camel.server.mcpSessionIdleTtl", "required": false, 
"description": "Idle TTL in milliseconds for MCP sessions on the Vert.x 
streamable transport. Sessions with no activity for longer than this interval 
are evicted. 0 disables idle eviction.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "integer", 
"javaType": "long", "defaultValue": 300000, "secret": false },
+    { "name": "camel.server.mcpSessionKeepAliveInterval", "required": false, 
"description": "Keep-alive ping interval in milliseconds for MCP sessions on 
the Vert.x streamable transport. Dead sessions are evicted after consecutive 
ping failures. 0 disables keep-alive pings.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "integer", 
"javaType": "long", "defaultValue": 30000, "secret": false },
     { "name": "camel.server.mcpTags", "required": false, "description": 
"Comma-separated list of ai-tool tags to expose as MCP tools. Only tools 
registered under one of these tags are exposed; the untagged default pool is 
never exposed. When not set, no tools are exposed.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "string", 
"javaType": "java.lang.String", "secret": false },
     { "name": "camel.server.mcpToolTimeout", "required": false, "description": 
"Per-call MCP tool execution timeout in milliseconds. A call exceeding the 
timeout returns an error result to the MCP client; the underlying route keeps 
running until it completes on its own.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "integer", 
"javaType": "long", "defaultValue": 20000, "secret": false },
     { "name": "camel.server.path", "required": false, "description": 
"Context-path to use for embedded HTTP server", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "string", 
"javaType": "java.lang.String", "defaultValue": "\/", "secret": false },
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
index cb94a38b520a..1c478aed0b1c 100644
--- 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
+++ 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
@@ -117,7 +117,13 @@ public class McpServerBridge extends ServiceSupport 
implements CamelContextAware
         CamelContextAware.trySetCamelContext(engine, camelContext);
 
         String serverName = configuration.getServerName() != null ? 
configuration.getServerName() : camelContext.getName();
-        engine.initialize(new McpServerInfo(serverName, 
camelContext.getVersion(), configuration.getPath()));
+        String version = camelContext.getVersion();
+        if (version == null || version.isBlank()) {
+            version = "1.0";
+        }
+        engine.initialize(new McpServerInfo(
+                serverName, version, configuration.getPath(),
+                configuration.getSessionKeepAliveInterval(), 
configuration.getSessionIdleTtl()));
 
         if (!engine.consumesServingConfiguration()) {
             if 
(!McpServerConstants.DEFAULT_PATH.equals(configuration.getPath())) {
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
index 3439bf7db8de..376b4c37637a 100644
--- 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
+++ 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConfiguration.java
@@ -31,6 +31,8 @@ public class McpServerConfiguration {
     private long toolTimeout = McpServerConstants.DEFAULT_TOOL_TIMEOUT;
     private String path = McpServerConstants.DEFAULT_PATH;
     private String serverName;
+    private long sessionKeepAliveInterval = 
McpServerConstants.DEFAULT_SESSION_KEEP_ALIVE_INTERVAL;
+    private long sessionIdleTtl = McpServerConstants.DEFAULT_SESSION_IDLE_TTL;
 
     /**
      * Comma-separated list of ai-tool tags to expose as MCP tools. Only tools 
registered under one of these tags are
@@ -78,4 +80,29 @@ public class McpServerConfiguration {
     public void setServerName(String serverName) {
         this.serverName = serverName;
     }
+
+    /**
+     * Keep-alive ping interval in milliseconds for the Vert.x streamable 
transport. Dead sessions are evicted after
+     * consecutive ping failures. {@code 0} disables keep-alive pings. 
Engine-owned: ignored by native engines.
+     */
+    public long getSessionKeepAliveInterval() {
+        return sessionKeepAliveInterval;
+    }
+
+    public void setSessionKeepAliveInterval(long sessionKeepAliveInterval) {
+        this.sessionKeepAliveInterval = sessionKeepAliveInterval;
+    }
+
+    /**
+     * Idle TTL in milliseconds for MCP sessions managed by the Vert.x 
streamable transport. Sessions with no activity
+     * for longer than this interval are evicted. {@code 0} disables idle 
eviction. Engine-owned: ignored by native
+     * engines.
+     */
+    public long getSessionIdleTtl() {
+        return sessionIdleTtl;
+    }
+
+    public void setSessionIdleTtl(long sessionIdleTtl) {
+        this.sessionIdleTtl = sessionIdleTtl;
+    }
 }
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConstants.java
 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConstants.java
index 59ffd9b8b717..59750cce6e1c 100644
--- 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConstants.java
+++ 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerConstants.java
@@ -39,6 +39,18 @@ public final class McpServerConstants {
      */
     public static final long DEFAULT_TOOL_TIMEOUT = 20_000;
 
+    /**
+     * Default MCP session keep-alive ping interval in milliseconds for the 
Vert.x streamable transport. {@code 0}
+     * disables keep-alive pings.
+     */
+    public static final long DEFAULT_SESSION_KEEP_ALIVE_INTERVAL = 30_000;
+
+    /**
+     * Default MCP session idle TTL in milliseconds for the Vert.x streamable 
transport. Sessions with no activity for
+     * longer than this interval are evicted. {@code 0} disables idle eviction.
+     */
+    public static final long DEFAULT_SESSION_IDLE_TTL = 300_000;
+
     private McpServerConstants() {
     }
 }
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerInfo.java
 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerInfo.java
index 1893a4789c86..9e13bfb4baa0 100644
--- 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerInfo.java
+++ 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerInfo.java
@@ -22,11 +22,35 @@ package org.apache.camel.component.mcp.server;
  * Engines backed by a native runtime MCP server (Quarkus, Spring Boot) MAY 
ignore the serving hints ({@code path}) —
  * their own runtime configuration decides how the server is exposed.
  *
- * @param serverName the MCP server name advertised to clients (defaults to 
the CamelContext name)
- * @param version    the MCP server version advertised to clients
- * @param path       the HTTP path where the MCP endpoint should be served
+ * @param serverName                 the MCP server name advertised to clients 
(defaults to the CamelContext name)
+ * @param version                    the MCP server version advertised to 
clients
+ * @param path                       the HTTP path where the MCP endpoint 
should be served
+ * @param sessionKeepAliveIntervalMs keep-alive ping interval in milliseconds 
for the Vert.x streamable transport;
+ *                                   {@code 0} disables keep-alive pings
+ * @param sessionIdleTtlMs           idle TTL in milliseconds; sessions with 
no activity for longer are evicted;
+ *                                   {@code 0} disables idle eviction
  *
- * @since            4.22
+ * @since                            4.22
  */
-public record McpServerInfo(String serverName, String version, String path) {
+public record McpServerInfo(
+        String serverName,
+        String version,
+        String path,
+        long sessionKeepAliveIntervalMs,
+        long sessionIdleTtlMs) {
+
+    public McpServerInfo(String serverName, String version, String path) {
+        this(serverName, version, path,
+             McpServerConstants.DEFAULT_SESSION_KEEP_ALIVE_INTERVAL,
+             McpServerConstants.DEFAULT_SESSION_IDLE_TTL);
+    }
+
+    public McpServerInfo {
+        if (sessionKeepAliveIntervalMs < 0) {
+            throw new IllegalArgumentException("sessionKeepAliveIntervalMs 
must be >= 0");
+        }
+        if (sessionIdleTtlMs < 0) {
+            throw new IllegalArgumentException("sessionIdleTtlMs must be >= 
0");
+        }
+    }
 }
diff --git a/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc 
b/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
index ffe21a708a75..fdc8fdef6e3a 100644
--- a/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
+++ b/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
@@ -169,6 +169,14 @@ JBang (see the xref:main.adoc[camel-main] options) or on
   `/mcp` | engine
 | `camel.server.mcp-server-name` | MCP server name advertised to clients. |
   CamelContext name | engine
+| `camel.server.mcp-session-keep-alive-interval` | Keep-alive ping interval in
+  milliseconds for MCP sessions on the Vert.x streamable transport. Dead 
sessions
+  are evicted after consecutive ping failures. `0` disables keep-alive
+  pings. | `30000` | engine
+| `camel.server.mcp-session-idle-ttl` | Idle TTL in milliseconds for MCP
+  sessions on the Vert.x streamable transport. Sessions with no activity for
+  longer than this interval are evicted. `0` disables idle eviction. |
+  `300000` | engine
 |===
 
 Bridge-owned options are honored identically on every runtime. Engine-owned
@@ -192,6 +200,11 @@ The Vert.x engine implements the MCP streamable HTTP 
transport:
   `Last-Event-ID` replay,
 * session management via the `Mcp-Session-Id` header and `DELETE /mcp` for
   session termination.
+* active session eviction: keep-alive pings (default every 30 seconds) remove
+  sessions whose ping fails repeatedly, and an idle TTL (default 5 minutes)
+  removes sessions with no traffic. Configure with
+  `camel.server.mcp-session-keep-alive-interval` and
+  `camel.server.mcp-session-idle-ttl` (set either to `0` to disable).
 
 Tools appearing or disappearing (routes starting and stopping) emit
 `notifications/tools/list_changed` to connected clients.
diff --git 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/main/DefaultMcpServerFactory.java
 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/main/DefaultMcpServerFactory.java
index d524fa46fa20..e080fdf2c559 100644
--- 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/main/DefaultMcpServerFactory.java
+++ 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/main/DefaultMcpServerFactory.java
@@ -39,6 +39,8 @@ public class DefaultMcpServerFactory implements 
McpServerFactory {
         mcpConfiguration.setToolTimeout(configuration.getMcpToolTimeout());
         mcpConfiguration.setPath(configuration.getMcpPath());
         mcpConfiguration.setServerName(configuration.getMcpServerName());
+        
mcpConfiguration.setSessionKeepAliveInterval(configuration.getMcpSessionKeepAliveInterval());
+        
mcpConfiguration.setSessionIdleTtl(configuration.getMcpSessionIdleTtl());
         return new McpServerBridge(mcpConfiguration);
     }
 }
diff --git 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
index a8381639dbd3..a0435525722c 100644
--- 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
+++ 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
@@ -104,7 +104,9 @@ public class VertxMcpServerEngine extends ServiceSupport 
implements McpServerEng
     protected void doStart() throws Exception {
         VertxPlatformHttpRouter router = lookupRouter();
         jsonMapper = McpJsonDefaults.getMapper();
-        transport = new VertxMcpStreamableServerTransportProvider(jsonMapper, 
info.path());
+        transport = new VertxMcpStreamableServerTransportProvider(
+                jsonMapper, info.path(),
+                info.sessionKeepAliveIntervalMs(), info.sessionIdleTtlMs());
         server = McpServer.sync(transport)
                 .serverInfo(info.serverName(), info.version())
                 
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
@@ -167,6 +169,10 @@ public class VertxMcpServerEngine extends ServiceSupport 
implements McpServerEng
         }
     }
 
+    int sessionCount() {
+        return transport != null ? transport.sessionCount() : 0;
+    }
+
     private McpSchema.Tool buildMcpTool(McpServerTool tool) {
         String schema = tool.inputSchemaJson() != null ? 
tool.inputSchemaJson() : EMPTY_OBJECT_SCHEMA;
         McpSchema.Tool.Builder builder = McpSchema.Tool.builder(tool.name(), 
jsonMapper, schema)
diff --git 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
index 12c8e5ed90a2..7ef1cf0a2ffe 100644
--- 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
+++ 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
@@ -20,6 +20,8 @@ import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import io.modelcontextprotocol.json.McpJsonMapper;
 import io.modelcontextprotocol.json.TypeRef;
@@ -59,6 +61,8 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
 
     private static final Duration NOTIFICATION_TIMEOUT = Duration.ofSeconds(5);
     private static final Duration INITIALIZATION_TIMEOUT = 
Duration.ofSeconds(30);
+    private static final Duration KEEP_ALIVE_PING_TIMEOUT = 
Duration.ofSeconds(5);
+    private static final int MAX_CONSECUTIVE_PING_FAILURES = 3;
 
     private static final String ACCEPT = "Accept";
     private static final String APPLICATION_JSON = "application/json";
@@ -66,15 +70,30 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
 
     private final McpJsonMapper jsonMapper;
     private final String path;
-    private final ConcurrentHashMap<String, McpStreamableServerSession> 
sessions = new ConcurrentHashMap<>();
+    private final long sessionKeepAliveIntervalMs;
+    private final long sessionIdleTtlMs;
+    private final ConcurrentHashMap<String, ManagedSession> sessions = new 
ConcurrentHashMap<>();
     private final List<Route> routes = new ArrayList<>();
 
     private McpStreamableServerSession.Factory sessionFactory;
     private volatile boolean closing;
+    private Vertx vertx;
+    private Long keepAliveTimerId;
+    private Long idleTimerId;
+    private final AtomicBoolean keepAliveInProgress = new AtomicBoolean();
+    private final AtomicBoolean idleEvictionInProgress = new AtomicBoolean();
 
     public VertxMcpStreamableServerTransportProvider(McpJsonMapper jsonMapper, 
String path) {
+        this(jsonMapper, path, 0, 0);
+    }
+
+    public VertxMcpStreamableServerTransportProvider(
+                                                     McpJsonMapper jsonMapper, 
String path, long sessionKeepAliveIntervalMs,
+                                                     long sessionIdleTtlMs) {
         this.jsonMapper = jsonMapper;
         this.path = path;
+        this.sessionKeepAliveIntervalMs = sessionKeepAliveIntervalMs;
+        this.sessionIdleTtlMs = sessionIdleTtlMs;
     }
 
     @Override
@@ -87,12 +106,12 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
         if (sessions.isEmpty()) {
             return Mono.empty();
         }
-        return Mono.fromRunnable(() -> sessions.values().forEach(session -> {
+        return Mono.fromRunnable(() -> sessions.values().forEach(managed -> {
             try {
                 // bounded so a single stalled session cannot starve 
notifications to healthy sessions
-                session.sendNotification(method, 
params).block(NOTIFICATION_TIMEOUT);
+                managed.session.sendNotification(method, 
params).block(NOTIFICATION_TIMEOUT);
             } catch (Exception e) {
-                LOG.debug("Failed to send notification to MCP session {}: {}", 
session.getId(), e.getMessage());
+                LOG.debug("Failed to send notification to MCP session {}: {}", 
managed.session.getId(), e.getMessage());
             }
         }));
     }
@@ -101,11 +120,12 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
     public Mono<Void> closeGracefully() {
         return Mono.fromRunnable(() -> {
             closing = true;
-            sessions.values().forEach(session -> {
+            stopSessionMaintenance();
+            sessions.values().forEach(managed -> {
                 try {
-                    session.closeGracefully().block(NOTIFICATION_TIMEOUT);
+                    
managed.session.closeGracefully().block(NOTIFICATION_TIMEOUT);
                 } catch (Exception e) {
-                    LOG.debug("Failed to close MCP session {}: {}", 
session.getId(), e.getMessage());
+                    LOG.debug("Failed to close MCP session {}: {}", 
managed.session.getId(), e.getMessage());
                 }
             });
             sessions.clear();
@@ -117,7 +137,7 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
      * (the server sets the session factory on construction).
      */
     public void registerRoutes(VertxPlatformHttpRouter router) {
-        Vertx vertx = router.vertx();
+        vertx = router.vertx();
         Route post = router.route(path).method(HttpMethod.POST);
         post.handler(BodyHandler.create(false));
         post.handler(ctx -> dispatch(vertx, ctx, this::handlePost));
@@ -128,13 +148,19 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
         Route delete = router.route(path).method(HttpMethod.DELETE);
         delete.handler(ctx -> dispatch(vertx, ctx, this::handleDelete));
         routes.add(delete);
+        startSessionMaintenance();
     }
 
     public void unregisterRoutes() {
+        stopSessionMaintenance();
         routes.forEach(Route::remove);
         routes.clear();
     }
 
+    int sessionCount() {
+        return sessions.size();
+    }
+
     @FunctionalInterface
     private interface BlockingRequestHandler {
         void handle(RoutingContext ctx, Context connection) throws Exception;
@@ -200,23 +226,24 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
         if (respondBadRequest(connection, ctx, badRequestErrors)) {
             return;
         }
-        McpStreamableServerSession session = sessions.get(sessionId);
-        if (session == null) {
+        ManagedSession managed = sessions.get(sessionId);
+        if (managed == null) {
             respondError(connection, ctx, 404, 
McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
                     .message("Session not found: " + sessionId).build());
             return;
         }
+        managed.touch();
 
         if (message instanceof McpSchema.JSONRPCResponse response) {
-            session.accept(response).block();
+            managed.session.accept(response).block();
             endWithStatus(connection, ctx, 202);
         } else if (message instanceof McpSchema.JSONRPCNotification 
notification) {
-            session.accept(notification).block();
+            managed.session.accept(notification).block();
             endWithStatus(connection, ctx, 202);
         } else if (message instanceof McpSchema.JSONRPCRequest request) {
             VertxMcpSessionTransport transport = startSseResponse(ctx, 
connection, sessionId);
             try {
-                session.responseStream(request, transport).block();
+                managed.session.responseStream(request, transport).block();
             } catch (Exception e) {
                 LOG.warn("Failed to handle MCP request stream: {}", 
e.getMessage());
                 transport.close();
@@ -233,7 +260,7 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
                 = jsonMapper.convertValue(request.params(), new 
TypeRef<McpSchema.InitializeRequest>() {
                 });
         McpStreamableServerSession.McpStreamableServerSessionInit init = 
sessionFactory.startSession(initializeRequest);
-        sessions.put(init.session().getId(), init.session());
+        sessions.put(init.session().getId(), new 
ManagedSession(init.session()));
         McpSchema.InitializeResult initResult = 
init.initResult().block(INITIALIZATION_TIMEOUT);
         String json = 
jsonMapper.writeValueAsString(McpSchema.JSONRPCResponse.result(request.id(), 
initResult));
         connection.runOnContext(v -> ctx.response()
@@ -260,24 +287,25 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
         if (respondBadRequest(connection, ctx, badRequestErrors)) {
             return;
         }
-        McpStreamableServerSession session = sessions.get(sessionId);
-        if (session == null) {
+        ManagedSession managed = sessions.get(sessionId);
+        if (managed == null) {
             endWithStatus(connection, ctx, 404);
             return;
         }
+        managed.touch();
 
         VertxMcpSessionTransport transport = startSseResponse(ctx, connection, 
sessionId);
         String lastEventId = 
ctx.request().getHeader(HttpHeaders.LAST_EVENT_ID);
         if (lastEventId != null) {
             try {
-                session.replay(lastEventId).toIterable().forEach(message -> 
transport.sendMessage(message).block());
+                
managed.session.replay(lastEventId).toIterable().forEach(message -> 
transport.sendMessage(message).block());
             } catch (Exception e) {
                 LOG.warn("Failed to replay MCP messages: {}", e.getMessage());
                 transport.close();
             }
         } else {
             McpStreamableServerSession.McpStreamableServerSessionStream 
listeningStream
-                    = session.listeningStream(transport);
+                    = managed.session.listeningStream(transport);
             connection.runOnContext(v -> ctx.response().closeHandler(x -> 
listeningStream.close()));
         }
     }
@@ -293,16 +321,141 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
                     .message("Session ID required in " + 
HttpHeaders.MCP_SESSION_ID + " header").build());
             return;
         }
-        McpStreamableServerSession session = sessions.get(sessionId);
-        if (session == null) {
+        ManagedSession managed = sessions.get(sessionId);
+        if (managed == null) {
             endWithStatus(connection, ctx, 404);
             return;
         }
-        session.delete().block();
+        managed.session.delete().block();
         sessions.remove(sessionId);
         endWithStatus(connection, ctx, 200);
     }
 
+    private void startSessionMaintenance() {
+        if (vertx == null) {
+            return;
+        }
+        if (sessionKeepAliveIntervalMs > 0) {
+            keepAliveTimerId = vertx.setPeriodic(sessionKeepAliveIntervalMs, 
id -> {
+                if (!keepAliveInProgress.compareAndSet(false, true)) {
+                    return;
+                }
+                vertx.executeBlocking(() -> {
+                    try {
+                        pingSessions();
+                    } finally {
+                        keepAliveInProgress.set(false);
+                    }
+                    return null;
+                }, false);
+            });
+        }
+        if (sessionIdleTtlMs > 0) {
+            long scanInterval = Math.max(100L, Math.min(sessionIdleTtlMs / 2, 
1_000L));
+            idleTimerId = vertx.setPeriodic(scanInterval, id -> {
+                if (!idleEvictionInProgress.compareAndSet(false, true)) {
+                    return;
+                }
+                vertx.executeBlocking(() -> {
+                    try {
+                        evictIdleSessions();
+                    } finally {
+                        idleEvictionInProgress.set(false);
+                    }
+                    return null;
+                }, false);
+            });
+        }
+    }
+
+    private void stopSessionMaintenance() {
+        if (vertx == null) {
+            return;
+        }
+        if (keepAliveTimerId != null) {
+            vertx.cancelTimer(keepAliveTimerId);
+            keepAliveTimerId = null;
+        }
+        if (idleTimerId != null) {
+            vertx.cancelTimer(idleTimerId);
+            idleTimerId = null;
+        }
+    }
+
+    private void pingSessions() {
+        if (closing || sessions.isEmpty()) {
+            return;
+        }
+        for (ManagedSession managed : sessions.values()) {
+            try {
+                managed.session.sendRequest(McpSchema.METHOD_PING, null, new 
TypeRef<Object>() {
+                }).block(KEEP_ALIVE_PING_TIMEOUT);
+                managed.resetPingFailures();
+                managed.touch();
+            } catch (Exception e) {
+                if (isMissingTransport(e)) {
+                    // POST-only clients without an open GET stream cannot 
receive pings; idle TTL handles orphans
+                    continue;
+                }
+                if (managed.recordPingFailure() >= 
MAX_CONSECUTIVE_PING_FAILURES) {
+                    evictSession(managed.session.getId(), "keep-alive ping 
failed");
+                }
+            }
+        }
+    }
+
+    private static boolean isMissingTransport(Throwable error) {
+        Throwable current = error;
+        while (current != null) {
+            if (current instanceof IllegalStateException && 
current.getMessage() != null
+                    && current.getMessage().contains("Stream unavailable")) {
+                return true;
+            }
+            current = current.getCause();
+        }
+        return false;
+    }
+
+    private void evictIdleSessions() {
+        if (closing || sessions.isEmpty()) {
+            return;
+        }
+        long cutoff = System.nanoTime() - 
Duration.ofMillis(sessionIdleTtlMs).toNanos();
+        sessions.forEach((sessionId, managed) -> {
+            if (managed.lastActivityNanos() < cutoff) {
+                evictSession(sessionId, "idle TTL exceeded");
+            }
+        });
+    }
+
+    private void evictSession(String sessionId, String reason) {
+        ManagedSession managed = sessions.remove(sessionId);
+        if (managed == null) {
+            return;
+        }
+        LOG.debug("Evicting MCP session {}: {}", sessionId, reason);
+        closeSessionQuietly(managed.session, sessionId);
+    }
+
+    private void scheduleAsyncSessionClose(String sessionId, 
McpStreamableServerSession session) {
+        if (vertx == null) {
+            closeSessionQuietly(session, sessionId);
+            return;
+        }
+        vertx.executeBlocking(() -> {
+            closeSessionQuietly(session, sessionId);
+            return null;
+        }, false);
+    }
+
+    private void closeSessionQuietly(McpStreamableServerSession session, 
String sessionId) {
+        try {
+            session.closeGracefully().block(NOTIFICATION_TIMEOUT);
+        } catch (Exception e) {
+            LOG.debug("Failed to close MCP session {}: {}", sessionId, 
e.getMessage());
+        }
+    }
+
     private VertxMcpSessionTransport startSseResponse(RoutingContext ctx, 
Context connection, String sessionId) {
         connection.runOnContext(v -> ctx.response()
                 .setChunked(true)
@@ -338,6 +491,35 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
         connection.runOnContext(v -> 
ctx.response().setStatusCode(status).end());
     }
 
+    private static final class ManagedSession {
+
+        private final McpStreamableServerSession session;
+        private volatile long lastActivityNanos;
+        private final AtomicInteger consecutivePingFailures = new 
AtomicInteger();
+
+        private ManagedSession(McpStreamableServerSession session) {
+            this.session = session;
+            touch();
+        }
+
+        private void touch() {
+            lastActivityNanos = System.nanoTime();
+            consecutivePingFailures.set(0);
+        }
+
+        private long lastActivityNanos() {
+            return lastActivityNanos;
+        }
+
+        private void resetPingFailures() {
+            consecutivePingFailures.set(0);
+        }
+
+        private int recordPingFailure() {
+            return consecutivePingFailures.incrementAndGet();
+        }
+    }
+
     /**
      * Per-connection transport writing SSE frames on the connection's 
event-loop context. The SDK session awaits each
      * write, providing natural backpressure.
@@ -377,8 +559,10 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
                             LOG.debug("Failed to write to MCP session {}: {}", 
sessionId,
                                     result.cause() != null ? 
result.cause().getMessage() : "unknown");
                             closed = true;
-                            // the client is gone: drop the session like the 
SDK servlet transport does
-                            sessions.remove(sessionId);
+                            ManagedSession managed = 
sessions.remove(sessionId);
+                            if (managed != null) {
+                                scheduleAsyncSessionClose(sessionId, 
managed.session);
+                            }
                         }
                         sink.success();
                     });
diff --git 
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/McpServerSessionEvictionPropertiesTest.java
 
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/McpServerSessionEvictionPropertiesTest.java
new file mode 100644
index 000000000000..0eb27a4e10bf
--- /dev/null
+++ 
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/McpServerSessionEvictionPropertiesTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.component.mcp.server.vertx;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mcp.server.McpServerBridge;
+import org.apache.camel.main.Main;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * Verifies MCP session idle TTL wiring from {@code 
camel.server.mcp-session-*} properties (CAMEL-24327).
+ */
+class McpServerSessionEvictionPropertiesTest {
+
+    private static final HttpClient HTTP = HttpClient.newHttpClient();
+
+    private static final String INITIALIZE_REQUEST = """
+            
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05",
+            "capabilities":{},"clientInfo":{"name":"test","version":"1"}}}""";
+
+    @Test
+    void testSessionIdleTtlPropertyEvictsOrphanSessions() throws Exception {
+        int port = AvailablePortFinder.getNextAvailable();
+
+        Main main = new Main();
+        main.configure().addRoutesBuilder(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("ai-tool:ping?tags=crm&description=Ping tool")
+                        .setBody(constant("pong"));
+            }
+        });
+        main.addInitialProperty("camel.server.enabled", "true");
+        main.addInitialProperty("camel.server.port", String.valueOf(port));
+        main.addInitialProperty("camel.server.mcp-enabled", "true");
+        main.addInitialProperty("camel.server.mcp-tags", "crm");
+        
main.addInitialProperty("camel.server.mcp-session-keep-alive-interval", "0");
+        main.addInitialProperty("camel.server.mcp-session-idle-ttl", "500");
+        main.start();
+
+        try {
+            initializeSession(port);
+            McpServerBridge bridge = 
main.getCamelContext().hasService(McpServerBridge.class);
+            assertThat(bridge).isNotNull();
+            
assertThat(bridge.getEngine()).isInstanceOf(VertxMcpServerEngine.class);
+            VertxMcpServerEngine engine = (VertxMcpServerEngine) 
bridge.getEngine();
+            assertThat(engine.sessionCount()).isEqualTo(1);
+
+            await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> 
assertThat(engine.sessionCount()).isZero());
+        } finally {
+            main.stop();
+        }
+    }
+
+    private static void initializeSession(int port) throws Exception {
+        HttpResponse<String> response = HTTP.send(HttpRequest.newBuilder()
+                .uri(URI.create("http://localhost:"; + port + "/mcp"))
+                .timeout(Duration.ofSeconds(10))
+                .header("Content-Type", "application/json")
+                .header("Accept", "application/json, text/event-stream")
+                .POST(HttpRequest.BodyPublishers.ofString(INITIALIZE_REQUEST))
+                .build(), HttpResponse.BodyHandlers.ofString());
+        assertThat(response.statusCode()).isEqualTo(200);
+    }
+}
diff --git 
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpSessionEvictionTest.java
 
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpSessionEvictionTest.java
new file mode 100644
index 000000000000..d8232a6db1a7
--- /dev/null
+++ 
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpSessionEvictionTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.component.mcp.server.vertx;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.mcp.server.McpServerInfo;
+import org.apache.camel.component.platform.http.main.MainHttpServer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * Verifies active MCP session eviction on the Vert.x streamable transport 
(CAMEL-24327).
+ */
+class VertxMcpSessionEvictionTest {
+
+    private static final HttpClient HTTP = HttpClient.newHttpClient();
+
+    private static final String INITIALIZE_REQUEST = """
+            
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05",
+            "capabilities":{},"clientInfo":{"name":"test","version":"1"}}}""";
+
+    private static final String INITIALIZED_NOTIFICATION = """
+            
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}""";
+
+    @Test
+    void evictsIdleSessionWithoutDelete() throws Exception {
+        int port = AvailablePortFinder.getNextAvailable();
+        VertxMcpServerEngine engine = startEngine(port, new 
McpServerInfo("eviction", "1.0", "/mcp", 0, 500));
+        try {
+            initializeSession(port);
+            assertThat(engine.sessionCount()).isEqualTo(1);
+
+            await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> 
assertThat(engine.sessionCount()).isZero());
+        } finally {
+            engine.stop();
+        }
+    }
+
+    @Test
+    void postOnlySessionSurvivesKeepAliveUntilIdle() throws Exception {
+        int port = AvailablePortFinder.getNextAvailable();
+        VertxMcpServerEngine engine = startEngine(port, new 
McpServerInfo("eviction", "1.0", "/mcp", 300, 10_000));
+        try {
+            String sessionId = initializeSession(port);
+
+            await().during(2, TimeUnit.SECONDS).atMost(3, 
TimeUnit.SECONDS).untilAsserted(() -> {
+                assertThat(postWithSession(port, sessionId, 
INITIALIZED_NOTIFICATION)).isEqualTo(202);
+                assertThat(engine.sessionCount()).isEqualTo(1);
+            });
+        } finally {
+            engine.stop();
+        }
+    }
+
+    @Test
+    void activeSessionSurvivesIdleEviction() throws Exception {
+        int port = AvailablePortFinder.getNextAvailable();
+        VertxMcpServerEngine engine = startEngine(port, new 
McpServerInfo("eviction", "1.0", "/mcp", 0, 2_000));
+        try {
+            String sessionId = initializeSession(port);
+
+            await().during(2, TimeUnit.SECONDS).atMost(3, 
TimeUnit.SECONDS).untilAsserted(() -> {
+                assertThat(postWithSession(port, sessionId, 
INITIALIZED_NOTIFICATION)).isEqualTo(202);
+                assertThat(engine.sessionCount()).isEqualTo(1);
+            });
+        } finally {
+            engine.stop();
+        }
+    }
+
+    @Test
+    void explicitDeleteRemovesSessionImmediately() throws Exception {
+        int port = AvailablePortFinder.getNextAvailable();
+        VertxMcpServerEngine engine = startEngine(port, new 
McpServerInfo("eviction", "1.0", "/mcp", 0, 60_000));
+        try {
+            String sessionId = initializeSession(port);
+            assertThat(deleteSession(port, sessionId)).isEqualTo(200);
+            assertThat(engine.sessionCount()).isZero();
+        } finally {
+            engine.stop();
+        }
+    }
+
+    private static VertxMcpServerEngine startEngine(int port, McpServerInfo 
info) throws Exception {
+        CamelContext camelContext = new DefaultCamelContext();
+        MainHttpServer main = new MainHttpServer();
+        main.setCamelContext(camelContext);
+        main.setHost("0.0.0.0");
+        main.setPort(port);
+        camelContext.addService(main);
+        camelContext.start();
+
+        VertxMcpServerEngine engine = new VertxMcpServerEngine();
+        engine.setCamelContext(camelContext);
+        engine.initialize(info);
+        engine.start();
+        return engine;
+    }
+
+    private static String initializeSession(int port) throws Exception {
+        HttpResponse<String> response = HTTP.send(HttpRequest.newBuilder()
+                .uri(URI.create("http://localhost:"; + port + "/mcp"))
+                .timeout(Duration.ofSeconds(10))
+                .header("Content-Type", "application/json")
+                .header("Accept", "application/json, text/event-stream")
+                .POST(HttpRequest.BodyPublishers.ofString(INITIALIZE_REQUEST))
+                .build(), HttpResponse.BodyHandlers.ofString());
+        assertThat(response.statusCode()).isEqualTo(200);
+        return response.headers().firstValue("mcp-session-id").orElseThrow();
+    }
+
+    private static int postWithSession(int port, String sessionId, String 
body) throws Exception {
+        HttpResponse<Void> response = HTTP.send(HttpRequest.newBuilder()
+                .uri(URI.create("http://localhost:"; + port + "/mcp"))
+                .timeout(Duration.ofSeconds(10))
+                .header("Content-Type", "application/json")
+                .header("Accept", "application/json, text/event-stream")
+                .header("Mcp-Session-Id", sessionId)
+                .POST(HttpRequest.BodyPublishers.ofString(body))
+                .build(), HttpResponse.BodyHandlers.discarding());
+        return response.statusCode();
+    }
+
+    private static int deleteSession(int port, String sessionId) throws 
Exception {
+        HttpResponse<Void> response = HTTP.send(HttpRequest.newBuilder()
+                .uri(URI.create("http://localhost:"; + port + "/mcp"))
+                .timeout(Duration.ofSeconds(10))
+                .header("Mcp-Session-Id", sessionId)
+                .DELETE()
+                .build(), HttpResponse.BodyHandlers.discarding());
+        return response.statusCode();
+    }
+}
diff --git 
a/core/camel-main/src/generated/java/org/apache/camel/main/HttpServerConfigurationPropertiesConfigurer.java
 
b/core/camel-main/src/generated/java/org/apache/camel/main/HttpServerConfigurationPropertiesConfigurer.java
index 9129b2b45a81..42f63d53a06d 100644
--- 
a/core/camel-main/src/generated/java/org/apache/camel/main/HttpServerConfigurationPropertiesConfigurer.java
+++ 
b/core/camel-main/src/generated/java/org/apache/camel/main/HttpServerConfigurationPropertiesConfigurer.java
@@ -40,6 +40,8 @@ public class HttpServerConfigurationPropertiesConfigurer 
extends org.apache.came
         map.put("McpEnabled", boolean.class);
         map.put("McpPath", java.lang.String.class);
         map.put("McpServerName", java.lang.String.class);
+        map.put("McpSessionIdleTtl", long.class);
+        map.put("McpSessionKeepAliveInterval", long.class);
         map.put("McpTags", java.lang.String.class);
         map.put("McpToolTimeout", long.class);
         map.put("Path", java.lang.String.class);
@@ -89,6 +91,10 @@ public class HttpServerConfigurationPropertiesConfigurer 
extends org.apache.came
         case "mcpPath": target.setMcpPath(property(camelContext, 
java.lang.String.class, value)); return true;
         case "mcpservername":
         case "mcpServerName": target.setMcpServerName(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "mcpsessionidlettl":
+        case "mcpSessionIdleTtl": 
target.setMcpSessionIdleTtl(property(camelContext, long.class, value)); return 
true;
+        case "mcpsessionkeepaliveinterval":
+        case "mcpSessionKeepAliveInterval": 
target.setMcpSessionKeepAliveInterval(property(camelContext, long.class, 
value)); return true;
         case "mcptags":
         case "mcpTags": target.setMcpTags(property(camelContext, 
java.lang.String.class, value)); return true;
         case "mcptooltimeout":
@@ -149,6 +155,10 @@ public class HttpServerConfigurationPropertiesConfigurer 
extends org.apache.came
         case "mcpPath": return java.lang.String.class;
         case "mcpservername":
         case "mcpServerName": return java.lang.String.class;
+        case "mcpsessionidlettl":
+        case "mcpSessionIdleTtl": return long.class;
+        case "mcpsessionkeepaliveinterval":
+        case "mcpSessionKeepAliveInterval": return long.class;
         case "mcptags":
         case "mcpTags": return java.lang.String.class;
         case "mcptooltimeout":
@@ -205,6 +215,10 @@ public class HttpServerConfigurationPropertiesConfigurer 
extends org.apache.came
         case "mcpPath": return target.getMcpPath();
         case "mcpservername":
         case "mcpServerName": return target.getMcpServerName();
+        case "mcpsessionidlettl":
+        case "mcpSessionIdleTtl": return target.getMcpSessionIdleTtl();
+        case "mcpsessionkeepaliveinterval":
+        case "mcpSessionKeepAliveInterval": return 
target.getMcpSessionKeepAliveInterval();
         case "mcptags":
         case "mcpTags": return target.getMcpTags();
         case "mcptooltimeout":
diff --git 
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
 
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
index 3d8ec1317988..c03dd1557f00 100644
--- 
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
+++ 
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
@@ -442,6 +442,8 @@
     { "name": "camel.server.mcpEnabled", "required": false, "description": 
"Whether to expose ai-tool routes as MCP tools over streamable HTTP. Requires 
camel-mcp-server on the classpath. By default, the MCP server is not enabled.", 
"sourceType": "org.apache.camel.main.HttpServerConfigurationProperties", 
"type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": 
false },
     { "name": "camel.server.mcpPath", "required": false, "description": "HTTP 
path where the MCP endpoint is served.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "string", 
"javaType": "java.lang.String", "defaultValue": "\/mcp", "secret": false },
     { "name": "camel.server.mcpServerName", "required": false, "description": 
"MCP server name advertised to clients. Defaults to the CamelContext name.", 
"sourceType": "org.apache.camel.main.HttpServerConfigurationProperties", 
"type": "string", "javaType": "java.lang.String", "secret": false },
+    { "name": "camel.server.mcpSessionIdleTtl", "required": false, 
"description": "Idle TTL in milliseconds for MCP sessions on the Vert.x 
streamable transport. Sessions with no activity for longer than this interval 
are evicted. 0 disables idle eviction.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "integer", 
"javaType": "long", "defaultValue": 300000, "secret": false },
+    { "name": "camel.server.mcpSessionKeepAliveInterval", "required": false, 
"description": "Keep-alive ping interval in milliseconds for MCP sessions on 
the Vert.x streamable transport. Dead sessions are evicted after consecutive 
ping failures. 0 disables keep-alive pings.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "integer", 
"javaType": "long", "defaultValue": 30000, "secret": false },
     { "name": "camel.server.mcpTags", "required": false, "description": 
"Comma-separated list of ai-tool tags to expose as MCP tools. Only tools 
registered under one of these tags are exposed; the untagged default pool is 
never exposed. When not set, no tools are exposed.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "string", 
"javaType": "java.lang.String", "secret": false },
     { "name": "camel.server.mcpToolTimeout", "required": false, "description": 
"Per-call MCP tool execution timeout in milliseconds. A call exceeding the 
timeout returns an error result to the MCP client; the underlying route keeps 
running until it completes on its own.", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "integer", 
"javaType": "long", "defaultValue": 20000, "secret": false },
     { "name": "camel.server.path", "required": false, "description": 
"Context-path to use for embedded HTTP server", "sourceType": 
"org.apache.camel.main.HttpServerConfigurationProperties", "type": "string", 
"javaType": "java.lang.String", "defaultValue": "\/", "secret": false },
diff --git a/core/camel-main/src/main/docs/main.adoc 
b/core/camel-main/src/main/docs/main.adoc
index e3d33c3295b0..db7bd0eb0a19 100644
--- a/core/camel-main/src/main/docs/main.adoc
+++ b/core/camel-main/src/main/docs/main.adoc
@@ -195,7 +195,7 @@ The camel.routecontroller supports 12 options, which are 
listed below.
 
 
 === Camel Embedded HTTP Server (only for standalone; not Spring Boot or 
Quarkus) configurations
-The camel.server supports 26 options, which are listed below.
+The camel.server supports 28 options, which are listed below.
 
 [width="100%",cols="2,5,^1,2",options="header"]
 |===
@@ -218,6 +218,8 @@ The camel.server supports 26 options, which are listed 
below.
 | *camel.server.mcpEnabled* | Whether to expose ai-tool routes as MCP tools 
over streamable HTTP. Requires camel-mcp-server on the classpath. By default, 
the MCP server is not enabled. | false | boolean
 | *camel.server.mcpPath* | HTTP path where the MCP endpoint is served. | /mcp 
| String
 | *camel.server.mcpServerName* | MCP server name advertised to clients. 
Defaults to the CamelContext name. |  | String
+| *camel.server.mcpSessionIdleTtl* | Idle TTL in milliseconds for MCP sessions 
on the Vert.x streamable transport. Sessions with no activity for longer than 
this interval are evicted. 0 disables idle eviction. | 300000 | long
+| *camel.server.mcpSessionKeepAliveInterval* | Keep-alive ping interval in 
milliseconds for MCP sessions on the Vert.x streamable transport. Dead sessions 
are evicted after consecutive ping failures. 0 disables keep-alive pings. | 
30000 | long
 | *camel.server.mcpTags* | Comma-separated list of ai-tool tags to expose as 
MCP tools. Only tools registered under one of these tags are exposed; the 
untagged default pool is never exposed. When not set, no tools are exposed. |  
| String
 | *camel.server.mcpToolTimeout* | Per-call MCP tool execution timeout in 
milliseconds. A call exceeding the timeout returns an error result to the MCP 
client; the underlying route keeps running until it completes on its own. | 
20000 | long
 | *camel.server.path* | Context-path to use for embedded HTTP server | / | 
String
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
index da2603c3cce3..5285a09a12af 100644
--- 
a/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/HttpServerConfigurationProperties.java
@@ -79,6 +79,10 @@ public class HttpServerConfigurationProperties implements 
BootstrapCloseable {
     private long mcpToolTimeout = 20000;
     @Metadata(defaultValue = "/mcp")
     private String mcpPath = "/mcp";
+    @Metadata(defaultValue = "30000")
+    private long mcpSessionKeepAliveInterval = 30000;
+    @Metadata(defaultValue = "300000")
+    private long mcpSessionIdleTtl = 300000;
     @Metadata
     private String mcpServerName;
 
@@ -395,6 +399,30 @@ public class HttpServerConfigurationProperties implements 
BootstrapCloseable {
         this.mcpServerName = mcpServerName;
     }
 
+    public long getMcpSessionKeepAliveInterval() {
+        return mcpSessionKeepAliveInterval;
+    }
+
+    /**
+     * Keep-alive ping interval in milliseconds for MCP sessions on the Vert.x 
streamable transport. Dead sessions are
+     * evicted after consecutive ping failures. {@code 0} disables keep-alive 
pings.
+     */
+    public void setMcpSessionKeepAliveInterval(long 
mcpSessionKeepAliveInterval) {
+        this.mcpSessionKeepAliveInterval = mcpSessionKeepAliveInterval;
+    }
+
+    public long getMcpSessionIdleTtl() {
+        return mcpSessionIdleTtl;
+    }
+
+    /**
+     * Idle TTL in milliseconds for MCP sessions on the Vert.x streamable 
transport. Sessions with no activity for
+     * longer than this interval are evicted. {@code 0} disables idle eviction.
+     */
+    public void setMcpSessionIdleTtl(long mcpSessionIdleTtl) {
+        this.mcpSessionIdleTtl = mcpSessionIdleTtl;
+    }
+
     /**
      * Whether embedded HTTP server is enabled. By default, the server is not 
enabled.
      */
@@ -618,4 +646,20 @@ public class HttpServerConfigurationProperties implements 
BootstrapCloseable {
         return this;
     }
 
+    /**
+     * Keep-alive ping interval in milliseconds for MCP sessions on the Vert.x 
streamable transport.
+     */
+    public HttpServerConfigurationProperties 
withMcpSessionKeepAliveInterval(long mcpSessionKeepAliveInterval) {
+        this.mcpSessionKeepAliveInterval = mcpSessionKeepAliveInterval;
+        return this;
+    }
+
+    /**
+     * Idle TTL in milliseconds for MCP sessions on the Vert.x streamable 
transport.
+     */
+    public HttpServerConfigurationProperties withMcpSessionIdleTtl(long 
mcpSessionIdleTtl) {
+        this.mcpSessionIdleTtl = mcpSessionIdleTtl;
+        return this;
+    }
+
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 1fcc875dc9b4..8a05e7b290b1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -23,6 +23,14 @@ this executor is functionally identical to the built-in 
`DefaultReactiveExecutor
 To migrate, remove the `camel-reactive-executor-tomcat` dependency from your 
project. Camel will
 automatically use the default reactive executor.
 
+=== camel-mcp-server
+
+The Vert.x streamable transport now evicts orphaned MCP sessions 
automatically: keep-alive pings
+every 30 seconds (evict after consecutive failures on sessions with an open 
SSE stream) and an idle
+TTL of 5 minutes (evict sessions with no POST/GET activity). Configure with
+`camel.server.mcp-session-keep-alive-interval` and 
`camel.server.mcp-session-idle-ttl`; set either
+option to `0` to disable that mechanism.
+
 === camel-core
 
 ==== Property placeholders in toD and enrich dynamic endpoint URIs

Reply via email to