Skip to content

Add E2E test for MCPServer cross-replica session routing with Redis#4525

Open
jerm-dro wants to merge 5 commits intomainfrom
e2e-mcpserver-cross-replica-session-test
Open

Add E2E test for MCPServer cross-replica session routing with Redis#4525
jerm-dro wants to merge 5 commits intomainfrom
e2e-mcpserver-cross-replica-session-test

Conversation

@jerm-dro
Copy link
Copy Markdown
Contributor

@jerm-dro jerm-dro commented Apr 3, 2026

Summary

  • MCPServer supports horizontal scaling with Redis session storage, but the proxy runner never read the ScalingConfig.SessionRedis config — sessions always used in-memory LocalStorage, making cross-replica routing non-functional.
  • Wire ScalingConfig.SessionRedis from RunConfig into the transport layer via types.Config.SessionStorage, so both StdioTransport and HTTPTransport (transparent proxy) use Redis-backed session storage when configured.
  • Add an E2E test that deploys MCPServer with replicas=2 and Redis, initializes a session on pod A via mcp-go, then creates a second mcp-go client on pod B with transport.WithSession reusing the same session ID — proving sessions are shared via Redis across replicas.

Fixes #4531

Type of change

  • New feature

Test plan

  • Unit tests (task test)
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)
  • Manual testing (describe below)

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

File Change
pkg/transport/types/transport.go Add SessionStorage field to Config
pkg/transport/factory.go Pass Config.SessionStorage to transports during construction
pkg/transport/http.go Add sessionStorage field, pass to transparent proxy in Start()
pkg/transport/session/redis_config.go Add RedisPasswordEnvVar constant (moved from pkg/vmcp/config)
pkg/runner/runner.go Build Redis session storage from ScalingConfig.SessionRedis and set on transport config
test/e2e/images/images.go Add RedisImage constant
test/e2e/thv-operator/virtualmcp/mcpserver_scaling_test.go New E2E test for cross-replica session routing

Special notes for reviewers

  • StdioTransport.SetSessionStorage already existed but was never called. The factory now sets session storage directly on both StdioTransport and HTTPTransport during construction via Config.SessionStorage.
  • The E2E test uses kubectl port-forward to deterministically target specific pods rather than relying on round-robin through a Service.
  • The test uses 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.Start gained a nolint:gocyclo — it's a candidate for refactoring but keeping this PR focused on the Redis wiring.
  • RedisPasswordEnvVar was moved from pkg/vmcp/config to pkg/transport/session to 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

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>
@github-actions github-actions bot added the size/M Medium PR: 300-599 lines changed label Apr 3, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 0% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.91%. Comparing base (2e8e487) to head (4d2ab58).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
pkg/runner/runner.go 0.00% 19 Missing ⚠️
pkg/transport/factory.go 0.00% 11 Missing ⚠️
pkg/transport/http.go 0.00% 1 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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>
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Apr 3, 2026
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>
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Apr 3, 2026
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>
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Apr 3, 2026
@jerm-dro jerm-dro requested a review from Copilot April 3, 2026 19:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 SessionStorage to pkg/transport/types.Config and 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.SessionRedis and 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.

JAORMX
JAORMX previously approved these changes Apr 3, 2026
- 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>
@jerm-dro jerm-dro requested a review from rdimitrov as a code owner April 3, 2026 20:53
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Apr 3, 2026
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Apr 3, 2026
@jerm-dro jerm-dro requested a review from JAORMX April 3, 2026 20:55
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Apr 3, 2026
Copy link
Copy Markdown
Contributor

@yrobla yrobla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire Redis session storage from RunConfig into MCPServer proxy runner transports

4 participants