Cannot test MCP servers behind cookie-affinity load balancers: Set-Cookie is dropped, later requests hit replicas without the session

Đang mở
#2,936 2 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
15/100
Loại issue
Tính năng
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
node.js, typescript

Hướng nghiên cứu

Start with core/mcp/node/transport.ts and core/mcp/node/suppressNotificationStreamFetch.ts to understand the shared transport fetch chains, then trace how suppressNotificationStream is stored and exposed in server settings. The requested work spans both HTTP transports, persistence, API validation, settings UI, and unit and integration coverage. Done means cookie affinity works across a session when enabled, remains off by default, and the described tests and checks pass.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

enhancement v2
Which client?

All / shared core — the Node transport fetch chain in core/mcp/node/transport.ts is shared by the Web, CLI, and TUI clients.

The problem

I'm testing an MCP server deployed as multiple replicas behind a load balancer that routes by cookie affinity (in my case an nginx ingress in front of a multi-replica MCPHub-based proxy; the ingress does Set-Cookie: <affinity>=… on the first response and pins later requests carrying that cookie to the same replica).

The Inspector drops Set-Cookie entirely — the transport fetch chains never store or replay cookies — so:

  1. initialize lands on replica A, succeeds, and A hands out an Mcp-Session-Id (and the LB hands out an affinity cookie);
  2. every later request (notifications/initialized, tools/list, tools/call) carries no cookie, so the LB round-robins; requests landing on replica B/C return 404 Session not found;
  3. the connection is unusable: initialize succeeds and nothing else works.

This happens every time against this deployment shape — it is not intermittent, and both Streamable HTTP and legacy SSE servers behind such a balancer are affected. Related history: #2783 (a load-balancer-related SSE failure, closed without touching the cookie gap) and #1454 (the credentials: 'include' request, declined for v1 — the closing comment invited a v2 issue describing the workflow that cannot be completed, which is this one).

What I do instead today: test the server with Cherry Studio over stdio (bypasses the LB entirely), or manually copy the affinity cookie out of a curl session and set it as a static custom header per session — both defeat the purpose of a UI inspector.

Solution you have in mind (optional)

A per-server cookie jar toggle (default off), implemented as a connection-scoped fetch wrapper on both HTTP transports:

  • Store Set-Cookie from responses on the server's origin; replay as Cookie on later requests to the same origin only. OAuth requests to other origins are untouched — the existing SSRF/auth layers are unaffected.
  • Simplified host-only semantics: name=value + Max-Age/Expires expiry, same-name overwrite, bounded jar (50 entries, 4 KB per entry). Not a full RFC 6265 implementation — an affinity cookie needs nothing more.
  • One jar per connection: cookies never outlive the session, a reconnect re-learns affinity.
  • Persists as cookieJar in server settings (omit-on-false, mirroring suppressNotificationStream from #2317), validated in the settings API, and a checkbox in the server settings form.
  • The wrapper sits above the fetch tracker so the Network log records what was actually sent; Cookie/Set-Cookie are already in the log's redaction list.

This is one possible answer — the problem above is the request.

Alternatives or workarounds you have tried
  • Cherry Studio (stdio) — works but bypasses the HTTP deployment being tested.
  • Static Cookie custom header — requires capturing the cookie out-of-band first, breaks when the LB reassigns, impossible to discover before connecting.
  • Source-IP affinity on the LB instead of cookies — an infrastructure change outside my control in shared clusters.
Already built it locally?

Per CONTRIBUTING.md I have not opened a PR. I implemented the change locally (both Streamable HTTP and legacy SSE transports) and verified it.

Behavior before. With the setting off (the only behavior today), initialize succeeds, but every later request leaves without the affinity cookie; behind a cookie-affinity LB the session-bound requests round-robin, and replicas that do not hold the session answer 404 Session not found. The connection is unusable after initialize.

Behavior after. With the per-server cookieJar setting enabled, the transport's connection-scoped jar stores Set-Cookie from the server's origin and replays Cookie on every later same-origin request, so the whole session pins to the replica that holds it, and session-bound requests work end to end (verified with tools/list; the same replay carries every later request, tools/call included). With the setting off, both transport fetch chains are byte-identical to today's composition (asserted by tests).

How I verified it. All gates from clients/web/:

  • npm run check (prettier format:check + eslint --max-warnings 0 + build) — green.
  • npm test (unit) — 7,893 tests / 409 files, all passing, including 17 new tests for the jar wrapper (cookieJarFetch.test.ts) and 4 transport-wiring tests (transportCookieJar.test.ts, which also asserts the byte-identical default-off behavior).
  • npm run test:integration — 2,279 tests / 102 files, all passing.
  • npm run test:coverage — 10,172 tests / 511 files, passing the repo's uniform per-file gate (lines/statements/functions/branches ≥ 90 each). Overall: 98.75% statements, 96.69% branches, 98.81% functions, 99.13% lines. New/touched files:
File Stmts Branch Funcs Lines
core/mcp/node/cookieJarFetch.ts 100% 96.15% 100% 100%
core/mcp/node/transport.ts 100% 94.91% 100% 100%
core/mcp/serverList.ts 99.36% 99.03% 100% 100%
core/mcp/remote/node/server.ts 97.13% 93.42% 93.89% 97.73%
clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx 97.45% 97.74% 95.52% 97.43%
clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.tsx 95.65% 94.59% 93.75% 95.45%

(core/mcp/types.ts is excluded from the coverage report as a pure-type module by the repo's existing config; the change there is type-only.)

The motivating end-to-end test (clients/web/src/test/integration/mcp/cookie-jar-affinity.test.ts) stands up two single-pod MCP "replicas" behind an in-process round-robin proxy shaped like an nginx sticky ingress: the proxy stamps Set-Cookie: lb_affinity=<pod> on the first response, and each replica answers session-bound requests with 404 {"error":"Session not found"} unless they carry that replica's own affinity cookie. Both arms run Streamable HTTP with suppressNotificationStream: true, so the failure signal lands on session-bound tools/list:

  • Jar off (control): initialize succeeds and the proxy assigns a pod, but the fetch chain drops the Set-Cookie, so the tools/list POST round-robins to the other pod and is answered 404 {"error":"Session not found"}; listTools() rejects with exactly the SDK's rendering of that failure: Error POSTing to endpoint (HTTP 404): {"error":"Session not found"}.
  • Jar on: the jar stores lb_affinity from the initialize response and replays it on every subsequent same-origin request; the proxy pins the whole session to that one pod, and tools/list succeeds, returning ["ping"].

The exact prompt used to generate the change (reproduce it through your pipeline):

Implement a per-server "cookie jar" setting for the MCP Inspector (v2, base branch v2/main), default off, mirroring how suppressNotificationStream (#2317) is threaded through settings:

1. Add `cookieJar?: boolean` to both `InspectorServerSettings` and the inspector-specific body of the `StoredMCPServer` disk type in core/mcp/types.ts (`export type StoredMCPServer = MCPServerConfig & { … }` — the exact home of `suppressNotificationStream`, so persisted sse and streamable-http entries can both carry it; runtime gates the jar to the HTTP transports, and stdio ignores it). Give both declarations a doc comment explaining the use case (multi-replica deployments behind cookie-affinity load balancers: Set-Cookie must be stored and replayed or requests land on replicas without the MCP session, and the connection dies with "404 Session not found" right after a successful initialize).
2. Thread the field through core/mcp/serverList.ts exactly like suppressNotificationStream: StoredInspectorFields union, hasAny check, read side lifts only an explicit true (hand-edited junk values are dropped), write side omits when false, register in INSPECTOR_FIELD_KEY_MAP. Round-trip tests in serverList.test.ts.
3. Add core/mcp/node/cookieJarFetch.ts exporting createCookieJarFetch(baseFetch, serverUrl): a fetch wrapper with a connection-scoped jar. Same-origin only (store Set-Cookie from and replay Cookie onto requests whose origin matches serverUrl; never touch other origins, so OAuth discovery/token requests are unaffected). Simplified host-only RFC 6265: name=value, Max-Age (wins) then Expires expiry, same-name overwrite, malformed lines dropped, bounded at 50 entries / 4096 bytes per name=value pair, oldest evicted first. Follow the doc-comment density and style of suppressNotificationStreamFetch.ts.
4. Wire it into core/mcp/node/transport.ts for BOTH the Streamable HTTP branch and the legacy SSE branch when settings.cookieJar === true: on the Streamable HTTP path wrap fetchWithOptionalAuthIntercept so the jar sits above the fetch tracker but below the notification-header stamping; on the SSE path wrap trackedFetch and pass it as both the eventSourceInit GET-stream fetch and the transport's POST fetch, so an affinity cookie delivered on either response pins the whole session. The Network log must record the headers actually sent (#2385); Cookie/Set-Cookie are already redacted there. When the setting is off, both paths must be byte-identical to the pre-jar composition.
5. Validate the new field in the /api/servers settings route in core/mcp/remote/node/server.ts (non-boolean → 400 "settings.cookieJar must be a boolean"; normalize to carry only an explicit true), with integration tests next to the existing settings-route tests in clients/web/src/test/integration/mcp/remote/server-extra-coverage.test.ts (reject, persist, omit-when-off + read-back).
6. Add an "Enable HTTP Cookie Jar (sticky sessions)" checkbox to ServerSettingsForm shown only for streamable-http and sse server types, with a handler in ServerSettingsModal that omits the field when off, plus story and component tests mirroring the suppressNotificationStream ones.
7. Tests: unit tests for the wrapper (store/replay, merge onto an existing Cookie header, expiry via Max-Age and Expires with Max-Age precedence, same-name overwrite, size/count bounds, malformed input, cross-origin isolation, Request-object input, independent jars), transport wiring tests with a mocked fetchFn and the real SDK transports (assert the replayed Cookie header per transport, plus a default-off control arm asserting byte-identical fetches), and an integration test that stands up two single-pod MCP replicas behind an in-process round-robin affinity proxy (each replica answers session-bound requests with 404 "Session not found" without its own affinity cookie): without the jar listTools must fail with that error after a successful initialize, with it the session must pin to one pod and tools/list must succeed. Per-file coverage ≥ 90% on lines/statements/functions/branches; keep new dependencies at zero; English comments and UI copy; conventional commits.
Ngôn ngữ chính
TypeScript
Star
11.3k
Fork
1.6k
Merge trung bình
1 giờ 28 phút
Pull request đã merge (30 ngày)
138

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của modelcontextprotocol/inspector

Tất cả issue của modelcontextprotocol/inspector

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.