[GH-ISSUE #7682] [Bug] Stdio frame validation errors advise restarting a still-connected MCP client #4605

Closed
opened 2026-10-31 00:51:13 +00:00 by bsakaguchi · 0 comments
Owner

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

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

After initializing node scripts/rea.mjs mcp, sending {"jsonrpc":"2.0","id":9007199254740992,"method":"ping"} produces stderr advice that REA lost its client connection and the client should be restarted. No response arrives within two seconds. A subsequent ping with ID 3 succeeds on the same process and connection.

The fractional-ID control {"jsonrpc":"2.0","id":1.5,"method":"ping"} produces the same advice and missing reply. IDs 0, -1, "9007199254740992", and "fraction-control" all receive normal results afterward. Each rejected frame produces one JSON log and one plain stderr line. Closing stdin exits the server with status 0. This report concerns the incorrect connection-loss diagnosis and recovery advice; the missing replies are produced by the dependency's frame validator.

Reproduction

  1. Use source commit 65f1c447344a423fa2785847dbcea1e9a9e2454a with its locked dependencies and built runtime. Verification used the prebuilt checkout without modifying it.
  2. Run the following from its root with Node 24.22.1 on PATH. The inline fixtures refuse package-manager work, and each server gets isolated HOME/XDG directories. The probe initializes, sends both numeric cases, checks the still-open connection, records stderr and exit status, and cleans up its directories.
python3 -I - <<'PY'
import json, os, pathlib, queue, shutil, subprocess, tempfile, threading

root = pathlib.Path(tempfile.mkdtemp(prefix="rea-id-verification-"))
node = shutil.which("node")
assert node, "Put Node 24.22.1 on PATH"
fixtures = root / "bin"
fixtures.mkdir()
for name in ("npm", "npx", "bun"):
    path = fixtures / name
    path.write_text('#!/bin/sh\nif [ "$1" = "--version" ]; then echo fixture-1; exit 0; fi\nexit 127\n')
    path.chmod(0o755)
base = {k: v for k, v in os.environ.items()
        if not k.startswith(("REA_", "HOPPER_", "GHIDRA_", "JAVA_", "JDK_", "XDG_"))}
base["PATH"] = str(fixtures) + os.pathsep + str(pathlib.Path(node).parent) + os.pathsep + base["PATH"]
try:
    for run in range(1, 4):
        home = root / str(run)
        home.mkdir()
        env = dict(base, HOME=str(home), XDG_CONFIG_HOME=str(home / "config"),
                   XDG_CACHE_HOME=str(home / "cache"), XDG_DATA_HOME=str(home / "data"),
                   XDG_STATE_HOME=str(home / "state"), npm_config_prefix=str(home / "npm"))
        child = subprocess.Popen([node, "scripts/rea.mjs", "mcp"], env=env,
                                 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE, text=True, bufsize=1)
        replies, errors = queue.Queue(), []
        def read_stdout():
            for line in child.stdout:
                replies.put(json.loads(line))
        def read_stderr():
            for line in child.stderr:
                errors.append(line.rstrip("\n"))
        readers = [threading.Thread(target=read_stdout), threading.Thread(target=read_stderr)]
        for reader in readers:
            reader.start()
        def send(value):
            child.stdin.write(json.dumps(value) + "\n")
            child.stdin.flush()
        def receive(timeout):
            try:
                return replies.get(timeout=timeout)
            except queue.Empty:
                return None
        try:
            send({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
                "protocolVersion": "2025-06-18", "capabilities": {},
                "clientInfo": {"name": "id-verifier", "version": "1"}}})
            initialized = receive(15)
            assert initialized and initialized.get("result"), initialized
            send({"jsonrpc": "2.0", "method": "notifications/initialized"})
            results = []
            for request_id in [9007199254740992, 1.5, 3, 0, -1,
                               "9007199254740992", "fraction-control"]:
                send({"jsonrpc": "2.0", "id": request_id, "method": "ping"})
                results.append({"id": request_id, "reply": receive(2)})
            child.stdin.close()
            child.wait(timeout=10)
        finally:
            if child.poll() is None:
                child.kill()
                child.wait()
            for reader in readers:
                reader.join()
        print(json.dumps({"run": run, "replies": results, "stderr": errors,
                          "exit": child.returncode, "extra_stdout": list(replies.queue)}))
finally:
    shutil.rmtree(root)
PY
  1. Observed in all three sessions: no response for either numeric case, successful subsequent controls, four stderr lines carrying the connection-loss advice, no extra stdout, and exit 0.

Expected behavior

A frame validation failure on a connection that continues to answer requests should not tell the caller that the connection was lost or direct them to restart it. REA's initialization instructions make the restart ladder conditional on a dropped connection. The diagnostic should distinguish a rejected message from actual transport loss.

Under JSON-RPC 2.0, Number IDs are permitted and fractional parts are discouraged. The negotiated MCP 2025-06-18 base protocol requires string or integer IDs: 1.5 is therefore an invalid MCP control. That does not establish a lost connection. The large integer is exactly representable, although outside the SDK validator's safe-integer range. Accepting these frames or supplying their missing responses is outside this report's requested diagnostic correction.

Impact and scope

P3: REA gives an incorrect failure classification and recovery instruction. No actual client restart, target loss, changed analysis result or host timeout was exercised. The observed connection continues to serve requests. Both tested string IDs work without a restart. Verification covers target-free stdio on macOS; HTTP and other platforms were not tested.

REA version

Exact node scripts/rea.mjs --version output:

6.8.0

Source commit: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS, darwin arm64. Locked MCP server SDK 2.3.1. No analysis provider was required.

Target and artifact context

No target or artifact; initialized stdio sessions and target-free ping requests only.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

For each numeric case, stderr includes this standalone line and a JSON logger record with the same msg:

Retry the tool call once only when it is safe to repeat or known not to have started. If it may already have changed the target, or it fails again, tell the user REA lost its MCP client connection and ask them to restart the MCP client.

The subsequent control response on every connection is:

{"result":{},"jsonrpc":"2.0","id":3}

Source checked at the reported baseline:

  • src/main.ts:29-39,108-115 connects this public stdio entry to SDK serving and the deferred runtime.
  • src/main/deferredRuntime.ts:217-222 ignores the error argument and emits the fixed connection-loss message through both logging and stderr. src/main/messages.ts:1-2 owns that text. src/main/transport.ts:153-157 contains the equivalent mapping in the other runtime.
  • src/server/recoveryInstructions.ts:4 conditions the advertised retry/restart ladder on connection loss.
  • The locked SDK's node_modules/@modelcontextprotocol/server/dist/stdio.mjs:78-86,562-564 catches frame-validation exceptions, forwards them to the error callback, and continues reading. Its dist/src-Cqbh3MYc.mjs:7195-7196 parses inbound lines with the message schema.
  • docs/mcp-contracts.md:829-833 describes HTTP listener errors; it is not a stdio request-ID acceptance contract. Its tool-argument normalization discussion does not cover frames rejected before tool dispatch.

Typed outcome or recovery expected

Keep actual connection-loss recovery for confirmed loss. For a rejected frame on a live connection, report the validation category and useful corrective guidance without telling the caller to restart. This does not require echoing raw untrusted frames or changing the SDK's accepted ID range.

Analysis or suspected cause

Source-confirmed ownership split: the SDK rejects the incoming frame before REA request handlers receive it. Its exported deserializeMessage independently raises ZodError for both numeric cases; the large integer produces an ID validation issue with maximum 9007199254740991. The error callback receives an error rather than the rejected raw frame or correlated ID.

REA then replaces that error with its own connection-loss text. REA can correct this classification and recovery independently of the upstream parser. This report does not attribute the missing JSON-RPC responses to a REA tool handler or propose bypassing protocol validation.

Additional context

Open and closed issue searches, issue comments, and all-state PR searches found no duplicate of the live-connection validation diagnostic. Reviewed related issues include #3509, #3221, #7372, #7438, #4885, #6454 and #7346. Merged PR #6353 updates the recovery ladder for connection loss; it does not distinguish nonfatal frame errors. Issue #7346 and PR #7367 concern HTTP bind-failure recovery, a different trigger and restoration check. No last-known-good version was established.

Originally created by @coygeek on GitHub (Oct 28, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7682 ### 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 After initializing `node scripts/rea.mjs mcp`, sending `{"jsonrpc":"2.0","id":9007199254740992,"method":"ping"}` produces stderr advice that REA lost its client connection and the client should be restarted. No response arrives within two seconds. A subsequent ping with ID `3` succeeds on the same process and connection. The fractional-ID control `{"jsonrpc":"2.0","id":1.5,"method":"ping"}` produces the same advice and missing reply. IDs `0`, `-1`, `"9007199254740992"`, and `"fraction-control"` all receive normal results afterward. Each rejected frame produces one JSON log and one plain stderr line. Closing stdin exits the server with status `0`. This report concerns the incorrect connection-loss diagnosis and recovery advice; the missing replies are produced by the dependency's frame validator. ### Reproduction 1. Use source commit `65f1c447344a423fa2785847dbcea1e9a9e2454a` with its locked dependencies and built runtime. Verification used the prebuilt checkout without modifying it. 2. Run the following from its root with Node 24.22.1 on PATH. The inline fixtures refuse package-manager work, and each server gets isolated HOME/XDG directories. The probe initializes, sends both numeric cases, checks the still-open connection, records stderr and exit status, and cleans up its directories. ```sh python3 -I - <<'PY' import json, os, pathlib, queue, shutil, subprocess, tempfile, threading root = pathlib.Path(tempfile.mkdtemp(prefix="rea-id-verification-")) node = shutil.which("node") assert node, "Put Node 24.22.1 on PATH" fixtures = root / "bin" fixtures.mkdir() for name in ("npm", "npx", "bun"): path = fixtures / name path.write_text('#!/bin/sh\nif [ "$1" = "--version" ]; then echo fixture-1; exit 0; fi\nexit 127\n') path.chmod(0o755) base = {k: v for k, v in os.environ.items() if not k.startswith(("REA_", "HOPPER_", "GHIDRA_", "JAVA_", "JDK_", "XDG_"))} base["PATH"] = str(fixtures) + os.pathsep + str(pathlib.Path(node).parent) + os.pathsep + base["PATH"] try: for run in range(1, 4): home = root / str(run) home.mkdir() env = dict(base, HOME=str(home), XDG_CONFIG_HOME=str(home / "config"), XDG_CACHE_HOME=str(home / "cache"), XDG_DATA_HOME=str(home / "data"), XDG_STATE_HOME=str(home / "state"), npm_config_prefix=str(home / "npm")) child = subprocess.Popen([node, "scripts/rea.mjs", "mcp"], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1) replies, errors = queue.Queue(), [] def read_stdout(): for line in child.stdout: replies.put(json.loads(line)) def read_stderr(): for line in child.stderr: errors.append(line.rstrip("\n")) readers = [threading.Thread(target=read_stdout), threading.Thread(target=read_stderr)] for reader in readers: reader.start() def send(value): child.stdin.write(json.dumps(value) + "\n") child.stdin.flush() def receive(timeout): try: return replies.get(timeout=timeout) except queue.Empty: return None try: send({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "id-verifier", "version": "1"}}}) initialized = receive(15) assert initialized and initialized.get("result"), initialized send({"jsonrpc": "2.0", "method": "notifications/initialized"}) results = [] for request_id in [9007199254740992, 1.5, 3, 0, -1, "9007199254740992", "fraction-control"]: send({"jsonrpc": "2.0", "id": request_id, "method": "ping"}) results.append({"id": request_id, "reply": receive(2)}) child.stdin.close() child.wait(timeout=10) finally: if child.poll() is None: child.kill() child.wait() for reader in readers: reader.join() print(json.dumps({"run": run, "replies": results, "stderr": errors, "exit": child.returncode, "extra_stdout": list(replies.queue)})) finally: shutil.rmtree(root) PY ``` 3. Observed in all three sessions: no response for either numeric case, successful subsequent controls, four stderr lines carrying the connection-loss advice, no extra stdout, and exit `0`. ### Expected behavior A frame validation failure on a connection that continues to answer requests should not tell the caller that the connection was lost or direct them to restart it. REA's initialization instructions make the restart ladder conditional on a dropped connection. The diagnostic should distinguish a rejected message from actual transport loss. Under [JSON-RPC 2.0](https://www.jsonrpc.org/specification), Number IDs are permitted and fractional parts are discouraged. The negotiated [MCP 2025-06-18 base protocol](https://modelcontextprotocol.io/specification/2025-06-18/basic) requires string or integer IDs: `1.5` is therefore an invalid MCP control. That does not establish a lost connection. The large integer is exactly representable, although outside the SDK validator's safe-integer range. Accepting these frames or supplying their missing responses is outside this report's requested diagnostic correction. ### Impact and scope P3: REA gives an incorrect failure classification and recovery instruction. No actual client restart, target loss, changed analysis result or host timeout was exercised. The observed connection continues to serve requests. Both tested string IDs work without a restart. Verification covers target-free stdio on macOS; HTTP and other platforms were not tested. ### REA version Exact `node scripts/rea.mjs --version` output: ```text 6.8.0 ``` Source commit: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS, darwin arm64. Locked MCP server SDK 2.3.1. No analysis provider was required. ### Target and artifact context No target or artifact; initialized stdio sessions and target-free ping requests only. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence For each numeric case, stderr includes this standalone line and a JSON logger record with the same `msg`: ```text Retry the tool call once only when it is safe to repeat or known not to have started. If it may already have changed the target, or it fails again, tell the user REA lost its MCP client connection and ask them to restart the MCP client. ``` The subsequent control response on every connection is: ```json {"result":{},"jsonrpc":"2.0","id":3} ``` Source checked at the reported baseline: - `src/main.ts:29-39,108-115` connects this public stdio entry to SDK serving and the deferred runtime. - `src/main/deferredRuntime.ts:217-222` ignores the error argument and emits the fixed connection-loss message through both logging and stderr. `src/main/messages.ts:1-2` owns that text. `src/main/transport.ts:153-157` contains the equivalent mapping in the other runtime. - `src/server/recoveryInstructions.ts:4` conditions the advertised retry/restart ladder on connection loss. - The locked SDK's `node_modules/@modelcontextprotocol/server/dist/stdio.mjs:78-86,562-564` catches frame-validation exceptions, forwards them to the error callback, and continues reading. Its `dist/src-Cqbh3MYc.mjs:7195-7196` parses inbound lines with the message schema. - `docs/mcp-contracts.md:829-833` describes HTTP listener errors; it is not a stdio request-ID acceptance contract. Its tool-argument normalization discussion does not cover frames rejected before tool dispatch. ### Typed outcome or recovery expected Keep actual connection-loss recovery for confirmed loss. For a rejected frame on a live connection, report the validation category and useful corrective guidance without telling the caller to restart. This does not require echoing raw untrusted frames or changing the SDK's accepted ID range. ### Analysis or suspected cause Source-confirmed ownership split: the SDK rejects the incoming frame before REA request handlers receive it. Its exported `deserializeMessage` independently raises `ZodError` for both numeric cases; the large integer produces an ID validation issue with maximum `9007199254740991`. The error callback receives an error rather than the rejected raw frame or correlated ID. REA then replaces that error with its own connection-loss text. REA can correct this classification and recovery independently of the upstream parser. This report does not attribute the missing JSON-RPC responses to a REA tool handler or propose bypassing protocol validation. ### Additional context Open and closed issue searches, issue comments, and all-state PR searches found no duplicate of the live-connection validation diagnostic. Reviewed related issues include #3509, #3221, #7372, #7438, #4885, #6454 and #7346. Merged [PR #6353](https://github.com/morluto/rea/pull/6353) updates the recovery ladder for connection loss; it does not distinguish nonfatal frame errors. [Issue #7346](https://github.com/morluto/rea/issues/7346) and [PR #7367](https://github.com/morluto/rea/pull/7367) concern HTTP bind-failure recovery, a different trigger and restoration check. No last-known-good version was established.
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#4605
No description provided.