Summary
convertContentBlocks in the Anthropic provider/transport mistranslates non-image, non-text MCP tool-result blocks (notably resource_link, also resource/audio) into malformed image blocks with undefined data/media_type. Anthropic's API rejects these with HTTP 400, and because the bad block is now in the conversation history, every subsequent request in the session also 400s — the session is permanently stuck until history is reset.
Affected code
src/llm/providers/anthropic.ts — convertContentBlocks (~L121)
src/agents/anthropic-transport-stream.ts — convertContentBlocks (~L261)
Both have the same shape:
const hasImages = content.some((c) => c.type === "image");
if (!hasImages) { /* join text */ }
const blocks = content.map((block) => {
if (block.type === "text") return { type: "text", ... };
return { type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } }; // <-- bug
});
The else branch assumes "not text => image". But MCP CallToolResult.content[] can include resource_link, resource, and audio blocks. A resource_link has no data/mimeType, so the produced block is { type:"image", source:{ type:"base64", media_type: undefined, data: undefined } }.
Trigger / repro
- Use an MCP server whose tool returns a
resource_link content block (e.g. Superhuman Mail get_attachment for non-image files like .docx/.pdf).
- The tool result is materialized verbatim by
toAgentToolResult (agent-bundle-mcp-materialize.ts) — resource_link passes through untouched.
convertContentBlocks coerces it to an empty image block.
- Anthropic returns 400; the malformed block stays in history; all later turns 400.
Expected
Only emit an image block when the block is actually a valid image (non-empty data + mimeType). Convert all other block types to text:
resource_link → title ?? name ?? uri
resource → resource.text ?? resource.uri
audio → [audio <mime>]
- unknown →
JSON.stringify(block)
This also matches the existing extractText/contentToUserContent handling elsewhere in the runtime (runtime ACP path already handles resource_link).
Suggested fix
Add isValidImageBlock() + blockToText() helpers and route non-image blocks through blockToText instead of the image branch (in both files).
Environment
- openclaw
2026.6.2 (also reproduced in installed dist)
- Provider: anthropic (claude-opus-4-8)
- Repro MCP: Superhuman Mail remote MCP,
get_attachment on a .docx
Notes
I've patched my local dist/anthropic-*.js with the helper approach and the 400/poison loop is gone. Happy to open a PR for both source files if useful.
Summary
convertContentBlocksin the Anthropic provider/transport mistranslates non-image, non-text MCP tool-result blocks (notablyresource_link, alsoresource/audio) into malformedimageblocks withundefineddata/media_type. Anthropic's API rejects these with HTTP 400, and because the bad block is now in the conversation history, every subsequent request in the session also 400s — the session is permanently stuck until history is reset.Affected code
src/llm/providers/anthropic.ts—convertContentBlocks(~L121)src/agents/anthropic-transport-stream.ts—convertContentBlocks(~L261)Both have the same shape:
The
elsebranch assumes "not text => image". But MCPCallToolResult.content[]can includeresource_link,resource, andaudioblocks. Aresource_linkhas nodata/mimeType, so the produced block is{ type:"image", source:{ type:"base64", media_type: undefined, data: undefined } }.Trigger / repro
resource_linkcontent block (e.g. Superhuman Mailget_attachmentfor non-image files like.docx/.pdf).toAgentToolResult(agent-bundle-mcp-materialize.ts) — resource_link passes through untouched.convertContentBlockscoerces it to an emptyimageblock.Expected
Only emit an
imageblock when the block is actually a valid image (non-emptydata+mimeType). Convert all other block types to text:resource_link→title ?? name ?? uriresource→resource.text ?? resource.uriaudio→[audio <mime>]JSON.stringify(block)This also matches the existing
extractText/contentToUserContenthandling elsewhere in the runtime (runtime ACP path already handlesresource_link).Suggested fix
Add
isValidImageBlock()+blockToText()helpers and route non-image blocks throughblockToTextinstead of the image branch (in both files).Environment
2026.6.2(also reproduced in installeddist)get_attachmenton a.docxNotes
I've patched my local
dist/anthropic-*.jswith the helper approach and the 400/poison loop is gone. Happy to open a PR for both source files if useful.