iOfficeAI / iOfficeAI/OfficeCLI

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

Open
#339 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
C#
Stars
30.7k
Forks
2.1k
Avg merge
9d 8h
Merged PRs (30d)
5

Description

## 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`:

```python
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:

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

Contributor guide

Open the contributing guide

Research direction

Start with McpServer.RunAsync and CommandBuilder.Batch.cs, focusing on the StdIn.Peek() path, then run the supplied Python reproduction against a real officecli mcp process. Done means sequential explicit-input batches both return their request IDs, implicit-input MCP batches fail promptly without consuming later JSON-RPC requests, and standalone batch stdin behavior remains intact.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
cli, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.