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:
- Endpoint —
POST /mcp(and theGET /mcpkeepalive stream) carry every JSON-RPC method:initialize,tools/list,tools/call, andping. A verified token identifies the caller; thatClientIDflows through to the broker and, for the agent-bridge tools, to the agent asX-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.closeuse 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"
}
}| 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 -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 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); for example, to allow a single web
app:
export CONNECTORSMCP_TRUSTED_ORIGINS=https://app.example.comA 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
TrustedOriginsto the exact web origins that need browser access instead of leavingautoor*in place. - Restrict network access to the endpoint with firewall rules or Kubernetes network policies as defense in depth.
Related
Auth & Security
The shared JWT, CORS, origin-validation, and TLS model behind every connector.
Configuration
McpConfig fields, including ToolTimeoutSeconds and TrustedOrigins.
Error codes
The full JSON-RPC error catalog, including -32010 and isError semantics.
Client setup
Connect Claude Desktop and the MCP SDKs and complete the session handshake.
Was this page helpful?
Client Setup
Connect any MCP client to KubeMQ — Claude Desktop config, generic JSON-RPC over HTTP, session headers, and the official SDK in nine languages.
Channel Resolution
How MCP tools map to KubeMQ channels — naming, the five channel types, the reserved _AGENTS_ prefix, and discovery via channel_list and channel_info.