Cannot test MCP servers behind cookie-affinity load balancers: Set-Cookie is dropped, later requests hit replicas without the session
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
- Lĩnh vực
- backend-api-design, devtools
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ả
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:
initializelands on replica A, succeeds, and A hands out anMcp-Session-Id(and the LB hands out an affinity cookie);- every later request (
notifications/initialized,tools/list,tools/call) carries no cookie, so the LB round-robins; requests landing on replica B/C return404 Session not found; - the connection is unusable:
initializesucceeds 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-Cookiefrom responses on the server's origin; replay asCookieon 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/Expiresexpiry, 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
cookieJarin server settings (omit-on-false, mirroringsuppressNotificationStreamfrom #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-Cookieare 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
Cookiecustom 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):
initializesucceeds and the proxy assigns a pod, but the fetch chain drops theSet-Cookie, so thetools/listPOST round-robins to the other pod and is answered404 {"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_affinityfrom theinitializeresponse and replays it on every subsequent same-origin request; the proxy pins the whole session to that one pod, andtools/listsucceeds, 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
- Có Dockerfile hoặc tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của modelcontextprotocol/inspector
-
bug v2
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 68/100
modelcontextprotocol/inspector#2935 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug v2
Độ khó 4/5 1-2 ngày Mức phù hợp với người mới 48/100
modelcontextprotocol/inspector#2934 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
chore v2
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 53/100
modelcontextprotocol/inspector#2922 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Document load testing and Active Sessions on modelcontextprotocol.ioCó thể đã có người làm @cliffhall đã nhận 6 ngày trước. Đang mởdocumentation v2 waiting-on-docs
modelcontextprotocol/inspector#2869 · 2 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
CLI: OAuth denials and other non-usage failures exit 1 with code "error", the same as a usage errorĐang mởbug v2
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 68/100
modelcontextprotocol/inspector#2842 · 5 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của modelcontextprotocol/inspector
Issue tương tự
-
[Bug]: t3 connect status reports "Exposure: enabled" for publish-only links without a tunnelĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 83/100
awslabs/visual-asset-management-system#1278 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
mifi/lossless-cut#3213 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 83/100
plasma-umass/scalene#1123 ·
Maintainer thường phản hồi trong vòng 1 ngày