Skip to content

[BUG] MCP Tools Receive Empty Arguments When Using Permission Approval Flow #1063

@gunpal5

Description

@gunpal5

What's Wrong?

When using the Claude Agent SDK (Python) with in-process MCP servers and permission approval callbacks (can_use_tool), all MCP tools receive empty {} arguments despite Claude CLI sending correct parameters in the permission request.

Observed behavior:

  1. Claude CLI correctly sends tool arguments in the control_request permission prompt: "input": {"files": ["README.md"]}
  2. Python SDK approval callback returns PermissionResultAllow() (approval granted)
  3. SDK sends approval response with "updatedInput": {} in the control_response
  4. Claude CLI then invokes the MCP tool with empty arguments: "arguments": {}
  5. Tool fails with "missing required parameter" errors

Root cause: The SDK's permission approval handler incorrectly includes "updatedInput": {} (empty object) in approval responses when the user doesn't explicitly set updated_input. This causes Claude CLI to discard the original arguments and use the empty object instead.

What Should Happen?

When a permission approval callback returns PermissionResultAllow() without explicitly setting updated_input:

  1. The SDK should send a control_response with only "behavior": "allow"
  2. The SDK should NOT include an updatedInput field in the response
  3. Claude CLI should use the original tool arguments it sent in the permission request
  4. The MCP tool should receive the correct arguments (e.g., {"files": ["README.md"]})
  5. The tool should execute successfully

Error Messages/Logs

Permission request from Claude CLI (arguments are correct):

{
  "type": "control_request",
  "request_id": "bd66e5e1-a64f-4e68-acbc-538583bb94bf",
  "request": {
    "subtype": "can_use_tool",
    "tool_name": "mcp__codify-tools__read",
    "input": {"files": ["README.md"]},
    "tool_use_id": "toolu_xyz"
  }
}

BUGGY approval response from SDK:

{
  "type": "control_response",
  "response": {
    "subtype": "success",
    "request_id": "bd66e5e1-a64f-4e68-acbc-538583bb94bf",
    "response": {
      "behavior": "allow",
      "updatedInput": {}  // ❌ BUG: This overwrites Claude's arguments
    }
  }
}

Tool invocation (arguments lost):

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

MCP server error:

Error: Missing required parameter 'files'

Steps to Reproduce

  1. Set up Claude Agent SDK with in-process MCP server:

    from claude_agent import ClaudeAgent
    
    def read_files(files: list[str]) -> str:
        """Read file contents."""
        # Implementation
        pass
    
    async def can_use_tool(tool_name: str, arguments: dict, context) -> PermissionResult:
        # Simple approval - should preserve original arguments
        return PermissionResultAllow()
    
    agent = ClaudeAgent(
        can_use_tool=can_use_tool,
        mcp_servers={
            "my-tools": {
                "type": "sdk",
                "tools": [read_files]
            }
        }
    )
  2. Ask Claude to use a multi-parameter tool:

    response = await agent.query("Read the README.md file")
  3. Approval callback is invoked - returns PermissionResultAllow()

  4. Observe the error: Tool receives {} instead of {"files": ["README.md"]}

Expected: Tool executes successfully with correct arguments
Actual: Tool fails with "missing required parameter" error

Claude Model

claude-haiku-4-5-20251001 (also tested with claude-sonnet-4-5)

Is this a regression?

Unknown - This appears to be a longstanding issue in the SDK's permission approval implementation.

Last Working Version

N/A - Workaround is to use permission_mode="bypass_permissions" (no approval required), which works correctly.

Claude Code Version

1.0.48+ (tested with latest)

Platform

Claude API (via Python SDK)

Operating System

Windows 11 (also affects Linux/macOS)

Terminal/Shell

PowerShell / Bash

Additional Information

Root Cause Analysis

The SDK's permission approval response handler appears to always include updatedInput in the response, even when the user's callback doesn't set it. The likely code path:

Suspected buggy behavior:

# In SDK permission handler
if isinstance(result, PermissionResultAllow):
    response_data = {
        "behavior": "allow",
        "updatedInput": result.updated_input or arguments  # ❌ BUG
    }

Problem:

  • result.updated_input is None by default (not explicitly set)
  • Code falls back to arguments variable (from permission request parsing)
  • arguments can be an empty {} if parsing fails or is malformed
  • Empty dict is sent as "updatedInput": {}
  • Claude CLI interprets this as "replace arguments with empty object"

Expected Behavior

Correct implementation should be:

# In SDK permission handler
if isinstance(result, PermissionResultAllow):
    response_data = {"behavior": "allow"}

    # Only include updatedInput if explicitly provided
    if result.updated_input is not None and len(result.updated_input) > 0:
        response_data["updatedInput"] = result.updated_input

Why this works:

  • updatedInput is optional per the control protocol
  • If omitted, Claude uses original arguments
  • If present (even as {}), Claude uses the provided value
  • Fix: Only include updatedInput when explicitly set by user

Verification

Test 1: With permission_mode="bypass_permissions"

  • ✅ Tools work correctly (confirms MCP bridge is functional)

Test 2: With proposed fix + permission approval

  • ✅ Tools should receive correct arguments after approval
  • ✅ Approval flow should work as intended

Related Issues

This issue is related but distinct from:

  • Issue #2089: "MCP tools with multiple parameters receive empty {} arguments"
    • That issue may be about Claude CLI itself sending empty arguments
    • This issue is specifically about the SDK's approval response overwriting correct arguments

It's possible both issues have the same root cause in how Claude CLI handles the updatedInput field.

Protocol Documentation Question

Is the permission approval protocol documented? Specifically:

  1. Should updatedInput be omitted if not modifying arguments?
  2. Does updatedInput: {} mean "use empty arguments" or "no modification"?
  3. Is there official documentation on the control_request/control_response protocol?

This would help developers implement the permission system correctly.

Workaround

Until fixed, users can:

  1. Use permission_mode="bypass_permissions" (no approval prompts)
  2. Always set updated_input explicitly in PermissionResultAllow:
    async def can_use_tool(tool_name: str, arguments: dict, context):
        # Explicitly pass through original arguments
        return PermissionResultAllow(updated_input=arguments)
  3. Note: Workaround format code to unified style #2 may not work if arguments is already empty

Environment Details

  • Python Version: 3.11+
  • Claude Agent SDK: Latest
  • MCP Protocol Version: 2024-11-05
  • Node.js Version: Latest (for MCP bridge)

Impact

  • ❌ All multi-parameter MCP tools fail when using permission approval
  • ❌ No way to have both user approval AND working tools
  • ❌ Forces developers to bypass permissions entirely
  • ✅ Single-parameter tools may work (unverified)

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