# Session Management (/aiway/mcp/guides/session-management)



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 [#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.

<Callout type="info">
  The session model is part of the [shared HTTP server](/connectors/concepts/shared-http-server). For who may open a session and how auth applies, see [Auth & security](/connectors/reference/auth-and-security).
</Callout>

## How it works [#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.

<Mermaid
  chart="`
sequenceDiagram
participant C as MCP client
participant S as KubeMQ MCP connector
C->>S: POST /mcp — initialize
S-->>C: result + _meta.sessionId
C->>S: POST /mcp — notifications/initialized
S-->>C: 200 OK (result: null)
C->>S: POST /mcp — tools/call (MCP-Session-Id)
S-->>C: result.content[]
Note over C,S: session reused for every later call
`"
/>

*The initialize handshake opens a session; the session ID is replayed on each later request.*

## The handshake [#the-handshake]

### Step 1 — initialize [#step-1--initialize]

Send an `initialize` request with the protocol version, your capabilities, and `clientInfo`:

```json
{
  "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 [#step-2--receive-the-session-id]

The server responds with its `protocolVersion`, `capabilities`, `serverInfo`, and the session ID under `result._meta.sessionId`:

```json
{
  "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 [#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}`:

```json
{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}
```

## Establishing a session [#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.

<Tabs groupId="language" items="['curl','C#','Go','Java','Kotlin','Python','Ruby','Rust','Swift','TypeScript']">
  <Tab value="curl">
    ```bash
    # 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"}}}'
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    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}");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    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)
        }
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    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();
        }
    }
    ```
  </Tab>

  <Tab value="Kotlin">
    ```kotlin
    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()
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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())
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby
    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
    ```
  </Tab>

  <Tab value="Rust">
    ```rust
    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(())
    }
    ```
  </Tab>

  <Tab value="Swift">
    ```swift
    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)")
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    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();
    ```
  </Tab>
</Tabs>

## Session headers [#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 [#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 [#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.

<Tabs groupId="language" items="['curl','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    # 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"}}}
      ]'
    ```
  </Tab>

  <Tab value="Python">
    ```python
    # 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"})
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    // 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" } });
    ```
  </Tab>
</Tabs>

## Keepalive stream — GET /mcp [#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`.

```bash
# Hold an SSE keepalive open (a ": keepalive" comment arrives every 30s)
curl -N http://localhost:9090/mcp
```

## Related [#related]

<Cards>
  <Card title="Client setup" href="/aiway/mcp/guides/client-setup" description="Connect Claude Desktop and generic MCP clients to the KubeMQ connector." />

  <Card title="Endpoints reference" href="/aiway/mcp/reference/endpoints" description="POST /mcp and GET /mcp, JSON-RPC methods, and status codes." />

  <Card title="Authentication" href="/aiway/mcp/guides/authentication" description="JWT Bearer auth and origin validation for MCP sessions." />
</Cards>
