Skip to content

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

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

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
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.

1 participant