# Authentication (/aiway/mcp/guides/authentication)



The MCP connector is guarded by the same JWT Bearer authentication as every other
KubeMQ connector. You authenticate to `POST /mcp` with an `Authorization: Bearer`
header; KubeMQ verifies the token, attaches your identity claims, and lets the tool
call through. This guide shows how to attach that header through each MCP SDK's
transport — the place an SDK client differs from a raw `curl` call — plus how MCP
reports auth failures and validates request origins.

## Overview [#overview]

Authentication for MCP is the connector-wide model described in
[Auth & Security](/connectors/reference/auth-and-security), applied to the single MCP
endpoint:

* **Endpoint** — `POST /mcp` (and the `GET /mcp` keepalive stream) carry every
  JSON-RPC method: `initialize`, `tools/list`, `tools/call`, and `ping`. A verified
  token identifies the caller; that `ClientID` flows through to the broker and, for the
  [agent-bridge tools](/aiway/mcp/tools/agent-bridge), to the agent as
  `X-KubeMQ-Caller-ID`.

When server authentication is **disabled** (the default for local development), every
caller is treated as the synthetic `anonymous` principal and no token is required. When
it is **enabled**, a missing or unverified token is rejected as a JSON-RPC `-32010`
error.

<Callout type="info">
  Authentication is **disabled by default** so MCP works out of the box for local
  development. Enable it before exposing the endpoint beyond a trusted network — see
  [Production recommendations](#production-recommendations).
</Callout>

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

The token is verified once, at the shared auth middleware, before the request reaches
the MCP connector. The middleware extracts the `Bearer` token from the `Authorization`
header, verifies it against KubeMQ's authentication singleton, and attaches the caller's
claims (including `ClientID`) to the request context. The connector then resolves the
tool to a KubeMQ operation under that identity.

<Mermaid
  chart="`
graph LR
CLIENT[&#x22;MCP client<br/>+ Bearer token&#x22;]
AUTH[&#x22;JWT auth middleware&#x22;]
MCP[&#x22;MCP connector<br/>:9090&#x22;]
BROKER[&#x22;Message Broker&#x22;]

CLIENT --> AUTH
AUTH --> MCP
MCP --> BROKER

class CLIENT external
class AUTH,MCP aiway
class BROKER broker
`"
/>

*KubeMQ verifies the Bearer token at the edge, then runs the tool call under the caller's identity.*

## Attaching the token [#attaching-the-token]

Every MCP request must carry an `Authorization: Bearer <jwt>` header when server auth is
enabled. With raw JSON-RPC the header goes straight onto the HTTP request; with an MCP
SDK you attach it through the **streamable-HTTP transport** so the header rides on every
call in the session — the `initialize` handshake, `tools/list`, and each `tools/call`.

These snippets store the token in the `KUBEMQ_MCP_AUTH_TOKEN` environment variable —
the same variable the KubeMQ MCP examples use — and send it on a `tools/list` request.

<Tabs groupId="language" items="['curl','C#','Go','Java','Kotlin','Python','Ruby','Rust','Swift','TypeScript']">
  <Tab value="curl">
    ```bash
    curl -X POST http://localhost:9090/mcp \
      -H "Authorization: Bearer $KUBEMQ_MCP_AUTH_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
    ```
  </Tab>

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

    var url = Environment.GetEnvironmentVariable("KUBEMQ_MCP_URL") ?? "http://localhost:9090";
    var token = Environment.GetEnvironmentVariable("KUBEMQ_MCP_AUTH_TOKEN");

    var transport = new HttpClientTransport(new HttpClientTransportOptions
    {
        Endpoint = new Uri($"{url}/mcp"),
        AdditionalHeaders = new Dictionary<string, string>
        {
            ["Authorization"] = $"Bearer {token}",
        },
    });
    await using var client = await McpClientFactory.CreateAsync(transport);

    var tools = await client.ListToolsAsync();
    Console.WriteLine($"Tools: {tools.Count}");
    ```
  </Tab>

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

    import (
    	"context"
    	"fmt"
    	"log"
    	"os"

    	"github.com/mark3labs/mcp-go/client"
    	"github.com/mark3labs/mcp-go/client/transport"
    	"github.com/mark3labs/mcp-go/mcp"
    )

    func main() {
    	url := os.Getenv("KUBEMQ_MCP_URL")
    	if url == "" {
    		url = "http://localhost:9090"
    	}
    	token := os.Getenv("KUBEMQ_MCP_AUTH_TOKEN")

    	c, err := client.NewStreamableHttpClient(url+"/mcp",
    		transport.WithHTTPHeaders(map[string]string{
    			"Authorization": "Bearer " + token,
    		}),
    	)
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer c.Close()

    	ctx := context.Background()
    	if err := c.Start(ctx); err != nil {
    		log.Fatal(err)
    	}
    	if _, err := c.Initialize(ctx, mcp.InitializeRequest{}); err != nil {
    		log.Fatal(err)
    	}

    	tools, err := c.ListTools(ctx, mcp.ListToolsRequest{})
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Printf("Tools: %d\n", len(tools.Tools))
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import io.modelcontextprotocol.sdk.McpClient;
    import io.modelcontextprotocol.sdk.client.transport.HttpClientStreamableHttpTransport;

    public class Auth {
        public static void main(String[] args) {
            String url = System.getenv().getOrDefault("KUBEMQ_MCP_URL", "http://localhost:9090");
            String token = System.getenv("KUBEMQ_MCP_AUTH_TOKEN");

            var transport = HttpClientStreamableHttpTransport.builder(url)
                .endpoint("/mcp")
                .httpRequestCustomizer((builder, method, endpoint, body, context) ->
                    builder.header("Authorization", "Bearer " + token))
                .build();
            var client = McpClient.sync(transport).build();
            client.initialize();

            var tools = client.listTools();
            System.out.println("Tools: " + tools.tools().size());

            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.*
    import io.ktor.client.plugins.sse.*
    import io.ktor.client.request.*
    import kotlinx.coroutines.runBlocking

    fun main() = runBlocking {
        val url = System.getenv("KUBEMQ_MCP_URL") ?: "http://localhost:9090"
        val token = System.getenv("KUBEMQ_MCP_AUTH_TOKEN")

        val httpClient = HttpClient {
            install(SSE)
            defaultRequest { header("Authorization", "Bearer $token") }
        }
        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 tools = client.listTools()
        println("Tools: ${tools?.tools?.size}")
    }
    ```
  </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")
    TOKEN = os.environ.get("KUBEMQ_MCP_AUTH_TOKEN")


    async def main():
        headers = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else None
        async with streamablehttp_client(f"{KUBEMQ_MCP_URL}/mcp", headers=headers) as (read, write, _):
            async with ClientSession(read, write) as session:
                await session.initialize()
                tools = await session.list_tools()
                print(f"Tools: {len(tools.tools)}")


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby
    require "mcp"

    url = ENV.fetch("KUBEMQ_MCP_URL", "http://localhost:9090")
    token = ENV["KUBEMQ_MCP_AUTH_TOKEN"]

    client = MCP::Client.new(
      transport: MCP::Transport::StreamableHTTP.new(
        "#{url}/mcp",
        headers: { "Authorization" => "Bearer #{token}" }
      ),
      name: "kubemq-mcp-ruby-example",
      version: "1.0.0"
    )
    client.initialize_handshake

    tools = client.list_tools
    puts "Tools: #{tools.size}"

    client.close
    ```
  </Tab>

  <Tab value="Rust">
    ```rust
    use rmcp::transport::streamable_http::StreamableHttpClientTransport;
    use rmcp::transport::streamable_http::client::StreamableHttpClientTransportConfig;
    use rmcp::service::RunService;

    #[tokio::main]
    async fn main() -> anyhow::Result<()> {
        let url = std::env::var("KUBEMQ_MCP_URL")
            .unwrap_or_else(|_| "http://localhost:9090".to_string());
        let token = std::env::var("KUBEMQ_MCP_AUTH_TOKEN").unwrap_or_default();

        let http = reqwest::Client::builder()
            .default_headers({
                let mut h = reqwest::header::HeaderMap::new();
                h.insert(
                    reqwest::header::AUTHORIZATION,
                    format!("Bearer {token}").parse()?,
                );
                h
            })
            .build()?;

        let transport = StreamableHttpClientTransport::with_client(
            http,
            StreamableHttpClientTransportConfig::with_uri(format!("{url}/mcp")),
        );
        let client = ().serve(transport).await?;

        let tools = client.list_all_tools().await?;
        println!("Tools: {}", tools.len());
        Ok(())
    }
    ```
  </Tab>

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

    @main
    struct Auth {
        static func main() async throws {
            let url = ProcessInfo.processInfo.environment["KUBEMQ_MCP_URL"] ?? "http://localhost:9090"
            let token = ProcessInfo.processInfo.environment["KUBEMQ_MCP_AUTH_TOKEN"] ?? ""

            let transport = HTTPClientTransport(
                endpoint: URL(string: "\(url)/mcp")!,
                streaming: true,
                requestModifier: { request in
                    var modified = request
                    modified.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
                    return modified
                }
            )
            let client = Client(name: "kubemq-mcp-swift-example", version: "1.0.0")
            try await client.connect(transport: transport)

            let (tools, _) = try await client.listTools()
            print("Tools: \(tools.count)")
        }
    }
    ```
  </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 token = process.env.KUBEMQ_MCP_AUTH_TOKEN;

    const transport = new StreamableHTTPClientTransport(
      new URL(`${KUBEMQ_MCP_URL}/mcp`),
      {
        requestInit: {
          headers: { Authorization: `Bearer ${token}` },
        },
      }
    );
    const client = new Client({ name: "kubemq-mcp-ts-example", version: "1.0.0" });
    await client.connect(transport);

    const tools = await client.listTools();
    console.log(`Tools: ${tools.tools.length}`);

    await client.close();
    ```
  </Tab>
</Tabs>

<Callout type="info">
  The token is set on the **transport**, not on a single request, so it is sent with the
  `initialize` handshake and every subsequent `tools/call` in the session. Set
  `KUBEMQ_MCP_AUTH_TOKEN` in your environment rather than hard-coding the JWT in source.
</Callout>

## How auth failures are reported [#how-auth-failures-are-reported]

MCP is a JSON-RPC endpoint, so authentication errors come back as a JSON-RPC error
object — not an HTTP 401. The HTTP status stays `200`; the failure is encoded in the
`error` field with code `-32010`.

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32010,
    "message": "Authentication failed"
  }
}
```

| Condition                              | Result                                    |
| -------------------------------------- | ----------------------------------------- |
| Auth disabled (default)                | Request runs as the `anonymous` principal |
| Auth enabled, valid token              | Request runs as the token's `ClientID`    |
| Auth enabled, missing or invalid token | JSON-RPC error &#x2A;*`-32010`**          |

<Callout type="warn">
  `-32010` is distinct from the standard JSON-RPC codes (`-32700` … `-32603`) and from
  tool-level errors, which return a normal result with `isError: true`. A `-32010` is an
  **authentication** failure, never a tool failure. The full code list is in the
  [error codes reference](/aiway/mcp/reference/error-codes).
</Callout>

## Origin validation [#origin-validation]

Beyond the shared HTTP server's CORS and origin middleware, MCP applies its **own**
origin check (`validateOrigin`) against `McpConfig.TrustedOrigins`. This guards the
endpoint against DNS-rebinding and cross-site requests from untrusted browser pages.

| `TrustedOrigins` value | Behavior                                                                                                          |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `auto` (default)       | Matches localhost variants — `localhost`, `127.0.0.1`, `::1`, `[::1]`, `0.0.0.0` — plus the server's bind address |
| `*`                    | Allows all origins                                                                                                |
| *(custom list)*        | Allows exactly the listed origins                                                                                 |

An **empty `Origin` header** — which non-browser clients such as `curl`, the MCP SDKs,
and server-to-server calls send — is **always allowed**. Only browser callers, which set
an `Origin`, are screened. Configure the list with `CONNECTORSMCP_TRUSTED_ORIGINS` (see
[Configuration](/aiway/mcp/configuration)); for example, to allow a single web
app:

```bash title="env.sh"
export CONNECTORSMCP_TRUSTED_ORIGINS=https://app.example.com
```

A rejected origin is refused before the JSON-RPC method runs.

## Production recommendations [#production-recommendations]

Authentication is off by default for convenience; turn it on before the endpoint is
reachable from anywhere untrusted.

* **Enable authentication** in any non-local deployment and require a Bearer token on
  every MCP call.
* **Use TLS** (`https://`) for all production MCP endpoints — see
  [Auth & Security → TLS and mTLS](/connectors/reference/auth-and-security#tls-and-mtls).
* **Prefer short-lived, rotated tokens** to limit the blast radius of a leaked JWT.
* **Restrict `TrustedOrigins`** to the exact web origins that need browser access instead
  of leaving `auto` or `*` in place.
* **Restrict network access** to the endpoint with firewall rules or Kubernetes network
  policies as defense in depth.

## Related [#related]

<Cards>
  <Card title="Auth & Security" href="/connectors/reference/auth-and-security" description="The shared JWT, CORS, origin-validation, and TLS model behind every connector." />

  <Card title="Configuration" href="/aiway/mcp/configuration" description="McpConfig fields, including ToolTimeoutSeconds and TrustedOrigins." />

  <Card title="Error codes" href="/aiway/mcp/reference/error-codes" description="The full JSON-RPC error catalog, including -32010 and isError semantics." />

  <Card title="Client setup" href="/aiway/mcp/guides/client-setup" description="Connect Claude Desktop and the MCP SDKs and complete the session handshake." />
</Cards>
