import anyio
from datetime import datetime
import json
from typing import Any
from dotenv import load_dotenv
from claude_agent_sdk import (
ClaudeAgentOptions,
AssistantMessage,
TextBlock,
ClaudeSDKClient,
HookMatcher,
HookContext,
tool,
create_sdk_mcp_server,
)
DEFAULT_MODEL = "claude-haiku-4-5"
@tool(
"handler_a",
"Handle A function that returns data",
{"type": "object", "properties": {}},
)
async def handler_a(args):
return {
"content": [
{
"type": "text",
"text": json.dumps({"status": "S", "date": "06/01/2024"}),
}
]
}
@tool(
"handler_b",
"Handle B function that returns data",
{"type": "object", "properties": {}},
)
async def handler_b(args):
return {
"content": [
{
"type": "text",
"text": json.dumps({"status": 200, "date": datetime.now().timestamp()}),
}
]
}
@tool(
"handler_c",
"Handle C function that returns data",
{"type": "object", "properties": {}},
)
async def handler_c(args):
return {
"content": [
{
"type": "text",
"text": json.dumps(
{"status": "success", "date": "2022-09-27T18:00:00"}
),
}
]
}
def convert_status_letter_to_status(status_letter: str) -> str:
mapping = {"S": "success", "F": "failure", "P": "pending"}
return mapping.get(status_letter, "unknown")
def get_normalized_status(status: Any) -> str:
print("[DEBUG] Normalizing status: ", status)
if isinstance(status, int):
if status == 200:
return "success"
else:
return "failure"
elif isinstance(status, str):
if status in ["success", "failure", "pending"]:
return status
else:
return convert_status_letter_to_status(status)
else:
return "unknown"
def get_normalized_date(date: Any) -> str:
print("[DEBUG] Normalizing date: ", date)
if isinstance(date, int) or isinstance(date, float):
return datetime.fromtimestamp(date).isoformat()
elif isinstance(date, str):
try:
return datetime.strptime(date, "%d/%m/%Y").isoformat()
except ValueError:
try:
return datetime.fromisoformat(date).isoformat()
except ValueError:
return "unknown"
else:
return "unknown"
async def post_use_handler_hook(
input_data: dict[str, Any], tool_use_id: str | None, context: HookContext
) -> dict[str, Any]:
tool_name = input_data.get("tool_name")
tool_response = input_data.get("tool_response")
if isinstance(tool_response, list):
content = tool_response # bare list — your runtime
elif isinstance(tool_response, dict):
content = tool_response.get("content", []) # wrapped dict — docs version
else:
return {}
if not content:
return {}
data = json.loads(content[0].get("text", "{}"))
print(f"[post_use_handler_hook] Lookup handler called after {tool_name} is used")
print(f"[post_use_handler_hook] Response data: ", data)
status = get_normalized_status(data.get("status"))
date = get_normalized_date(data.get("date"))
print(
f"[post_use_handler_hook] Normalized status: {status}, Normalized date: {date}"
)
dump_data = json.dumps({"status": status, "date": date})
print(f"[post_use_handler_hook] Dumping normalized data: {dump_data}")
return {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"updatedMCPToolOutput": {
"content": [
{
"type": "text",
"text": dump_data,
}
]
},
}
}
server = create_sdk_mcp_server(
name="customer-support-mcp",
version="1.0.0",
tools=[handler_a, handler_b, handler_c],
)
async def main():
options = ClaudeAgentOptions(
model=DEFAULT_MODEL,
mcp_servers={"tools": server},
allowed_tools=[
"mcp__tools__handler_a",
"mcp__tools__handler_b",
"mcp__tools__handler_c",
],
hooks={
"PostToolUse": [
HookMatcher(
matcher="mcp__tools__handler_a|mcp__tools__handler_b|mcp__tools__handler_c",
hooks=[post_use_handler_hook],
)
],
},
)
async with ClaudeSDKClient(options) as client:
await client.query(
prompt="Collect information from the three handlers summarize and provide results."
)
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text)
if __name__ == "__main__":
load_dotenv()
anyio.run(main)
[post_use_handler_hook] Lookup handler called after mcp__tools__handler_a is used
[post_use_handler_hook] Response data: {'status': 'S', 'date': '06/01/2024'}
[post_use_handler_hook] Normalized status: success, Normalized date: 2024-01-06T00:00:00
[post_use_handler_hook] Dumping normalized data: {"status": "success", "date": "2024-01-06T00:00:00"}
[post_use_handler_hook] Lookup handler called after mcp__tools__handler_b is used
[post_use_handler_hook] Response data: {'status': 200, 'date': 1774904901.024368}
[post_use_handler_hook] Normalized status: success, Normalized date: 2026-03-30T23:08:21.024368
[post_use_handler_hook] Dumping normalized data: {"status": "success", "date": "2026-03-30T23:08:21.024368"}
[post_use_handler_hook] Lookup handler called after mcp__tools__handler_c is used
[post_use_handler_hook] Response data: {'status': 'success', 'date': '2022-09-27T18:00:00'}
[post_use_handler_hook] Normalized status: success, Normalized date: 2022-09-27T18:00:00
[post_use_handler_hook] Dumping normalized data: {"status": "success", "date": "2022-09-27T18:00:00"}
H.reduce is not a function.
(In 'H.reduce((_,$)=>_+($.type==="text"?$.text.length:0),0)', 'H.reduce' is undefined)
All three handlers reported as failed in the UI, with the message:
The handlers are attempting to use a .reduce() method on an object or variable named H, but that object/variable either doesn't exist or doesn't have a reduce method available.
Expected Behavior
The hook returns a valid updatedMCPToolOutput with a properly shaped content array
The renderer receives content as a JavaScript array and calls .reduce() successfully
All three tool results display the normalized status and date values
The assistant message is not truncated
Additional Findings
1. tool_response structure inconsistency
The official SDK documentation states tool_response in PostToolUseHookInput is:
python{"content": [{"type": "text", "text": "..."}]} # dict with content key
But at runtime the SDK delivers it as a bare list:
python[{"type": "text", "text": "..."}] # list directly
This required a defensive isinstance check to handle both shapes. The docs and runtime are inconsistent.
Environment
Package: claude-agent-sdk
Version: latest (March 2026, v0.1.48)
Python: 3.10+
Async runtime: anyio
Platform: Claude.ai (web/mobile renderer)
Description$.type==="text"?$ .text.length:0),0)', 'H.reduce' is undefined)
When a PostToolUse hook returns a valid updatedMCPToolOutput, the python script crashes with:
H.reduce is not a function.
(In 'H.reduce((,$)=>+(
This happens across all tools registered on the same MCP server simultaneously, despite the hook executing correctly and returning a properly shaped content array. The Python-side hook logs confirm all three tools normalize and return valid output — the crash originates in the JavaScript rendering layer.
Python code:
Actual Behavior
Python-side hook logs confirm correct execution:
Renderer crashes with: