Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/kimi_cli/soul/toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import importlib
import inspect
import json
import re
import time
from contextvars import ContextVar
from dataclasses import dataclass
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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 type(server_config) is 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]


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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=[]
)
Expand Down
55 changes: 55 additions & 0 deletions tests/core/test_mcp_stdio_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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()


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"}