KubeMQ
AiwayMCPGuides

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.close
use 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

HeaderDirectionRequiredDescription
Content-TypeRequestAlwaysMust be application/json
MCP-Session-IdRequestAfter initializeSession identifier from the initialize response
MCP-Session-IdResponseAlwaysEchoed back by the server
MCP-Protocol-VersionResponseAlwaysProtocol 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/mcp

Was this page helpful?

On this page