Skip to content

spawnCommand pipes stdin/stdout when used inside MCP server, breaking JSON-RPC transport #596

@tosoyn

Description

@tosoyn

When node-llama-cpp is used as a dependency in an MCP server running over stdio transport (JSON-RPC over stdin/stdout), the spawnCommand function in dist/utils/spawnCommand.js causes two critical issues during lazy model initialization.

Problem

Lines 57-60 in spawnCommand.js:

if (progressLogs) {
    child.stdout?.pipe(process.stdout);  // (1) cmake output → JSON-RPC stdout
    child.stderr?.pipe(process.stderr);
    process.stdin.pipe(child.stdin);     // (2) JSON-RPC stdin → cmake child
}

Issue 1: stdout pollution
child.stdout?.pipe(process.stdout) sends cmake/Vulkan build output directly into the MCP JSON-RPC response stream. The MCP client receives non-JSON lines and throws ValidationError, poisoning the session.

Issue 2: stdin hijack (more severe)
process.stdin.pipe(child.stdin) connects the MCP JSON-RPC request stream to the cmake child process. When cmake exits (e.g., exit 1 on Vulkan not found), the pipe closes or drains the parent's stdin. The parent node process can no longer read JSON-RPC requests and exits cleanly with code 0. The MCP client sees ClosedResourceError on the next request.

Reproduction

  1. Install an npm package that uses node-llama-cpp (e.g., @tobilu/qmd)
  2. Run it as an MCP server over stdio: qmd mcp
  3. Send a JSON-RPC tool call that triggers lazy model loading (e.g., a vector/semantic search)
  4. Observe: cmake build output appears in stdout between JSON-RPC messages
  5. After cmake exits, the MCP server exits — no further requests can be processed

Environment

  • node-llama-cpp: bundled via @tobilu/qmd
  • Platform: Linux x86_64 (WSL2 Ubuntu 24.04)
  • Node.js: 22.x
  • GPU: Vulkan not available (falls back to CPU — triggers compilation attempt)

Workaround

Comment out the two pipe lines in spawnCommand.js:

if (progressLogs) {
    // child.stdout?.pipe(process.stdout);
    child.stderr?.pipe(process.stderr);
    // process.stdin.pipe(child.stdin);
}

Suggested Fix

When running inside an MCP server (or any stdio-based IPC), progressLogs should not pipe to process.stdout/process.stdin. Options:

  • Check if stdout is a TTY before piping (process.stdout.isTTY)
  • Add an option to disable stdin/stdout piping (e.g., progressLogs: "stderr-only")
  • Never pipe process.stdin to child processes — cmake doesn't need interactive input

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions