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:
- IronClaw spawns the child process
- IronClaw sends
tools/list (JSON-RPC id=1) immediately
- The MCP server (Python SDK 1.26.0) rejects it:
"Received request before initialization was complete"
- IronClaw logs:
Failed to connect to MCP server: MCP error: Invalid request parameters (code -32602)
Expected behavior
Per the MCP specification, the client should:
- Send
initialize request with protocolVersion, capabilities, clientInfo
- Receive
InitializeResult from the server
- Send
notifications/initialized notification
- 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
Summary
The stdio MCP transport introduced in v0.17.0 (#721) skips the MCP protocol initialization handshake. It sends
tools/listdirectly without first sendinginitialize+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:
tools/list(JSON-RPC id=1) immediately"Received request before initialization was complete"Failed to connect to MCP server: MCP error: Invalid request parameters (code -32602)Expected behavior
Per the MCP specification, the client should:
initializerequest withprotocolVersion,capabilities,clientInfoInitializeResultfrom the servernotifications/initializednotificationtools/listor other requestsThe HTTP transport correctly implements this handshake (via
McpClient::initialize()). The stdio transport appears to skip it.Reproduction
Debug evidence
Adding debug logging to the Python MCP SDK's
ServerSession._received_requestmethod confirms:The server never transitions from
NotInitializedbecause noinitializerequest is received.Affected versions
McpClient::initialize())Workaround
Monkey-patch the Python MCP SDK to skip the initialization state check:
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 callsinitialize()beforelist_tools(), the same way the HTTP transport does. The relevant code path is likely inMcpClient::new_with_transport()or wherever stdio server connections are established during startup.Note:
ironclaw mcp testalso panics for stdio transport servers: