MoonshotAI / MoonshotAI/kimi-cli
MCP stdio server fails with 'Server is already initialized' due to fastmcp keep_alive=True
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:
- Each
async withstarts a fresh subprocess initializeis always sent to a brand-new process- 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
- Install a stdio MCP server (e.g.,
npm install -g @steipete/peekaboo) - Add to
~/.kimi/mcp.json:{ "mcpServers": { "peekaboo": { "command": "peekaboo", "args": ["mcp"] } } } - Start
kimi - 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 passkeep_alive, so there is no user-facing configuration to control this behavior.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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