Skip to content

Stdio MCP transport sends tools/list before initialize handshake #890

@j-h-scheufen

Description

@j-h-scheufen

Summary

The stdio MCP transport introduced in v0.17.0 (#721) skips the MCP protocol initialization handshake. It sends tools/list directly without first sending initialize + notifications/initialized. This violates the MCP specification and causes Python MCP SDK servers (v1.26.0) to reject the request with -32602 Invalid request parameters.

Observed behavior

When IronClaw v0.17.0 connects to a stdio MCP server:

  1. IronClaw spawns the child process
  2. IronClaw sends tools/list (JSON-RPC id=1) immediately
  3. The MCP server (Python SDK 1.26.0) rejects it: "Received request before initialization was complete"
  4. IronClaw logs: Failed to connect to MCP server: MCP error: Invalid request parameters (code -32602)

Expected behavior

Per the MCP specification, the client should:

  1. Send initialize request with protocolVersion, capabilities, clientInfo
  2. Receive InitializeResult from the server
  3. Send notifications/initialized notification
  4. Only then send tools/list or other requests

The HTTP transport correctly implements this handshake (via McpClient::initialize()). The stdio transport appears to skip it.

Reproduction

# 1. Install any Python MCP server using mcp SDK >= 1.23.0
uv tool install mem0-mcp-selfhosted

# 2. Register as stdio MCP server
ironclaw mcp add test-server \
  --transport stdio \
  --command /path/to/mcp-server-binary \
  --no-onboard --cli-only

# 3. Start IronClaw
ironclaw run --no-onboard

# 4. Check logs — will show:
#   WARNING root | Failed to validate request: Received request before initialization was complete
#   WARN Failed to connect to MCP server 'test-server': MCP error: Invalid request parameters (code -32602)

Debug evidence

Adding debug logging to the Python MCP SDK's ServerSession._received_request method confirms:

state=InitializationState.NotInitialized, request=ListToolsRequest

The server never transitions from NotInitialized because no initialize request is received.

Affected versions

  • IronClaw: v0.17.0
  • MCP Python SDK: 1.26.0 (likely affects all versions that enforce initialization)
  • Works fine with HTTP transport (which goes through McpClient::initialize())

Workaround

Monkey-patch the Python MCP SDK to skip the initialization state check:

#!/usr/bin/env python3
"""Wrapper that patches MCP SDK initialization check for IronClaw stdio compatibility."""
import mcp.server.session as sess

_original = sess.ServerSession._received_request

async def _patched(self, responder):
    if self._initialization_state != sess.InitializationState.Initialized:
        self._initialization_state = sess.InitializationState.Initialized
    return await _original(self, responder)

sess.ServerSession._received_request = _patched

# Import and run the actual MCP server
from your_mcp_server import main
main()

Then register the wrapper as the stdio command instead of the server binary directly.

Suggested fix

In src/tools/mcp/client.rs, ensure the stdio transport path calls initialize() before list_tools(), the same way the HTTP transport does. The relevant code path is likely in McpClient::new_with_transport() or wherever stdio server connections are established during startup.

Note: ironclaw mcp test also panics for stdio transport servers:

thread 'main' panicked at src/tools/mcp/client.rs:112:9:
new_with_config only supports HTTP transport; use new_with_transport for stdio/UDS

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions