MoonshotAI / MoonshotAI/kimi-cli

MCP server stderr leaks into interactive terminal despite CLI redirect

Open
#2,263 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
11.4k
Forks
1.3k
Avg merge
9h 47m
Merged PRs (30d)
2

Description

Bug Report: MCP server stderr leaks into interactive terminal despite CLI redirect

Repository: MoonshotAI/kimi-cli
Version: kimi, version 1.43.0
Platform: Linux 6.17.0-23-generic x86_64 x86_64
Subscription: N/A (local CLI usage)


What issue are you seeing?

After updating to v1.43.0, initialization logs from stdio MCP servers (especially Node.js/npx-based ones) are dumped directly into the interactive shell UI, causing severe terminal corruption. Server logs interleave with Rich spinners, prompt-toolkit status bars, and MCP connection progress indicators, producing unreadable garbled output.

Example of corrupted output on startup:

⠙ MCP Servers: 0/9 connected, 0 tools
⠋ context7 (pending)          40                                              ⠼s
• serena (pending)                                                            •)
• serenaigco necting))
• playwrightpecoinecting)
• tailwindpecoinecting)
• docker(pcodnecting)
• redis-pco necting))
• notes-prov 36 tools)
• solana-devenfailed)
• promptc  4 tools)
── input ───────────────────────────────────────────────────────────────────────
 ⠋ Resolving dependencies...                                                     Starting Docker MCP Server...
Docker MCP Server initialized: docker-mcp-server v1.0.0
Docker Options passed: undefined
Configured for local Docker daemon
Connected to Docker daemon: 29.4.3 (API: 1.54)
All Docker tools registered successfully
Docker MCP Server is running and ready to accept connections────────────────────
Initializing Flowbite MCP Server in stdio modeck | /theme: switch dark/light
Starting Flowbite MCP Server in stdio mode...
Flowbite MCP Server running in stdio mode
Ready to accept requests via standard I/O
INFO  2026-05-13 17:32:49,655 [MainThread] serena.cli:start_mcp_server:346 - Ini• playwrightre23 tools)
• tailwind-02 tools)
• dockers 8 tools)
t

The noise makes it impossible to read the MCP status panel or the input line while servers are connecting.


What steps can reproduce the bug?

  1. Configure multiple stdio MCP servers in ~/.kimi/mcp.json (e.g., docker-mcp, flowbite-mcp, redis-mcp, context7-mcp, @playwright/mcp, serena).
  2. Start an interactive session: kimi
  3. Observe the terminal during the MCP server connection phase (first few seconds after startup).

Minimal mcp.json that reproduces the issue:

{
  "mcpServers": {
    "docker": {
      "command": "npx",
      "args": ["-y", "docker-mcp"]
    },
    "flowbite": {
      "command": "npx",
      "args": ["-y", "flowbite-mcp"]
    }
  }
}

Root cause analysis

I traced the issue to an interaction between kimi-cli's OS-level stderr redirection and the mcp Python library's default argument capture of sys.stderr.

How kimi-cli redirects stderr

In kimi_cli/utils/logging.py, StderrRedirector.install() does the following:

  1. Saves the original fd 2 with os.dup(2).
  2. Creates a pipe (os.pipe()).
  3. Replaces fd 2 with the pipe's write end via os.dup2(write_fd, 2).
  4. Starts a daemon thread that reads from the pipe's read end and forwards lines to the internal logger.

This correctly prevents stderr from child processes that inherit fd 2 from reaching the terminal.

How the MCP library breaks this

In mcp/client/stdio/__init__.py, the stdio_client function is defined as:

@asynccontextmanager
async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stderr):
    ...
    process = await _create_platform_compatible_process(
        command=command,
        args=server.args,
        env=env,
        errlog=errlog,
        cwd=server.cwd,
    )

The default argument errlog: TextIO = sys.stderr is evaluated at module import time, not at call time.

Because sys.stderr is a Python-level TextIOWrapper object, replacing OS fd 2 with os.dup2() does not change the underlying file descriptor that sys.stderr points to. sys.stderr.fileno() remains 2 after the redirection, but the object itself still encapsulates the original stream state.

However, the critical issue is subtler: when anyio.open_process(..., stderr=sys.stderr) is called, asyncio.create_subprocess_exec uses the file descriptor of the provided stream. After os.dup2(write_fd, 2), sys.stderr.fileno() returns 2, which now points to the pipe. In theory, the child should write to the pipe.

But empirical testing shows the child processes still write to the original terminal stderr. This suggests that either:

  1. sys.stderr was captured by the mcp module before kimi-cli installs the redirector, or
  2. The mcp library or anyio resolves the stderr stream through a different path that bypasses the redirected fd.

Regardless of the exact mechanism, the outcome is that MCP stdio subprocesses bypass kimi-cli's stderr-to-logger redirection, and their initialization logs flood the terminal.

Additionally, npx writes package resolution progress (e.g., "Resolving dependencies...") to stdout, which cannot be redirected without breaking the MCP stdio protocol.


What is the expected behavior?

  • MCP server initialization logs should be captured into kimi-cli's log file (~/.local/share/kimi/logs/kimi.log) or suppressed entirely.
  • The interactive terminal should only show the clean MCP status panel (spinners, connection counts, tool lists) without interleaved server logs.
  • npx progress output should be silenced or captured.

Workaround

Replace direct npx/uvx commands in mcp.json with shell wrappers that redirect stderr to /dev/null before the server starts, and pass --silent to npx to suppress its stdout noise:

{
  "mcpServers": {
    "docker": {
      "command": "sh",
      "args": ["-c", "exec npx --silent -y docker-mcp 2>/dev/null"]
    },
    "serena": {
      "command": "sh",
      "args": ["-c", "exec uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context agent --project-from-cwd --open-web-dashboard False 2>/dev/null"]
    }
  }
}

This eliminates all terminal noise but has a downside: genuine server startup errors are lost (they go to /dev/null instead of the log file).


Suggested fix for upstream

Option A: Ensure sys.stderr is updated after fd redirection

After os.dup2(write_fd, 2) in StderrRedirector.install(), reassign sys.stderr to point to the new fd:

import os
import sys

read_fd, write_fd = os.pipe()
os.dup2(write_fd, 2)
os.close(write_fd)

# Rebind sys.stderr so libraries that capture it see the redirected fd
sys.stderr = os.fdopen(2, 'w', closefd=False)

This ensures that any library capturing sys.stderr (like mcp) receives the redirected pipe, not the original terminal stream.

Option B: Filter non-JSONRPC stdout from MCP stdio servers

Since MCP stdio servers are supposed to write only JSON-RPC messages to stdout, kimi-cli could install a line filter on the stdout pipe of stdio MCP processes. Lines that fail JSON-RPC validation during the connection handshake could be silently discarded (or logged to file) instead of being passed through or leaked to the terminal.

Option C: Pass stderr=subprocess.DEVNULL explicitly to MCP client creation

If the fastmcp/mcp client creation API supports it, pass an explicit stderr=None (or /dev/null) when spawning stdio MCP servers. Since the MCP protocol communicates over stdin/stdout, stderr is not needed for protocol operation. Server errors that prevent connection will still manifest as connection timeouts or failures, which kimi-cli already handles in the MCP status panel.


Additional information

  • The issue affects all stdio MCP servers that write to stderr during initialization. This is extremely common among Node.js-based MCP servers (docker-mcp, flowbite-mcp, redis-mcp, context7-mcp, @playwright/mcp), which use console.log/console.error for startup banners.
  • Python-based servers like serena are also affected because they use Python's logging module, which writes to stderr by default.
  • The issue is a regression in UX: while the CLI's stderr redirector existed before, the combination of MCP server verbosity and the interactive TUI makes the leakage visually catastrophic.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading kimi_cli/utils/logging.py, especially StderrRedirector.install(), then inspect mcp/client/stdio/init.py and reproduce the issue with the minimal mcp.json using kimi. Trace how stderr is passed to stdio subprocesses and verify that server output is captured without corrupting the interactive terminal; run the relevant existing tests if identified.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.