From 388bddec87a0d105d9b90a2d629236ee8fdac563 Mon Sep 17 00:00:00 2001 From: Brian Fox <878612+onematchfox@users.noreply.github.com> Date: Tue, 12 May 2026 09:40:02 +0200 Subject: [PATCH] feat(controller): make MCP stateless mode configurable via `KAGENT_MCP_STATELESS` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `StreamableHTTPHandler` stores sessions in memory and returns 404 for any `Mcp-Session-Id` it doesn't recognise. With multiple replicas, a request may land on a different replica than the one that created the session. `KAGENT_MCP_STATELESS=true` enables stateless mode, which skips session-ID validation and creates a temporary session per request, making any replica capable of handling any request. This enables use of the MCP when the network path does/can not provide sticky session routing based on the `Mcp-Session-Id` header. Server->client initiated streaming is disabled in stateless mode, but that is not needed here — both tools are request/response and A2A conversation continuity is carried in the `context_id` field of the `invoke_agent` input. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com> --- go/core/internal/mcp/mcp_handler.go | 7 ++++++- go/core/pkg/env/kagent.go | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/go/core/internal/mcp/mcp_handler.go b/go/core/internal/mcp/mcp_handler.go index 8182df6fb3..83543faabc 100644 --- a/go/core/internal/mcp/mcp_handler.go +++ b/go/core/internal/mcp/mcp_handler.go @@ -13,6 +13,7 @@ import ( authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" "github.com/kagent-dev/kagent/go/core/internal/version" "github.com/kagent-dev/kagent/go/core/pkg/auth" + "github.com/kagent-dev/kagent/go/core/pkg/env" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -102,11 +103,15 @@ func NewMCPHandler(kubeClient client.Client, a2aBaseURL string, authenticator au ) // Create HTTP handler + var httpOpts *mcpsdk.StreamableHTTPOptions + if env.KagentMCPStateless.Get() { + httpOpts = &mcpsdk.StreamableHTTPOptions{Stateless: true} + } handler.httpHandler = mcpsdk.NewStreamableHTTPHandler( func(*http.Request) *mcpsdk.Server { return server }, - nil, + httpOpts, ) return handler, nil diff --git a/go/core/pkg/env/kagent.go b/go/core/pkg/env/kagent.go index be8c62d417..5d158b2060 100644 --- a/go/core/pkg/env/kagent.go +++ b/go/core/pkg/env/kagent.go @@ -23,6 +23,16 @@ var ( ComponentController, ) + KagentMCPStateless = RegisterBoolVar( + "KAGENT_MCP_STATELESS", + false, + "When true, the MCP server operates in stateless mode (no session persistence). "+ + "Use when the network path does not provide sticky session routing based on the Mcp-Session-Id header. "+ + "Note: stateless mode disables server-initiated notifications; clients will not receive "+ + "resources/updated events.", + ComponentController, + ) + // Variables injected into agent pods (not read by the controller itself). KagentName = RegisterStringVar(