-
Notifications
You must be signed in to change notification settings - Fork 396
Description
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:
- Claude CLI correctly sends tool arguments in the
control_requestpermission prompt:"input": {"files": ["README.md"]} - Python SDK approval callback returns
PermissionResultAllow()(approval granted) - SDK sends approval response with
"updatedInput": {}in thecontrol_response - Claude CLI then invokes the MCP tool with empty arguments:
"arguments": {} - 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:
- The SDK should send a
control_responsewith only"behavior": "allow" - The SDK should NOT include an
updatedInputfield in the response - Claude CLI should use the original tool arguments it sent in the permission request
- The MCP tool should receive the correct arguments (e.g.,
{"files": ["README.md"]}) - 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
-
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] } } )
-
Ask Claude to use a multi-parameter tool:
response = await agent.query("Read the README.md file")
-
Approval callback is invoked - returns
PermissionResultAllow() -
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_inputisNoneby default (not explicitly set)- Code falls back to
argumentsvariable (from permission request parsing) argumentscan 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_inputWhy this works:
updatedInputis optional per the control protocol- If omitted, Claude uses original arguments
- If present (even as
{}), Claude uses the provided value - Fix: Only include
updatedInputwhen 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:
- Should
updatedInputbe omitted if not modifying arguments? - Does
updatedInput: {}mean "use empty arguments" or "no modification"? - Is there official documentation on the
control_request/control_responseprotocol?
This would help developers implement the permission system correctly.
Workaround
Until fixed, users can:
- Use
permission_mode="bypass_permissions"(no approval prompts) - Always set
updated_inputexplicitly inPermissionResultAllow:async def can_use_tool(tool_name: str, arguments: dict, context): # Explicitly pass through original arguments return PermissionResultAllow(updated_input=arguments)
- Note: Workaround format code to unified style #2 may not work if
argumentsis 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)