Session Management
Establish and reuse an MCP session with the KubeMQ connector — initialize handshake, MCP-Session-Id, batch requests, and the GET /mcp keepalive stream.
Every MCP interaction with the KubeMQ connector runs inside a session. The session is opened by an initialize handshake, identified by an MCP-Session-Id header, and reused across every tool call until the client disconnects.
Overview
The MCP connector speaks JSON-RPC 2.0 over the shared HTTP server on port 9090. A single endpoint — POST /mcp — handles initialize, tools/list, tools/call, and ping. A companion GET /mcp endpoint provides an SSE keepalive stream.
A session is server-managed: the server issues a session ID during initialize, and the client echoes it on every subsequent request. Multiple tool calls share one session, and each session keeps its own request context. You rarely build the handshake by hand — every official MCP SDK performs it for you when you connect. This page shows both: the raw protocol so you understand what travels on the wire, and the SDK call that establishes the session for you.
The session model is part of the shared HTTP server. For who may open a session and how auth applies, see Auth & security.
How it works
The handshake is three steps: the client sends initialize, the server returns its capabilities plus a session ID, and the client acknowledges with a notifications/initialized notification. After that, every request carries the MCP-Session-Id header.
The initialize handshake opens a session; the session ID is replayed on each later request.
The handshake
Step 1 — initialize
Send an initialize request with the protocol version, your capabilities, and clientInfo:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "my-agent", "version": "1.0.0" }
}
}Step 2 — receive the session ID
The server responds with its protocolVersion, capabilities, serverInfo, and the session ID under result._meta.sessionId:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "kubemq", "version": "<server version>" },
"_meta": { "sessionId": "abc123-def456" }
}
}The session ID is also returned in the MCP-Session-Id response header, alongside MCP-Protocol-Version: 2025-11-25.
Step 3 — send the initialized notification
Acknowledge with a notifications/initialized notification. A notification has no id field and the server returns HTTP 200 with body {"jsonrpc":"2.0","result":null,"id":null}:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}Establishing a session
Below, curl walks the raw three-step handshake; the SDK tabs perform the same handshake transparently when you connect, then reuse the session for every tool call.
# 1. initialize — capture the session ID from the MCP-Session-Id response header
curl -i -X POST http://localhost:9090/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0.0"}}}'
# 2. acknowledge — replay the session ID; server returns 200 with {"jsonrpc":"2.0","result":null,"id":null}
curl -X POST http://localhost:9090/mcp \
-H 'Content-Type: application/json' \
-H 'MCP-Session-Id: abc123-def456' \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# 3. call a tool inside the session
curl -X POST http://localhost:9090/mcp \
-H 'Content-Type: application/json' \
-H 'MCP-Session-Id: abc123-def456' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"queue_send","arguments":{"channel":"example-queue","body":"Hello"}}}'using ModelContextProtocol.Client;
var url = Environment.GetEnvironmentVariable("KUBEMQ_MCP_URL") ?? "http://localhost:9090";
var transport = new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri($"{url}/mcp") });
// CreateAsync runs the initialize handshake and holds the session for reuse.
await using var client = await McpClientFactory.CreateAsync(transport);
// Every later call rides the same session.
var result = await client.CallToolAsync("queue_send", new Dictionary<string, object>
{
["channel"] = "example-queue",
["body"] = "Hello from C# MCP",
});
Console.WriteLine($"Result: {result}");package main
import (
"context"
"log"
"os"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)
func main() {
url := os.Getenv("KUBEMQ_MCP_URL")
if url == "" {
url = "http://localhost:9090"
}
c, err := client.NewStreamableHttpClient(url + "/mcp")
if err != nil {
log.Fatal(err)
}
defer c.Close()
ctx := context.Background()
// Start performs the initialize handshake and binds the session.
if err := c.Start(ctx); err != nil {
log.Fatal(err)
}
// The same session is reused for every CallTool.
_, err = c.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "queue_send",
Arguments: map[string]any{"channel": "example-queue", "body": "Hello from Go MCP"},
},
})
if err != nil {
log.Fatal(err)
}
}import io.modelcontextprotocol.sdk.McpClient;
import io.modelcontextprotocol.sdk.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import java.util.Map;
public class Session {
public static void main(String[] args) {
String url = System.getenv().getOrDefault("KUBEMQ_MCP_URL", "http://localhost:9090");
var transport = HttpClientStreamableHttpTransport.builder(url).endpoint("/mcp").build();
var client = McpClient.sync(transport).build();
// initialize() performs the handshake and opens the session.
client.initialize();
// The client reuses the session for each call.
var result = client.callTool(new CallToolRequest(
"queue_send",
Map.of("channel", "example-queue", "body", "Hello from Java MCP")
));
System.out.println(result);
client.closeGracefully();
}
}import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport
import io.ktor.client.*
import io.ktor.client.plugins.sse.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val url = System.getenv("KUBEMQ_MCP_URL") ?: "http://localhost:9090"
val httpClient = HttpClient { install(SSE) }
val transport = StreamableHttpClientTransport(client = httpClient, url = "$url/mcp")
val client = Client(clientInfo = Implementation(name = "kubemq-mcp-kotlin", version = "1.0.0"))
// connect() runs the initialize handshake and holds the session.
client.connect(transport)
client.callTool("queue_send", mapOf("channel" to "example-queue", "body" to "Hello from Kotlin MCP"))
client.close()
httpClient.close()
}import asyncio
import os
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
KUBEMQ_MCP_URL = os.environ.get("KUBEMQ_MCP_URL", "http://localhost:9090")
async def main():
async with streamablehttp_client(f"{KUBEMQ_MCP_URL}/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
# initialize() performs the 3-step handshake and stores the session ID.
await session.initialize()
# Every call_tool on this session replays the same MCP-Session-Id.
result = await session.call_tool("queue_send", {
"channel": "example-queue",
"body": "Hello from Python MCP",
})
print(f"IsError: {result.isError}")
if __name__ == "__main__":
asyncio.run(main())require "mcp"
url = ENV.fetch("KUBEMQ_MCP_URL", "http://localhost:9090")
client = MCP::Client.new(
transport: MCP::Transport::StreamableHTTP.new("#{url}/mcp"),
name: "kubemq-mcp-ruby",
version: "1.0.0"
)
# initialize_handshake performs the handshake and opens the session.
client.initialize_handshake
# The session is reused for each call_tool.
result = client.call_tool("queue_send", {
"channel" => "example-queue",
"body" => "Hello from Ruby MCP",
})
puts "Result: #{result}"
client.closeuse rmcp::transport::streamable_http::StreamableHttpClientTransport;
use rmcp::service::RunService;
use serde_json::json;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let url = std::env::var("KUBEMQ_MCP_URL")
.unwrap_or_else(|_| "http://localhost:9090".to_string());
let transport = StreamableHttpClientTransport::from_uri(format!("{url}/mcp"))?;
// serve() runs the initialize handshake and binds the session.
let client = ().serve(transport).await?;
// The client reuses the session for each call.
let _ = client.call_tool("queue_send", json!({
"channel": "example-queue",
"body": "Hello from Rust MCP"
})).await?;
Ok(())
}import Foundation
import MCP
let url = ProcessInfo.processInfo.environment["KUBEMQ_MCP_URL"] ?? "http://localhost:9090"
let transport = HTTPClientTransport(endpoint: URL(string: "\(url)/mcp")!, streaming: true)
let client = Client(name: "kubemq-mcp-swift", version: "1.0.0")
// connect() performs the initialize handshake and opens the session.
try await client.connect(transport: transport)
// The session is reused for each callTool.
let result = try await client.callTool("queue_send", arguments: [
"channel": "example-queue",
"body": "Hello from Swift MCP",
])
print("Result: \(result)")import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const KUBEMQ_MCP_URL = process.env.KUBEMQ_MCP_URL || "http://localhost:9090";
const transport = new StreamableHTTPClientTransport(new URL(`${KUBEMQ_MCP_URL}/mcp`));
const client = new Client({ name: "kubemq-mcp-ts", version: "1.0.0" });
// connect() runs the initialize handshake and holds the session for reuse.
await client.connect(transport);
// Every callTool on this client rides the same session.
const result = await client.callTool({
name: "queue_send",
arguments: { channel: "example-queue", body: "Hello from TypeScript MCP" },
});
console.log(JSON.stringify(result, null, 2));
await client.close();Session headers
| Header | Direction | Required | Description |
|---|---|---|---|
Content-Type | Request | Always | Must be application/json |
MCP-Session-Id | Request | After initialize | Session identifier from the initialize response |
MCP-Session-Id | Response | Always | Echoed back by the server |
MCP-Protocol-Version | Response | Always | Protocol version 2025-11-25 |
Session lifecycle
- Sessions are server-managed — the server mints the session ID during
initialize. - Multiple tool calls share one session; each maintains its own request context.
- A session persists until the client disconnects or a server-side inactivity timeout occurs.
- Reusing the connection (and the
MCP-Session-Id) avoids re-running the handshake on every call.
Batch requests
POST /mcp accepts a JSON array of JSON-RPC requests and processes each one sequentially. Requests carrying an id produce a response entry; notifications (no id) are executed but produce no entry. Send the batch with the same MCP-Session-Id as any single request.
# Two tool calls in a single batched POST — one response entry per id.
curl -X POST http://localhost:9090/mcp \
-H 'Content-Type: application/json' \
-H 'MCP-Session-Id: abc123-def456' \
-d '[
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"queue_send","arguments":{"channel":"a","body":"one"}}},
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"queue_send","arguments":{"channel":"b","body":"two"}}}
]'# SDKs typically issue calls individually over one session rather than
# constructing a raw JSON-RPC array. The session is reused for each call:
await session.call_tool("queue_send", {"channel": "a", "body": "one"})
await session.call_tool("queue_send", {"channel": "b", "body": "two"})// SDKs reuse the open session per call; the connector batches at the HTTP
// layer when a raw JSON-RPC array is posted.
await client.callTool({ name: "queue_send", arguments: { channel: "a", body: "one" } });
await client.callTool({ name: "queue_send", arguments: { channel: "b", body: "two" } });Keepalive stream — GET /mcp
GET /mcp opens an SSE stream that emits a : keepalive comment every 30 seconds. It is a stateless keepalive only — no MCP messages travel over it, and the stream closes when the client disconnects. Use it to hold a long-lived connection open through intermediaries; all real work still goes through POST /mcp.
# Hold an SSE keepalive open (a ": keepalive" comment arrives every 30s)
curl -N http://localhost:9090/mcpRelated
Was this page helpful?
Channel Resolution
How MCP tools map to KubeMQ channels — naming, the five channel types, the reserved _AGENTS_ prefix, and discovery via channel_list and channel_info.
Error Handling
Detect and branch on the three MCP failure layers — HTTP/auth, JSON-RPC protocol errors, and tool isError results — and handle timeouts robustly.