From e6a6f9c6417727834914e82bc55202c7662286cb Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 13 May 2026 19:22:17 +0800 Subject: [PATCH 1/2] fix: redirect stdio MCP stderr to logs --- src/kimi_cli/soul/toolset.py | 35 +++++++++++++++++++++++++--- tests/core/test_mcp_stdio_logging.py | 28 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 tests/core/test_mcp_stdio_logging.py diff --git a/src/kimi_cli/soul/toolset.py b/src/kimi_cli/soul/toolset.py index 3868d3bdb..9d2b25159 100644 --- a/src/kimi_cli/soul/toolset.py +++ b/src/kimi_cli/soul/toolset.py @@ -6,6 +6,7 @@ import importlib import inspect import json +import re import time from contextvars import ContextVar from dataclasses import dataclass @@ -33,6 +34,7 @@ from kimi_cli import logger from kimi_cli.exception import InvalidToolError, MCPRuntimeError from kimi_cli.hooks.engine import HookEngine +from kimi_cli.share import get_share_dir from kimi_cli.tools import SkipThisTool from kimi_cli.wire.types import ( AudioURLPart, @@ -61,6 +63,8 @@ _current_session_id: ContextVar[str] = ContextVar("_current_session_id", default="") +_MCP_LOG_FILENAME_PATTERN = re.compile(r"[^A-Za-z0-9_.-]+") + def set_session_id(sid: str) -> None: _current_session_id.set(sid) @@ -82,6 +86,32 @@ def get_current_tool_call_or_none() -> ToolCall | None: return current_tool_call.get() +def _mcp_stdio_log_path(server_name: str) -> Path: + safe_name = _MCP_LOG_FILENAME_PATTERN.sub("_", server_name).strip("._") or "server" + return get_share_dir() / "logs" / "mcp" / f"{safe_name}.log" + + +def _build_mcp_client(server_name: str, server_config: Any) -> fastmcp.Client[Any]: + import fastmcp + from fastmcp.client.transports import StdioTransport + from fastmcp.mcp_config import MCPConfig, StdioMCPServer + + if isinstance(server_config, StdioMCPServer): + log_path = _mcp_stdio_log_path(server_name) + log_path.parent.mkdir(parents=True, exist_ok=True) + transport = StdioTransport( + command=server_config.command, + args=server_config.args, + env=server_config.env, + cwd=server_config.cwd, + keep_alive=server_config.keep_alive, + log_file=log_path, + ) + return fastmcp.Client(transport) + + return fastmcp.Client(MCPConfig(mcpServers={server_name: server_config})) + + type ToolType = CallableTool | CallableTool2[Any] @@ -538,8 +568,7 @@ async def load_mcp_tools( MCPRuntimeError(KimiCLIException, RuntimeError): When any MCP server cannot be connected. """ - import fastmcp - from fastmcp.mcp_config import MCPConfig, RemoteMCPServer + from fastmcp.mcp_config import RemoteMCPServer from kimi_cli.mcp_oauth import create_mcp_oauth, has_mcp_oauth_tokens from kimi_cli.ui.shell.prompt import toast @@ -644,7 +673,7 @@ async def _connect(): continue server_config = server_config.model_copy(update={"auth": auth}) - client = fastmcp.Client(MCPConfig(mcpServers={server_name: server_config})) + client = _build_mcp_client(server_name, server_config) self._mcp_servers[server_name] = MCPServerInfo( status="pending", client=client, tools=[] ) diff --git a/tests/core/test_mcp_stdio_logging.py b/tests/core/test_mcp_stdio_logging.py new file mode 100644 index 000000000..87b2247c8 --- /dev/null +++ b/tests/core/test_mcp_stdio_logging.py @@ -0,0 +1,28 @@ +from __future__ import annotations + + +def test_stdio_mcp_stderr_goes_to_kimi_log_file(tmp_path, monkeypatch): + monkeypatch.setenv("KIMI_SHARE_DIR", str(tmp_path)) + + from fastmcp.client.transports import StdioTransport + from fastmcp.mcp_config import MCPConfig + + from kimi_cli.soul.toolset import _build_mcp_client + + mcp_config = MCPConfig.model_validate( + { + "mcpServers": { + "chrome/devtools": { + "command": "npx", + "args": ["-y", "chrome-devtools-mcp@latest"], + } + } + } + ) + server_config = mcp_config.mcpServers["chrome/devtools"] + + client = _build_mcp_client("chrome/devtools", server_config) + + assert isinstance(client.transport, StdioTransport) + assert client.transport.log_file == tmp_path / "logs" / "mcp" / "chrome_devtools.log" + assert (tmp_path / "logs" / "mcp").is_dir() From 5526cc8695f34a31b0cd7be49bdd716bde56f024 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Thu, 14 May 2026 21:41:19 +0800 Subject: [PATCH 2/2] fix: preserve transforming stdio MCP configs --- src/kimi_cli/soul/toolset.py | 2 +- tests/core/test_mcp_stdio_logging.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/kimi_cli/soul/toolset.py b/src/kimi_cli/soul/toolset.py index 9d2b25159..140df1ebc 100644 --- a/src/kimi_cli/soul/toolset.py +++ b/src/kimi_cli/soul/toolset.py @@ -96,7 +96,7 @@ def _build_mcp_client(server_name: str, server_config: Any) -> fastmcp.Client[An from fastmcp.client.transports import StdioTransport from fastmcp.mcp_config import MCPConfig, StdioMCPServer - if isinstance(server_config, StdioMCPServer): + if type(server_config) is StdioMCPServer: log_path = _mcp_stdio_log_path(server_name) log_path.parent.mkdir(parents=True, exist_ok=True) transport = StdioTransport( diff --git a/tests/core/test_mcp_stdio_logging.py b/tests/core/test_mcp_stdio_logging.py index 87b2247c8..90ebbfe5e 100644 --- a/tests/core/test_mcp_stdio_logging.py +++ b/tests/core/test_mcp_stdio_logging.py @@ -26,3 +26,30 @@ def test_stdio_mcp_stderr_goes_to_kimi_log_file(tmp_path, monkeypatch): assert isinstance(client.transport, StdioTransport) assert client.transport.log_file == tmp_path / "logs" / "mcp" / "chrome_devtools.log" assert (tmp_path / "logs" / "mcp").is_dir() + + +def test_transforming_stdio_mcp_config_keeps_fastmcp_transport(tmp_path, monkeypatch): + monkeypatch.setenv("KIMI_SHARE_DIR", str(tmp_path)) + + from fastmcp.client.transports.config import MCPConfigTransport + from fastmcp.mcp_config import MCPConfig + + from kimi_cli.soul.toolset import _build_mcp_client + + mcp_config = MCPConfig.model_validate( + { + "mcpServers": { + "filtered": { + "command": "npx", + "args": ["-y", "some-mcp-server"], + "include_tags": ["safe"], + } + } + } + ) + server_config = mcp_config.mcpServers["filtered"] + + client = _build_mcp_client("filtered", server_config) + + assert isinstance(client.transport, MCPConfigTransport) + assert client.transport.config.mcpServers["filtered"].include_tags == {"safe"}