ClaudeSDKClient streaming mode: deferred MCP tools fail to execute after ToolSearch

Resolved 💬 1 comment Opened Mar 29, 2026 by moajo Closed Mar 29, 2026

Description

When using ClaudeSDKClient from claude-code-sdk (Python) with MCP servers, deferred MCP tools are found via ToolSearch but never actually executed. The conversation ends after the first ResultMessage with num_turns=1, even when max_turns is set to a higher value (e.g., 20).

The same MCP tools work correctly when using claude --print --mcp-config <file> directly via CLI.

Steps to Reproduce

from claude_code_sdk import ClaudeCodeOptions, ClaudeSDKClient, create_sdk_mcp_server, tool

@tool("greet", "Say hello to someone", {"name": str})
async def greet(args):
    return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}

server = create_sdk_mcp_server("test-tools", tools=[greet])

options = ClaudeCodeOptions(
    system_prompt="You are a helpful assistant. Use the greet tool when asked.",
    permission_mode="bypassPermissions",
    max_turns=20,
    mcp_servers={"test-tools": server},
)

async with ClaudeSDKClient(options) as client:
    await client.query("Use the greet tool to say hello to Alice")
    async for msg in client.receive_response():
        print(type(msg).__name__, msg)

Expected behavior

The model should:

  1. Find the greet tool via ToolSearch
  2. Call the greet tool with {"name": "Alice"}
  3. Return the tool result

Actual behavior

The model:

  1. Finds the tool via ToolSearch({'query': 'select:mcp__test-tools__greet', 'max_results': 1})
  2. Immediately returns a ResultMessage with num_turns=1
  3. The actual MCP tool is never called

Root Cause Analysis

ClaudeSDKClient uses the streaming/control-protocol mode (--output-format stream-json --input-format stream-json with CLAUDE_CODE_ENTRYPOINT=sdk-py). In contrast, the working claude --print --mcp-config path uses the standard --print mode.

In the streaming SDK mode, after the CLI finds a deferred MCP tool via ToolSearch, it does not proceed to the actual tool invocation. Instead, it emits the first ResultMessage and closes the conversation. receive_response() ends on that ResultMessage, so the client-side cannot recover.

max_turns=20 is not exhausted — the CLI itself decides to complete after 1 turn.

Also tried (same result):

  • McpStdioServerConfig (subprocess-based MCP server) instead of create_sdk_mcp_server
  • Removing allowed_tools constraint
  • Various cwd settings

Workaround

Using subprocess.run(["claude", "--print", "--mcp-config", config_path, "-p", prompt]) instead of ClaudeSDKClient. This uses the standard --print path where MCP tools work correctly.

Environment

  • claude-code-sdk: 0.0.25 (latest on PyPI)
  • Claude Code CLI: 2.1.87
  • Python: 3.13
  • OS: Linux (Kubernetes container)
  • Auth: OAuth (Claude Max subscription)

Additional note

rate_limit_event message type also causes MessageParseError in SDK 0.0.25 — unrelated but worth noting.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗