KubeMQ
AiwayMCPGuides

Authentication

Secure the MCP connector with JWT Bearer tokens — attach the Authorization header to your MCP client, handle -32010 errors, and configure origin validation.

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

Authentication for MCP is the connector-wide model described in Auth & Security, applied to the single MCP endpoint:

  • EndpointPOST /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, 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.

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.

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.

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

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.

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"}'
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}");
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))
}
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();
    }
}
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}")
}
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())
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
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(())
}
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)")
    }
}
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();

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.

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.

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32010,
    "message": "Authentication failed"
  }
}
ConditionResult
Auth disabled (default)Request runs as the anonymous principal
Auth enabled, valid tokenRequest runs as the token's ClientID
Auth enabled, missing or invalid tokenJSON-RPC error -32010

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

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 valueBehavior
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); for example, to allow a single web app:

env.sh
export CONNECTORSMCP_TRUSTED_ORIGINS=https://app.example.com

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

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.
  • 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.

Was this page helpful?

On this page