Skip to content

fix(mcp): avoid probing JSON-RPC stdin for explicit batch input - #293

Closed
EricLingRui wants to merge 1 commit into
iOfficeAI:mainfrom
EricLingRui:agent/fix-mcp-batch-stdin-probe
Closed

fix(mcp): avoid probing JSON-RPC stdin for explicit batch input#293
EricLingRui wants to merge 1 commit into
iOfficeAI:mainfrom
EricLingRui:agent/fix-mcp-batch-stdin-probe

Conversation

@EricLingRui

Copy link
Copy Markdown

Summary

  • skip the redirected-stdin probe when batch already has an explicit non-stdin input and the warning is disabled
  • ensure MCP batch --commands / batch --input calls never read from the JSON-RPC transport
  • preserve stdin fallback, explicit --input -, and the existing ignored-stdin warning for normal CLI use

Root cause

The MCP server sets OFFICECLI_BATCH_ALLOW_STDIN_REDIRECT=1 because its stdin is the JSON-RPC transport. However, the old batch handler checked that flag only when deciding whether to print a warning, after it had already started a background StdIn.Peek().

When the 50 ms wait expired, the blocked task was abandoned but remained alive. Once the client sent its next JSON-RPC request, that task could buffer the request from stdin, leaving the MCP read loop without the expected message. The caller then waited until its tool timeout.

The fix decides whether the ignored-stdin warning is needed before starting the probe. MCP disables that warning, so it no longer creates a competing stdin reader. The mutual-exclusion check also runs before any possible probe.

Validation

Both the baseline (459b1a47) and patched source were published for linux-arm64 with:

dotnet publish src/officecli/officecli.csproj \
  -c Release -r linux-arm64 -o publish --nologo

I then ran the same MCP sequence against each binary: initialize, send a malformed string-form batch (which returns an error), then immediately send a valid argv-form batch.

Version Malformed call returned Next valid call
Baseline error as expected no response within 3 seconds
Patched error as expected success in 559 ms

Portable reproducer (set OFFICECLI to the binary under test):

import json
import os
import select
import subprocess
import tempfile
import time

cli = os.environ.get("OFFICECLI", "officecli")


def send(proc, message):
    proc.stdin.write(json.dumps(message, separators=(",", ":")) + "\n")
    proc.stdin.flush()


def receive(proc, expected_id, timeout):
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        ready, _, _ = select.select(
            [proc.stdout], [], [], max(0, deadline - time.monotonic())
        )
        if not ready:
            break
        message = json.loads(proc.stdout.readline())
        if message.get("id") == expected_id:
            return message
    raise TimeoutError(f"no response for id={expected_id}")


with tempfile.TemporaryDirectory() as work:
    document = os.path.join(work, "probe.docx")
    subprocess.run([cli, "create", document], check=True)
    proc = subprocess.Popen(
        [cli, "mcp"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.DEVNULL,
        text=True,
        bufsize=1,
    )
    try:
        send(proc, {
            "jsonrpc": "2.0", "id": 1, "method": "initialize",
            "params": {
                "protocolVersion": "2024-11-05",
                "capabilities": {},
                "clientInfo": {"name": "stdin-regression", "version": "1.0"},
            },
        })
        receive(proc, 1, 10)
        send(proc, {
            "jsonrpc": "2.0",
            "method": "notifications/initialized",
            "params": {},
        })

        malformed = (
            f'batch {document} --commands '
            '[{"op":"add","path":"/body","type":"paragraph",'
            '"props":{"text":"malformed"}}]'
        )
        send(proc, {
            "jsonrpc": "2.0", "id": 2, "method": "tools/call",
            "params": {
                "name": "officecli",
                "arguments": {"command": malformed},
            },
        })
        print("malformed isError:", receive(proc, 2, 5)["result"]["isError"])

        commands = json.dumps([{
            "op": "add", "path": "/body", "type": "paragraph",
            "props": {"text": "request after malformed batch"},
        }], separators=(",", ":"))
        send(proc, {
            "jsonrpc": "2.0", "id": 3, "method": "tools/call",
            "params": {
                "name": "officecli",
                "arguments": {
                    "command": ["batch", document, "--commands", commands],
                },
            },
        })
        print("next request isError:", receive(proc, 3, 3)["result"]["isError"])
    finally:
        proc.terminate()
        proc.wait(timeout=3)

The patched output is:

malformed isError: True
next request isError: False

@EricLingRui
EricLingRui force-pushed the agent/fix-mcp-batch-stdin-probe branch from fc7d0d8 to 4a0ecf3 Compare August 8, 2026 09:53
@EricLingRui
EricLingRui marked this pull request as ready for review August 8, 2026 10:57
@cursor
cursor Bot force-pushed the agent/fix-mcp-batch-stdin-probe branch from 4a0ecf3 to be64bbf Compare August 30, 2026 15:30
@goworm

goworm commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closing — the bug this targets is already fixed on main.

Your diagnosis was right, and it matched what we landed in 68c67b6c (issues #340 / #339): batch probed stdin with a consuming read purely to decide whether to warn that a redirected stdin would be ignored, and under officecli mcp stdin is the JSON-RPC transport, so that read swallowed the next request frame.

The fix on main makes the probe non-consuming: it only inspects stream metadata, and a non-seekable stream — a pipe, which is what the MCP transport is — short-circuits before anything is read. So no frame can be swallowed regardless of source.

I did look at what your branch adds on top of that, and it is real but no longer a correctness matter: gating the probe on shouldWarnAboutIgnoredStdin so it does not execute at all when the warning is suppressed (which McpServer does by setting OFFICECLI_BATCH_ALLOW_STDIN_REDIRECT=1). With the probe already unable to read, that is defense-in-depth and tidiness rather than a behaviour change, so I am not carrying the restructure just for it.

Thanks for the careful write-up — the "explicit batch input should never touch the JSON-RPC channel" framing was exactly the right way to state the invariant.

@goworm goworm closed this Sep 7, 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