# Client Setup (/aiway/mcp/guides/client-setup)



The MCP connector speaks the [Model Context Protocol](/aiway/mcp) (version `2025-11-25`) as JSON-RPC 2.0 over the **Streamable HTTP transport** at `POST /mcp`. Any compliant MCP client — Claude Desktop, an official MCP SDK, or a hand-rolled JSON-RPC caller — connects the same way: point it at the endpoint, run the `initialize` handshake, and start calling tools. This guide covers each path in depth. For the 5-minute quick start, see [Getting started](/aiway/mcp/getting-started).

## Overview [#overview]

Every MCP client connects to a single URL and goes through the same three-step lifecycle before any tool call:

1. **`initialize`** — negotiate the protocol version and open a session.
2. **`notifications/initialized`** — acknowledge the handshake.
3. **`tools/list` / `tools/call`** — discover and invoke tools.

The official SDKs and Claude Desktop perform steps 1 and 2 automatically and track the [session ID](/aiway/mcp/guides/session-management) for you. With raw `curl` you drive each step yourself.

## How it works [#how-it-works]

The Streamable HTTP transport carries every JSON-RPC message as a standalone `POST /mcp`. The server returns an `MCP-Session-Id` on `initialize`, and the client echoes it on all subsequent requests. A separate `GET /mcp` opens a stateless keepalive stream that emits a `: keepalive` comment every 30 seconds.

<Mermaid
  chart="`
sequenceDiagram
participant C as MCP client
participant M as MCP connector (:9090)
C->>M: POST /mcp · initialize
M-->>C: capabilities + MCP-Session-Id
C->>M: POST /mcp · notifications/initialized
M-->>C: 200 OK · result: null
C->>M: POST /mcp · tools/list (MCP-Session-Id)
M-->>C: tool definitions
C->>M: POST /mcp · tools/call (MCP-Session-Id)
M-->>C: content[] + isError
`"
/>

*One session, established once, carries every later request via the `MCP-Session-Id` header.*

## Claude Desktop [#claude-desktop]

Claude Desktop connects to KubeMQ over the HTTP transport. Add a server entry to `claude_desktop_config.json` (Claude Desktop → Settings → Developer → Edit Config), then restart Claude Desktop.

```json title="claude_desktop_config.json"
{
  "mcpServers": {
    "kubemq": {
      "url": "http://localhost:9090/mcp"
    }
  }
}
```

On restart, Claude Desktop runs the `initialize` handshake, discovers all 15 KubeMQ [tools](/aiway/mcp/tools), and exposes them in the conversation. The `url` must include the `/mcp` path — it is the only required field for an unauthenticated server.

### Authenticated servers [#authenticated-servers]

If the [shared HTTP server](/connectors/concepts/shared-http-server) has JWT auth enabled, add an `Authorization` header so Claude Desktop sends a Bearer token with every request:

```json title="claude_desktop_config.json"
{
  "mcpServers": {
    "kubemq": {
      "url": "https://kubemq.example.com/mcp",
      "headers": {
        "Authorization": "Bearer <jwt-token>"
      }
    }
  }
}
```

Use `https://` for any remote endpoint. See [Authentication](/aiway/mcp/guides/authentication) for how the connector validates the token and returns `-32010` on failure.

<Callout type="warn">
  The MCP connector also validates the request `Origin` against `McpConfig.TrustedOrigins` (default `["auto"]`, which matches `localhost`, `127.0.0.1`, `::1`, `[::1]`, `0.0.0.0`, and the server's bind address). When connecting from a remote host or a custom origin, add it to the trusted-origins list in [Configuration](/aiway/mcp/configuration) or origin validation will reject the connection.
</Callout>

## Generic JSON-RPC client [#generic-json-rpc-client]

Any HTTP client can speak MCP directly — no SDK required. You manage the handshake, the `MCP-Session-Id` header, and request IDs yourself. This is the lowest-level path and mirrors exactly what an SDK does internally.

<Steps>
  <Step>
    ### Open a session with `initialize` [#open-a-session-with-initialize]

    `POST` an `initialize` request announcing the protocol version (`2025-11-25`) and your client identity:

    ```bash title="terminal"
    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" }
        }
      }'
    ```

    Capture the session ID from `result._meta.sessionId` in the body — it is also returned in the `MCP-Session-Id` response header (use `-i` to see headers):

    ```json
    {
      "jsonrpc": "2.0",
      "id": 1,
      "result": {
        "protocolVersion": "2025-11-25",
        "capabilities": { "tools": {} },
        "serverInfo": { "name": "kubemq", "version": "..." },
        "_meta": { "sessionId": "<session-id>" }
      }
    }
    ```
  </Step>

  <Step>
    ### Acknowledge with `notifications/initialized` [#acknowledge-with-notificationsinitialized]

    Send the `notifications/initialized` notification, carrying the session ID. It has no `id`, so the server replies with HTTP `200 OK` and body `{"jsonrpc":"2.0","result":null,"id":null}`:

    ```bash title="terminal"
    curl -X POST http://localhost:9090/mcp \
      -H 'Content-Type: application/json' \
      -H 'MCP-Session-Id: <session-id>' \
      -d '{
        "jsonrpc": "2.0",
        "method": "notifications/initialized"
      }'
    ```
  </Step>

  <Step>
    ### Call tools with the session header [#call-tools-with-the-session-header]

    Every later request — `tools/list`, `tools/call`, `ping` — includes the `MCP-Session-Id` header:

    ```bash title="terminal"
    curl -X POST http://localhost:9090/mcp \
      -H 'Content-Type: application/json' \
      -H 'MCP-Session-Id: <session-id>' \
      -d '{
        "jsonrpc": "2.0",
        "id": 2,
        "method": "tools/call",
        "params": {
          "name": "queue_send",
          "arguments": { "channel": "my-queue", "body": "Hello from MCP" }
        }
      }'
    ```
  </Step>
</Steps>

### Required headers [#required-headers]

| Header                 | Direction | When               | Description                                     |
| ---------------------- | --------- | ------------------ | ----------------------------------------------- |
| `Content-Type`         | Request   | Always             | Must be `application/json`.                     |
| `MCP-Session-Id`       | Request   | After `initialize` | The session ID returned by the handshake.       |
| `MCP-Session-Id`       | Response  | Always             | Echoed back by the server.                      |
| `MCP-Protocol-Version` | Response  | Always             | The protocol version, `2025-11-25`.             |
| `Authorization`        | Request   | When auth is on    | `Bearer <jwt-token>` for authenticated servers. |

<Callout type="info">
  A stateless `GET /mcp` opens an SSE keepalive stream that emits a `: keepalive` comment every 30 seconds and carries no MCP messages — it only keeps an idle connection alive and closes when the client disconnects. See [Session management](/aiway/mcp/guides/session-management) for the full lifecycle and batching.
</Callout>

## Official MCP SDKs [#official-mcp-sdks]

Each official MCP SDK wraps the Streamable HTTP transport: it performs the `initialize` handshake, sends `notifications/initialized`, and tracks the session ID automatically — you construct the transport with the endpoint URL and call tools. All examples read `KUBEMQ_MCP_URL` (default `http://localhost:9090`) and append `/mcp`.

<Tabs groupId="language" items="['curl','Go','Python','TypeScript','Java','C#','Kotlin','Ruby','Rust','Swift']">
  <Tab value="curl">
    ```bash
    # Raw JSON-RPC: initialize, then call a tool with the returned session ID.
    curl -X POST http://localhost:9090/mcp \
      -H 'Content-Type: application/json' \
      -H 'MCP-Session-Id: <session-id>' \
      -d '{
        "jsonrpc": "2.0",
        "id": 2,
        "method": "tools/call",
        "params": {
          "name": "queue_send",
          "arguments": {
            "channel": "example-queue",
            "body": "Hello from MCP",
            "metadata": "example-metadata",
            "tags": { "env": "dev", "source": "mcp-example" }
          }
        }
      }'
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"context"
    	"fmt"
    	"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()
    	if err := c.Start(ctx); err != nil {
    		log.Fatal(err)
    	}

    	result, err := c.CallTool(ctx, mcp.CallToolRequest{
    		Params: mcp.CallToolParams{
    			Name: "queue_send",
    			Arguments: map[string]any{
    				"channel":  "example-queue",
    				"body":     "Hello from Go MCP",
    				"metadata": "example-metadata",
    				"tags":     map[string]any{"env": "dev", "source": "mcp-example"},
    			},
    		},
    	})
    	if err != nil {
    		log.Fatal(err)
    	}

    	fmt.Println("Tool: queue_send")
    	fmt.Printf("Result: %+v\n", result)
    }
    ```
  </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:
                await session.initialize()

                result = await session.call_tool("queue_send", {
                    "channel": "example-queue",
                    "body": "Hello from Python MCP",
                    "metadata": "example-metadata",
                    "tags": {"env": "dev", "source": "mcp-example"},
                })

                print(f"Tool: queue_send")
                print(f"IsError: {result.isError}")
                for content in result.content:
                    print(f"Result: {content.text}")


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </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";

    async function main() {
      const transport = new StreamableHTTPClientTransport(
        new URL(`${KUBEMQ_MCP_URL}/mcp`)
      );
      const client = new Client({ name: "kubemq-mcp-ts-example", version: "1.0.0" });
      await client.connect(transport);

      const result = await client.callTool({
        name: "queue_send",
        arguments: {
          channel: "example-queue",
          body: "Hello from TypeScript MCP",
          metadata: "example-metadata",
          tags: { env: "dev", source: "mcp-example" },
        },
      });
      console.log(JSON.stringify(result, null, 2));

      await client.close();
    }

    main().catch(console.error);
    ```
  </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 QueueSend {
        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();
            client.initialize();

            var result = client.callTool(new CallToolRequest(
                "queue_send",
                Map.of(
                    "channel", "example-queue",
                    "body", "Hello from Java MCP",
                    "metadata", "example-metadata",
                    "tags", Map.of("env", "dev", "source", "mcp-example")
                )
            ));
            System.out.println(result);

            client.closeGracefully();
        }
    }
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using ModelContextProtocol.Client;

    class QueueSend
    {
        static async Task Main(string[] args)
        {
            var url = Environment.GetEnvironmentVariable("KUBEMQ_MCP_URL") ?? "http://localhost:9090";
            var transport = new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri($"{url}/mcp") });
            await using var client = await McpClientFactory.CreateAsync(transport);

            var result = await client.CallToolAsync("queue_send", new Dictionary<string, object>
            {
                ["channel"] = "example-queue",
                ["body"] = "Hello from C# MCP",
                ["metadata"] = "example-metadata",
                ["tags"] = new Dictionary<string, string> { ["env"] = "dev", ["source"] = "mcp-example" },
            });

            Console.WriteLine($"Tool: queue_send");
            Console.WriteLine($"Result: {result}");
        }
    }
    ```
  </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-example", version = "1.0.0"))
        client.connect(transport)

        val result = client.callTool("queue_send", mapOf(
            "channel" to "example-queue",
            "body" to "Hello from Kotlin MCP",
            "metadata" to "example-metadata",
            "tags" to mapOf("env" to "dev", "source" to "mcp-example")
        ))

        println("Tool: queue_send")
        println("Result: $result")

        client.close()
        httpClient.close()
    }
    ```
  </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-example",
      version: "1.0.0"
    )
    client.initialize_handshake

    result = client.call_tool("queue_send", {
      "channel" => "example-queue",
      "body" => "Hello from Ruby MCP",
      "metadata" => "example-metadata",
      "tags" => { "env" => "dev", "source" => "mcp-example" },
    })

    puts "Tool: queue_send"
    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"))?;
        let client = ().serve(transport).await?;

        let result = client.call_tool("queue_send", json!({
            "channel": "example-queue",
            "body": "Hello from Rust MCP",
            "metadata": "example-metadata",
            "tags": {"env": "dev", "source": "mcp-example"}
        })).await?;

        println!("Tool: queue_send");
        println!("Result: {result:#?}");
        Ok(())
    }
    ```
  </Tab>

  <Tab value="Swift">
    ```swift
    import Foundation
    import MCP

    @main
    struct QueueSend {
        static func main() async throws {
            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-example", version: "1.0.0")
            try await client.connect(transport: transport)

            let result = try await client.callTool("queue_send", arguments: [
                "channel": "example-queue",
                "body": "Hello from Swift MCP",
                "metadata": "example-metadata",
                "tags": ["env": "dev", "source": "mcp-example"],
            ])

            print("Tool: queue_send")
            print("Result: \(result)")
        }
    }
    ```
  </Tab>
</Tabs>

<Callout type="info">
  For an authenticated server, pass the JWT through the SDK's transport options (an `Authorization: Bearer <token>` header) — the same mechanism Claude Desktop uses. The standard environment variables are `KUBEMQ_MCP_URL`, `KUBEMQ_MCP_TIMEOUT` (client-side HTTP timeout, default `30`s), and `KUBEMQ_MCP_AUTH_TOKEN`.
</Callout>

## Related [#related]

<Cards>
  <Card title="Getting started" href="/aiway/mcp/getting-started" description="The 5-minute path: run KubeMQ, connect Claude Desktop, call your first tool." />

  <Card title="Session management" href="/aiway/mcp/guides/session-management" description="The initialize handshake, MCP-Session-Id tracking, batching, and GET /mcp keepalive." />

  <Card title="Authentication" href="/aiway/mcp/guides/authentication" description="JWT Bearer auth for /mcp, the -32010 error, and origin validation." />

  <Card title="Configuration" href="/aiway/mcp/configuration" description="McpConfig fields — tool timeout, trusted origins, and the disable env var." />
</Cards>
