Add E2E test for MCPServer cross-replica session routing with Redis#4525
Add E2E test for MCPServer cross-replica session routing with Redis#4525
Conversation
MCPServer supports horizontal scaling with Redis session storage, but there was no E2E test verifying that a session established on one pod is accessible from a different pod. This test deploys an MCPServer with replicas=2 and Redis session storage, initializes an MCP session, then sends raw JSON-RPC requests directly to each pod IP using the same Mcp-Session-Id header to prove sessions are shared via Redis. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4525 +/- ##
==========================================
- Coverage 69.06% 68.91% -0.16%
==========================================
Files 502 505 +3
Lines 51997 52382 +385
==========================================
+ Hits 35913 36098 +185
- Misses 13300 13494 +194
- Partials 2784 2790 +6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Pod IPs are not reachable from the CI runner host in Kind clusters. Replace direct pod IP HTTP calls with kubectl port-forward to each pod, which tunnels through the Kind node's network. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The MCPServer CRD's sessionStorage config was populated by the operator into RunConfig but the proxy runner never read it — sessions always used in-memory LocalStorage, making cross-replica routing non-functional. Add WithSessionStorage transport option and wire ScalingConfig.SessionRedis from RunConfig into the transport layer so both StdioTransport and HTTPTransport (transparent proxy) use Redis-backed session storage when configured. Rewrite the E2E test to use mcp-go clients throughout, including transport.WithSession to create a client on pod B that reuses the session established on pod A. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pass SessionStorage through types.Config instead of a factory option with interface assertion. The factory now sets the field directly on each transport type during construction. Add clientA to codespell ignore list. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes cross-replica MCP session routing for horizontally scaled MCPServer deployments by ensuring the proxy runner honors ScalingConfig.SessionRedis and injects Redis-backed session storage into the transport/proxy layer. It also adds an E2E regression test that proves a session created on one pod can be used from another pod when Redis session storage is configured.
Changes:
- Add
SessionStoragetopkg/transport/types.Configand plumb it through transport creation so HTTP and stdio paths can share sessions via Redis. - Update the proxy runner to construct a Redis session store from
ScalingConfig.SessionRedisand inject it into the transport config. - Add a Kubernetes E2E test that deploys MCPServer with
replicas=2+ Redis and validates session reuse across two distinct pods.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
test/e2e/thv-operator/virtualmcp/mcpserver_scaling_test.go |
New E2E test deploying Redis + MCPServer(2 replicas) and validating cross-pod session reuse via port-forward. |
pkg/transport/types/transport.go |
Adds Config.SessionStorage to allow overriding the default in-memory session storage. |
pkg/transport/http.go |
Adds sessionStorage field and forwards it into the transparent proxy via transparent.WithSessionStorage(...). |
pkg/transport/factory.go |
Wires Config.SessionStorage into stdio and HTTP transports during factory creation. |
pkg/runner/runner.go |
Builds Redis session storage from ScalingConfig.SessionRedis and injects it into the transport config. |
.codespellrc |
Extends codespell ignore words list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Move Redis image to test/e2e/images/images.go (RedisImage constant) - Move RedisPasswordEnvVar to pkg/transport/session/redis_config.go to avoid proxy runner depending on pkg/vmcp/config - Remove unused vmcpconfig import from runner.go Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
yrobla
left a comment
There was a problem hiding this comment.
Implementation is correct — the Redis wiring, factory plumbing, and E2E test structure all look solid. Two nits worth a follow-up but nothing blocking.
| config.TrustProxyHeaders, | ||
| config.Middlewares..., | ||
| ) | ||
| httpTransport.sessionStorage = config.SessionStorage |
There was a problem hiding this comment.
Nit: HTTPTransport wires session storage via direct unexported field assignment, while StdioTransport uses stdio.SetSessionStorage(...) (a method call). Consider adding a SetSessionStorage method to HTTPTransport to keep the two transports consistent and avoid the factory reaching into unexported fields.
| defer cleanupB() | ||
|
|
||
| ginkgo.By("Initializing a session on pod A") | ||
| clientA, err := CreateInitializedMCPClient(int32(localPortA), "e2e-cross-pod-test", 30*time.Second) |
There was a problem hiding this comment.
Nit: clientA had to be added to the global .codespellrc ignore list to silence a false positive. Renaming to something like podAClient removes the need for that global exception and keeps the spell-checker effective for the rest of the codebase.
Summary
ScalingConfig.SessionRedisconfig — sessions always used in-memoryLocalStorage, making cross-replica routing non-functional.ScalingConfig.SessionRedisfrom RunConfig into the transport layer viatypes.Config.SessionStorage, so bothStdioTransportandHTTPTransport(transparent proxy) use Redis-backed session storage when configured.replicas=2and Redis, initializes a session on pod A via mcp-go, then creates a second mcp-go client on pod B withtransport.WithSessionreusing the same session ID — proving sessions are shared via Redis across replicas.Fixes #4531
Type of change
Test plan
task test)task test-e2e)task lint-fix)Verified locally on a Kind cluster: deployed MCPServer with 2 replicas + Redis, confirmed cross-pod session routing via kubectl port-forward to each pod individually. Also validated with raw curl: Initialize on pod A, tools/list on pod B with same Mcp-Session-Id.
Changes
pkg/transport/types/transport.goSessionStoragefield toConfigpkg/transport/factory.goConfig.SessionStorageto transports during constructionpkg/transport/http.gosessionStoragefield, pass to transparent proxy inStart()pkg/transport/session/redis_config.goRedisPasswordEnvVarconstant (moved frompkg/vmcp/config)pkg/runner/runner.goScalingConfig.SessionRedisand set on transport configtest/e2e/images/images.goRedisImageconstanttest/e2e/thv-operator/virtualmcp/mcpserver_scaling_test.goSpecial notes for reviewers
StdioTransport.SetSessionStoragealready existed but was never called. The factory now sets session storage directly on bothStdioTransportandHTTPTransportduring construction viaConfig.SessionStorage.transport.WithSession(sessionID)from mcp-go to create a client on pod B that reuses pod A's session — this is the cleanest way to prove cross-pod session access.HTTPTransport.Startgained anolint:gocyclo— it's a candidate for refactoring but keeping this PR focused on the Redis wiring.RedisPasswordEnvVarwas moved frompkg/vmcp/configtopkg/transport/sessionto avoid a proxy runner dependency on the vMCP config package. The vMCP side should be updated to use the shared constant in a follow-up.Generated with Claude Code