MoonshotAI / MoonshotAI/kimi-cli

MCP stdio server fails with 'Server is already initialized' due to fastmcp keep_alive=True

Open
#2,031 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 stdio server fails with "Server is already initialized" due to fastmcp keep_alive=True

Environment
  • Kimi CLI version: 1.24.0
  • OS: macOS 15.x (arm64)
  • MCP Server: @steipete/peekaboo 3.0.0-beta3 (stdio transport)
  • fastmcp version: bundled with kimi-cli
Problem Description

When using a stdio-based MCP server (specifically Peekaboo), the first tool call succeeds, but all subsequent tool calls fail with:

Client failed to connect: Invalid Request: Server is already initialized

After the first failure, subsequent calls may also show:

Client failed to connect: Server session was closed unexpectedly
Root Cause Analysis

The issue is in fastmcp.client.transports.StdioTransport, which defaults to keep_alive=True:

class StdioTransport(ClientTransport):
    def __init__(self, ..., keep_alive: bool | None = None):
        if keep_alive is None:
            keep_alive = True   # <-- problematic default
        self.keep_alive = keep_alive

With keep_alive=True, the subprocess survives after the async with context exits:

async def connect_session(self, ...):
    try:
        await self.connect(...)
        yield cast(ClientSession, self._session)
    finally:
        if not self.keep_alive:
            await self.close()  # process is NOT killed when keep_alive=True

However, MCPTool.__call__ in kimi-cli wraps every tool invocation in a fresh async with self._client:

async with self._client as client:
    result = await client.call_tool(...)

This enters _context_manager()_connect()_session_runner(), which always sends a new initialize request:

async with self.transport.connect_session(...) as session:
    self._session_state.session = session
    self._session_state.initialize_result = await session.initialize()  # <-- called every time

Since the Peekaboo process is still alive from the previous call (thanks to keep_alive=True), it receives a second initialize request and correctly rejects it per the MCP spec (initialize is idempotent-once-only). This causes the connection to fail.

Why manual testing works

Piping JSON-RPC directly to peekaboo mcp works because we send initialize exactly once. The problem only manifests when fastmcp re-enters the session context and re-initializes.

Verified Workaround

Change the default in fastmcp/client/transports.py:335:

-            keep_alive = True
+            keep_alive = False

With this patch:

  1. Each async with starts a fresh subprocess
  2. initialize is always sent to a brand-new process
  3. Peekaboo MCP works correctly for all tool calls

Verified on: Peekaboo permissions, see, list, image, click, etc.

Suggested Fix

There are two possible proper fixes:

Option A (recommended): Change StdioTransport default to keep_alive=False, or expose it in ~/.kimi/mcp.json / StdioMCPServer config so users can override it per-server.

Option B: Make fastmcp client smarter about reusing sessions. If keep_alive=True, the client should detect that the session is still alive and skip re-initialization rather than sending a duplicate initialize request.

Reproduction Steps
  1. Install a stdio MCP server (e.g., npm install -g @steipete/peekaboo)
  2. Add to ~/.kimi/mcp.json:
    {
      "mcpServers": {
        "peekaboo": {
          "command": "peekaboo",
          "args": ["mcp"]
        }
      }
    }
    
  3. Start kimi
  4. Call any Peekaboo tool twice:
    >>> permissions
    [ok] Screen Recording: Granted
    >>> permissions
    Error: Client failed to connect: Invalid Request: Server is already initialized
    
Related
  • This affects any stdio MCP server that strictly enforces single-initialization (which is correct per MCP spec).
  • The StdioMCPServer.to_transport() method currently does not pass keep_alive, so there is no user-facing configuration to control this behavior.

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

Read fastmcp/client/transports.py, especially StdioTransport, then trace MCPTool.call and StdioMCPServer.to_transport() to understand session lifetime and initialization. Reproduce by calling the same stdio MCP tool twice; done means repeated calls no longer send an invalid second initialization and the configured server remains usable.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.