Skip to content

Fix legacy MCP session request isolation - #7337

Merged
Aias00 merged 4 commits into
apache:masterfrom
BobSong-dev:feat/mcp-request-isolation-independent
Sep 30, 2026
Merged

Aias00 merged 4 commits into
apache:masterfrom
BobSong-dev:feat/mcp-request-isolation-independent

Conversation

@BobSong-dev

@BobSong-dev BobSong-dev commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

What happened

Concurrent requests in the same legacy MCP session could reuse session-level transport state and associate an exchange or response with the wrong JSON-RPC request. This could cause missing results, mismatched IDs, or failures in one request affecting later requests.

Changes

  • Propagate the current request exchange through the MCP transport context for each session request.
  • Correlate responses by JSON-RPC request ID instead of clearing shared session state.
  • Remove the unsafe cached session queue behavior and clean response/session state on completion, error, and cancellation.
  • Keep the legacy callback holder as a compatibility fallback while preferring request-scoped context.
  • Add regression coverage for interleaved requests, cancellation isolation, error recovery, response cleanup, and callback context selection.

Verification

  • Windows JDK 17, Maven Wrapper, targeted MCP reactor: 11 modules, BUILD SUCCESS.
  • MCP server tests: 94/94 passed; failures 0, errors 0, skipped 0.
  • Checkstyle: 0 violations.
  • The branch is based directly on the current Apache master baseline and does not depend on the Agent Gateway PR or the separate AI Proxy cancellation change.

Master reconciliation

Merged current master without rewriting published history. Keep request-scoped transport context, per-ID business-response lookup and doFinally cleanup on completion/error/cancellation; preserve master's compatibility lookup and initialization-handshake slot. Reconcile the two competing response maps into one Object-ID map so compatibility lookup cannot retain a duplicate response after cleanup. Add a regression for cleanup through the compatibility getter and null-ID reset.

Local Maven wrapper: 50 targeted MCP transport/callback/plugin/handler tests passed, zero failures/errors/skips; Checkstyle and MCP Apache RAT passed. Expected negative-path SDK/reflection exception logs did not fail the tests. New head 9f7f043 is pushed; GitHub CI is running and not yet verified. No other open PR branch was merged.

Aias00
Aias00 previously approved these changes Sep 29, 2026

@Aias00 Aias00 left a comment

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.

Approving. Two independent pieces of per-session state were being shared across concurrent requests — the ServerWebExchange (one slot in ShenyuMcpExchangeHolder per session) and the captured response (one lastSentMessage slot per transport) — and this fixes both by keying each to the request instead of the session.

What I verified:

  • The fallback is genuinely preserved. getOriginExchange now tries mcpExchange.transportContext() first and only then ShenyuMcpExchangeHolder.get(sessionId), so transports that do not populate the context keep working. The two new unit tests pin both directions (context wins, holder still consulted) — good, because that ordering is the whole compatibility story.
  • Leaving the old single-slot isResponseReady() in place is correct, not an oversight. I checked both remaining callers: :880 is the initialize-handshake completion check and :973 is the "is this session initialised" heuristic. Both run during session creation, where there is exactly one request in flight, so they do not need per-id correlation. Scoping the new responses map to the concurrent business-request path is the right call.
  • doOnNext → doFinally matters more than it looks. A cancelled request previously left lastSentMessage populated, and that stale message is precisely what the next request on the session could read. Releasing on cancel and error, not just success, closes it.
  • removeSession marking closed = true is safe across threads — closed is volatile (:1059), and sendMessage checks it before capturing, so a discarded transport stops accumulating state while another thread is mid-send.

The tests are the right kind: testConcurrentRequestsKeepExchangeAndResponseCorrelation drives eight concurrent requests through one session with a per-request marker and asserts each response carries its own id and marker — that fails on master on both counts (every callback resolves to the last-bound exchange, and the single response slot mismatches ids), so it is a real regression test rather than a restatement of the implementation.

Non-blocking

  1. One map entry can outlive a cancelled request. doFinally removes responses.remove(messageId), but if the client cancels before the upstream response arrives, that remove is a no-op and the late response is then inserted and stays until removeSession or a full resetCapturedMessage(). It is one entry per cancelled in-flight request, bounded by session lifetime — but on a long-lived session with lots of client cancellations (users closing a tab mid-tool-call) it grows. Bounding the map, or recording cancelled ids so sendMessage can skip them, would close it.
  2. The fallback is now silent. The old CRITICAL: No exchange found ... ERROR was replaced by a debug line in processWithExistingSession. Since falling back to the session holder is exactly the behaviour that is wrong under concurrency, a WARN there would tell operators whether the context path is actually being taken in their deployment.
  3. Mono.defer(() -> session.handle(message)) before contextWrite is worth a comment: it guarantees the handler observes this request's context rather than anything captured at assembly time, and it is the sort of thing a later cleanup would remove.

BobSong-dev and others added 2 commits September 29, 2026 16:53
…olation-independent

# Conflicts:
#	shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java

@Aias00 Aias00 left a comment

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.

Re-reviewed: isolating legacy MCP session requests and reworking the streamable HTTP transport provider is a focused fix with tests on both sides. CI is green on this head — approving.

@Aias00
Aias00 merged commit c63fc2a into apache:master Sep 30, 2026
19 checks passed
@Aias00 Aias00 added this to the 2.7.2 milestone Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants