[GH-ISSUE #5445] [Bug] A one-element initialize batch replaces the active HTTP session #3306

Closed
opened 2026-10-31 00:38:59 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @coygeek on GitHub (Oct 20, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5445

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

MCP or agent integration

Observed behavior

With an active Streamable HTTP MCP session, a sessionless POST /mcp containing a one-element JSON-RPC array with initialize returns HTTP 200 and creates a replacement session. The original client's next binary_session call returns HTTP 404 with Session not found. The documented transport contract says initialize batches must be refused without disturbing the existing session.

An independent verification run reproduced this through the built CLI, with no target open.

Reproduction

The independent verifier ran this request sequence once, using isolated HOME/XDG/temp directories and a loopback listener. No analysis provider or target was needed.

  1. From the built source checkout, start node scripts/rea.mjs mcp --http --host 127.0.0.1 --port 0. Use the /mcp URL announced on stderr.

  2. Send the following JSON as a sessionless POST. Set Content-Type: application/json and Accept: application/json, text/event-stream on every POST. Save the returned Mcp-Session-Id as session A.

    {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"verification-client","version":"1"}}}
    
  3. With Mcp-Session-Id set to A, POST {"jsonrpc":"2.0","method":"notifications/initialized"}, then POST this tool request. The tool request returns HTTP 200.

    {"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"binary_session","arguments":{}}}
    
  4. Omit Mcp-Session-Id and POST the one-element batch below. It returns HTTP 200 and a distinct session B.

    [{"jsonrpc":"2.0","id":2,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"verification-client","version":"1"}}}]
    
  5. Repeat step 3's tool request with session A. It now returns HTTP 404 and error code -32001, Session not found. Sending the initialized notification and tool request on session B succeeds.

  6. Stop the server with SIGTERM and wait for its exit. The verification run observed exit code 143.

Expected behavior

A sessionless initialize array should receive a refusal, create no replacement session, and leave A's next tool call usable. A lone initialize object should retain its documented ability to replace the existing session.

At source revision 074768901f3cdc8205b61773fb1faf6f6c7b3ca8, docs/mcp-contracts.md:573-590 says only a lone initialize can replace an open session and a batch carrying one is left to the transport's refusal. The batch-preservation test in src/server/http/serveStreamableHttp.test.ts:796-818 expresses the same expectation for a two-element batch.

Impact and scope

The original client's active HTTP session becomes unusable after this specific sessionless request. The failure occurred once in the independent run. The opt-in HTTP endpoint is documented as trusted and single-client, without authentication or independent target sessions; this report does not claim a security boundary crossing.

No target was open. In-flight cancellation, analysis data loss, provider restart, and interrupted-analysis recovery were not tested. A new client could use the replacement session, but no workaround preserving the original session was verified. Ordinary tool calls, lone-object replacement, and the refusal controls below remained functional.

REA version

6.7.0, source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8; built CLI with locked dependencies and MCP SDK packages 2.3.1.

Runtime and platform

Node v26.12.0; macOS / Darwin arm64. No Hopper or Ghidra target was opened.

Target and artifact context

No target or artifact. Loopback Streamable HTTP MCP listener with no active analysis.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

The independent verifier recorded these responses from the built CLI:

lone initialize A: 200; initialized: 202; A tool before batch: 200
sessionless one-element initialize batch: 200, distinct session B
A tool after batch: 404
{"jsonrpc":"2.0","error":{"code":-32001,"message":"Session not found"},"id":null}
B initialized: 202; B tool after batch: 200

Controls actually run in the same verification:

  • A sessionless two-element array containing initialize and notifications/initialized returned HTTP 400 with Invalid Request: Server already initialized. B's next tool call remained HTTP 200.
  • A sessionless lone initialize object returned HTTP 200 and replaced B with C, as documented. B's next call returned HTTP 404; initialized/tool calls on C succeeded.
  • A one-element initialize array carrying C's current session ID returned HTTP 400. C's next tool call remained HTTP 200.
  • Exactly two MCP HTTP session replaced by a new client log entries accompanied the one-element batch and deliberate lone-object replacement.
  • npm run test:local -- src/server/http/serveStreamableHttp.test.ts passed all 62 tests despite the direct CLI failure for the one-element batch.

Typed outcome or recovery expected

Refuse the sessionless initialize array with a 4xx response, issue no replacement session ID, and keep the current session usable. Preserve the existing lone-object replacement behavior and two-element batch refusal.

Analysis or suspected cause

Source-derived reasoning: isInitializePost in src/server/http/serveStreamableHttp.ts:201-216 treats a one-element initialize array as a replacement request. The session handler at lines 338-356 closes the old transport before dispatching to a fresh transport. The pinned SDK accepts the one-message initialize array on that fresh transport. The existing batch-refusal test covers a two-element array only.

Additional context

This report was prepared with AI assistance and independently reproduced. Before submission, upstream main 052956af3416fa01d93837e54fdec3ad2739f6d1 still had unchanged HTTP implementation and tests; the only intervening change to the cited contract document adds an unrelated native-function-matching link.

NOT_ENOUGH_INFO: No last-known-good release or first-bad release was established. Other operating systems and in-flight analysis were not tested.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5445 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area MCP or agent integration ### Observed behavior With an active Streamable HTTP MCP session, a sessionless `POST /mcp` containing a one-element JSON-RPC array with `initialize` returns HTTP 200 and creates a replacement session. The original client's next `binary_session` call returns HTTP 404 with `Session not found`. The documented transport contract says initialize batches must be refused without disturbing the existing session. An independent verification run reproduced this through the built CLI, with no target open. ### Reproduction The independent verifier ran this request sequence once, using isolated HOME/XDG/temp directories and a loopback listener. No analysis provider or target was needed. 1. From the built source checkout, start `node scripts/rea.mjs mcp --http --host 127.0.0.1 --port 0`. Use the `/mcp` URL announced on stderr. 2. Send the following JSON as a sessionless POST. Set `Content-Type: application/json` and `Accept: application/json, text/event-stream` on every POST. Save the returned `Mcp-Session-Id` as session A. ```json {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"verification-client","version":"1"}}} ``` 3. With `Mcp-Session-Id` set to A, POST `{"jsonrpc":"2.0","method":"notifications/initialized"}`, then POST this tool request. The tool request returns HTTP 200. ```json {"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"binary_session","arguments":{}}} ``` 4. Omit `Mcp-Session-Id` and POST the one-element batch below. It returns HTTP 200 and a distinct session B. ```json [{"jsonrpc":"2.0","id":2,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"verification-client","version":"1"}}}] ``` 5. Repeat step 3's tool request with session A. It now returns HTTP 404 and error code `-32001`, `Session not found`. Sending the initialized notification and tool request on session B succeeds. 6. Stop the server with SIGTERM and wait for its exit. The verification run observed exit code 143. ### Expected behavior A sessionless initialize array should receive a refusal, create no replacement session, and leave A's next tool call usable. A lone initialize object should retain its documented ability to replace the existing session. At source revision `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`, `docs/mcp-contracts.md:573-590` says only a lone initialize can replace an open session and a batch carrying one is left to the transport's refusal. The batch-preservation test in `src/server/http/serveStreamableHttp.test.ts:796-818` expresses the same expectation for a two-element batch. ### Impact and scope The original client's active HTTP session becomes unusable after this specific sessionless request. The failure occurred once in the independent run. The opt-in HTTP endpoint is documented as trusted and single-client, without authentication or independent target sessions; this report does not claim a security boundary crossing. No target was open. In-flight cancellation, analysis data loss, provider restart, and interrupted-analysis recovery were not tested. A new client could use the replacement session, but no workaround preserving the original session was verified. Ordinary tool calls, lone-object replacement, and the refusal controls below remained functional. ### REA version 6.7.0, source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`; built CLI with locked dependencies and MCP SDK packages 2.3.1. ### Runtime and platform Node v26.12.0; macOS / Darwin arm64. No Hopper or Ghidra target was opened. ### Target and artifact context No target or artifact. Loopback Streamable HTTP MCP listener with no active analysis. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence The independent verifier recorded these responses from the built CLI: ```text lone initialize A: 200; initialized: 202; A tool before batch: 200 sessionless one-element initialize batch: 200, distinct session B A tool after batch: 404 {"jsonrpc":"2.0","error":{"code":-32001,"message":"Session not found"},"id":null} B initialized: 202; B tool after batch: 200 ``` Controls actually run in the same verification: - A sessionless two-element array containing initialize and `notifications/initialized` returned HTTP 400 with `Invalid Request: Server already initialized`. B's next tool call remained HTTP 200. - A sessionless lone initialize object returned HTTP 200 and replaced B with C, as documented. B's next call returned HTTP 404; initialized/tool calls on C succeeded. - A one-element initialize array carrying C's current session ID returned HTTP 400. C's next tool call remained HTTP 200. - Exactly two `MCP HTTP session replaced by a new client` log entries accompanied the one-element batch and deliberate lone-object replacement. - `npm run test:local -- src/server/http/serveStreamableHttp.test.ts` passed all 62 tests despite the direct CLI failure for the one-element batch. ### Typed outcome or recovery expected Refuse the sessionless initialize array with a 4xx response, issue no replacement session ID, and keep the current session usable. Preserve the existing lone-object replacement behavior and two-element batch refusal. ### Analysis or suspected cause Source-derived reasoning: `isInitializePost` in `src/server/http/serveStreamableHttp.ts:201-216` treats a one-element initialize array as a replacement request. The session handler at lines 338-356 closes the old transport before dispatching to a fresh transport. The pinned SDK accepts the one-message initialize array on that fresh transport. The existing batch-refusal test covers a two-element array only. ### Additional context This report was prepared with AI assistance and independently reproduced. Before submission, upstream main `052956af3416fa01d93837e54fdec3ad2739f6d1` still had unchanged HTTP implementation and tests; the only intervening change to the cited contract document adds an unrelated native-function-matching link. NOT_ENOUGH_INFO: No last-known-good release or first-bad release was established. Other operating systems and in-flight analysis were not tested.
Author
Owner

@Yashasm18 commented on GitHub (Oct 20, 2026):

I'd like to work on this one. I can reproduce it from the steps above and have a fix in progress with a regression test for the one-element initialize batch. PR coming shortly.

<!-- gh-comment-id:6289870821 --> @Yashasm18 commented on GitHub (Oct 20, 2026): I'd like to work on this one. I can reproduce it from the steps above and have a fix in progress with a regression test for the one-element initialize batch. PR coming shortly.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Sessionless initialize arrays are explicitly classified as batch before session handover, independent of array length, and the HTTP handler returns a refusal before replacing a transport. The regression sends one- and two-element arrays while a client is active and requires 4xx plus successful subsequent client echo; another test refuses an array before opening the first session. This tests the reported eviction contract, not just parsing.

Implementation: serveStreamableHttp.ts, serveStreamableHttp.ts.

The relevant regression checks in serveStreamableHttp.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380779016 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Sessionless initialize arrays are explicitly classified as batch before session handover, independent of array length, and the HTTP handler returns a refusal before replacing a transport. The regression sends one- and two-element arrays while a client is active and requires 4xx plus successful subsequent client echo; another test refuses an array before opening the first session. This tests the reported eviction contract, not just parsing. Implementation: [serveStreamableHttp.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/server/http/serveStreamableHttp.ts#L217), [serveStreamableHttp.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/server/http/serveStreamableHttp.ts#L343). The relevant regression checks in [serveStreamableHttp.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/server/http/serveStreamableHttp.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3306
No description provided.