Skip to content

MCP consecutive batch --input calls can lose the next JSON-RPC request #339

Description

@Neon-Wang

Environment

  • OfficeCLI v1.0.144
  • Windows 11 x64
  • MCP over stdio
  • Python 3.x used only as the minimal JSON-RPC client below

Summary

After one successful batch --input call through officecli mcp, the next tools/call can disappear from the MCP server's main reader and never receive a response.

The same recipe completes in under one second when run through the standalone CLI, and one batch in a fresh MCP process also completes in under one second. The failure appears when two explicit-input batch calls are sent sequentially through the same MCP process.

Minimal reproduction

Save and run this script with officecli available on PATH:

import json
import os
import queue
import shutil
import subprocess
import tempfile
import threading
import time

exe = shutil.which("officecli")
assert exe, "officecli is not on PATH"

root = tempfile.mkdtemp(prefix="officecli-mcp-two-batch-")
deck = os.path.join(root, "deck.pptx")
theme = os.path.join(root, "theme.json")
page = os.path.join(root, "page.json")

with open(theme, "w", encoding="utf-8") as f:
    json.dump([
        {"command": "set", "path": "/theme", "props": {"accent1": "1D42D8", "lt1": "FFFFFF"}},
        {"command": "set", "path": "/slidemaster[1]", "props": {"background": "lt1"}},
    ], f)

with open(page, "w", encoding="utf-8") as f:
    json.dump([
        {"command": "add", "parent": "/", "type": "slide", "props": {"layout": "blank"}},
        {"command": "add", "parent": "/slide[1]", "type": "textbox", "props": {"text": "Hello"}},
    ], f)

proc = subprocess.Popen(
    [exe, "mcp"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True,
    encoding="utf-8",
    bufsize=1,
)
responses = queue.Queue()
threading.Thread(
    target=lambda: [responses.put(line.rstrip("\r\n")) for line in proc.stdout],
    daemon=True,
).start()

def call(request_id, method, params, timeout=5):
    request = {"jsonrpc": "2.0", "id": request_id, "method": method, "params": params}
    started = time.monotonic()
    proc.stdin.write(json.dumps(request, separators=(",", ":")) + "\n")
    proc.stdin.flush()
    try:
        response = json.loads(responses.get(timeout=timeout))
        print(request_id, round(time.monotonic() - started, 3), response.get("id"))
        return response
    except queue.Empty:
        print(request_id, "TIMEOUT")
        return None

try:
    call(1, "initialize", {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": {"name": "repro", "version": "1"},
    })
    call(2, "tools/call", {"name": "officecli", "arguments": {"command": ["create", deck, "--json"]}})
    call(3, "tools/call", {"name": "officecli", "arguments": {"command": ["batch", deck, "--input", theme, "--json"]}})
    call(4, "tools/call", {"name": "officecli", "arguments": {"command": ["batch", deck, "--input", page, "--json"]}}, timeout=10)
finally:
    proc.terminate()
    try:
        proc.wait(timeout=3)
    except subprocess.TimeoutExpired:
        proc.kill()

Observed on v1.0.144: requests 1-3 return quickly; request 4 times out without a JSON-RPC response. Running the page batch directly as a standalone CLI command completes in about one second.

Likely cause

McpServer.RunAsync owns a StreamReader over standard input for JSON-RPC. CommandBuilder.Batch.cs also evaluates redirected stdin by starting:

Task.Run(() => StdIn.Peek())

This happens before the OFFICECLI_BATCH_ALLOW_STDIN_REDIRECT warning suppression is checked, including when --input or --commands already provides the complete payload.

When the 50ms wait expires, the Peek task is abandoned but not cancelled. It remains blocked on the same process stdin used by the MCP JSON-RPC reader and can consume or buffer the next request line. The MCP main loop then never sees that tools/call.

Expected behavior

  • Explicit --input or --commands must not inspect or read stdin.
  • In MCP mode, stdin must be reserved exclusively for JSON-RPC.
  • batch <file> without an explicit input source should fail immediately with actionable guidance instead of reading the MCP transport.
  • Two sequential batch calls must each return the response carrying their own request ID.

Suggested regression coverage

  1. Start a real officecli mcp process.
  2. Send create, an explicit-input theme batch, and a second explicit-input page batch.
  3. Assert both batch responses arrive within a bounded interval and preserve request IDs.
  4. Send an MCP batch without --input/--commands; assert it fails immediately and that the following ping or tools/call still succeeds.
  5. Preserve ordinary standalone officecli batch file < recipe.json behavior.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions