Fix: Anthropic SDK MCP Tools Get Empty Arguments

Written by Michael Lip · Solo founder of Zovo · $400K+ on Upwork · 100% JSS Join 50+ builders · More at zovo.one

The Error

When using the Claude Agent SDK (Python) with in-process MCP servers and permission approval callbacks, all MCP tools receive empty {} arguments:

[DEBUG] Request body: {"action":"call_tool","server":"my-tools","tool_name":"read","arguments":{}}

Error: Missing required parameter 'files'

The model correctly sends the arguments in the permission request, but after approval, the tool is called with an empty object.

Quick Fix

Bypass the permission system to confirm the root cause:

agent = ClaudeAgent(
    permission_mode="bypass_permissions",  # Skip approval flow
    mcp_servers={
        "my-tools": {
            "type": "sdk",
            "tools": [read_files]
        }
    }
)

If this works, the bug is confirmed — the permission approval flow is dropping arguments.

What’s Happening

When you use the can_use_tool permission callback, the following sequence occurs:

1. Model sends correct arguments in the permission request:

{
  "type": "control_request",
  "request_id": "bd66e5e1-...",
  "request": {
    "subtype": "can_use_tool",
    "tool_name": "mcp__my-tools__read",
    "input": {"files": ["README.md"]},
    "tool_use_id": "toolu_xyz"
  }
}

2. Your callback returns approval:

async def can_use_tool(tool_name, arguments, context):
    return PermissionResultAllow()  # Simple approval

3. SDK sends buggy approval response:

{
  "type": "control_response",
  "response": {
    "subtype": "success",
    "request_id": "bd66e5e1-...",
    "response": {
      "behavior": "allow",
      "updatedInput": {}    // BUG: Empty object overwrites original arguments
    }
  }
}

4. Claude CLI uses the empty updatedInput instead of the original arguments:

{"action":"call_tool","server":"my-tools","tool_name":"read","arguments":{}}

Root cause: The SDK’s permission handler always includes updatedInput in the approval response, even when PermissionResultAllow() is called without explicitly setting updated_input. The updatedInput field defaults to an empty object {}, which Claude CLI interprets as “replace the original arguments with this empty object.”

The correct behavior: when updated_input is not explicitly set, the approval response should omit the updatedInput field entirely, and Claude CLI should use the original arguments.

Step-by-Step Solution

Option 1: Pass Original Arguments Through

Explicitly forward the original arguments in your approval callback:

async def can_use_tool(tool_name: str, arguments: dict, context) -> PermissionResult:
    # Explicitly pass the original arguments back
    return PermissionResultAllow(updated_input=arguments)

This ensures updatedInput contains the correct values instead of an empty object.

Option 2: Bypass Permissions for Trusted Tools

If you do not need per-tool approval, skip the permission flow:

agent = ClaudeAgent(
    permission_mode="bypass_permissions",
    mcp_servers={
        "my-tools": {
            "type": "sdk",
            "tools": [read_files, write_files, search_files]
        }
    }
)

Option 3: Implement Selective Permission with Argument Forwarding

If you need to approve some tools and deny others, forward arguments on approval:

DANGEROUS_TOOLS = {"mcp__my-tools__delete", "mcp__my-tools__format_disk"}

async def can_use_tool(
    tool_name: str,
    arguments: dict,
    context
) -> PermissionResult:
    if tool_name in DANGEROUS_TOOLS:
        return PermissionResultDeny(reason=f"Tool {tool_name} is not allowed")

    # IMPORTANT: Forward the original arguments
    return PermissionResultAllow(updated_input=arguments)

Option 4: Monkey-Patch the SDK (Temporary)

If you cannot modify the approval callback to forward arguments (e.g., using a framework that wraps it):

# WARNING: This is a temporary workaround. Pin your SDK version.
import anthropic._internal.permission_handler as ph

_original_build_response = ph._build_approval_response

def _patched_build_response(result, request_id):
    response = _original_build_response(result, request_id)
    # Remove updatedInput if it's empty
    if (
        "response" in response
        and "response" in response["response"]
        and response["response"]["response"].get("updatedInput") == {}
    ):
        del response["response"]["response"]["updatedInput"]
    return response

ph._build_approval_response = _patched_build_response

Prevention

Tools That Help

When debugging MCP server interactions and permission flows, a dev tool extension can help inspect the JSON-RPC message exchanges between the SDK and Claude CLI.