Skip to content

MCP tool result with type: "resource" is converted to "(see attached image)" placeholder #75674

Description

@Jedi-Pz

Bug Description

When an MCP tool returns type: "resource" in its content array, OpenClaw incorrectly converts it to a (see attached image) placeholder instead of extracting the resource.text field.

Reproduction Steps

  1. Configure QMD as the memory backend ("memory": { "backend": "qmd" })
  2. Call the qmd__get tool via OpenClaw:
    {"name": "get", "arguments": {"file": "memory/2026-05-01.md"}}
  3. The MCP server returns a valid EmbeddedResource:
    {
      "content": [
        {
          "type": "resource",
          "resource": {
            "uri": "qmd://memory-root-main/memory/2026-05-01.md",
            "mimeType": "text/markdown",
            "text": "# 2026-05-01\n\nSome markdown content here..."
          }
        }
      ]
    }
  4. OpenClaw displays (see attached image) in the tool output instead of the markdown text.

Expected vs Actual Behavior

Aspect Expected Actual
Tool output The resource.text content (markdown text) (see attached image)
LLM context The text is passed to the model A placeholder is passed

Root Cause Analysis (Source Code Level)

I traced the issue through the OpenClaw distribution bundle (2026.4.27). The problem is that the entire content pipeline lacks support for the MCP EmbeddedResource type.

1. contentToText ignores resource

File: dist/compaction-successor-transcript-BwDunTlM.js:581-585

function contentToText(content) {
  // ...
  return content.filter((part) => Boolean(part) && typeof part === "object")
    .filter((part) => (part.type === "text" || part.type === "input_text" || part.type === "output_text") && typeof part.text === "string")
    .map((part) => part.text).join("");
}

Only text / input_text / output_text are recognized. type: "resource" is silently filtered out, returning an empty string.

2. contentToOpenAIParts ignores resource

File: dist/compaction-successor-transcript-BwDunTlM.js:586-644

for (const part of content) {
  if ((part.type === "text" || part.type === "input_text" || part.type === "output_text") && typeof part.text === "string") {
    parts.push({ type: "input_text", text: part.text });
    continue;
  }
  if (!includeImages) continue;
  if (part.type === "image" && typeof part.data === "string") { /* ... */ }
  if (part.type === "input_image" && part.source && ...) { /* ... */ }
}

Only text and image/input_image are handled. resource is skipped, producing an empty parts array.

3. OpenAI transport falls back to placeholder

File: dist/openai-transport-stream-DwlW8wDu.js:634-649

} else if (msg.role === "toolResult") {
  const textResult = msg.content.filter((item) => item.type === "text").map((item) => item.text).join("\n");
  const hasImages = msg.content.some((item) => item.type === "image");
  // ...
  output: hasImages && model.input.includes("image")
    ? [...]
    : sanitizeTransportPayloadText(textResult || "(see attached image)")
}
  • textResult is empty (resource is not text)
  • hasImages is false (resource is not image)
  • Falls through to sanitizeTransportPayloadText("" || "(see attached image)")

4. Anthropic transport has the same issue

File: dist/provider-stream-CQzDRxyR.js:104-126

function convertContentBlocks(content) {
  if (!content.some((item) => item.type === "image"))
    return sanitizeNonEmptyTransportPayloadText(content.map((item) => "text" in item ? item.text : "").join("\n"));
  // ...
  if (!blocks.some((block) => block.type === "text"))
    blocks.unshift({ type: "text", text: "(see attached image)" });
}

Non-text content is treated as image, and when no text blocks exist, the placeholder is inserted.

Proposed Fix

Add resource type handling to the content pipeline:

// In contentToText
if (part.type === "resource" && part.resource && typeof part.resource.text === "string") {
  return part.resource.text;
}

// In contentToOpenAIParts
if (part.type === "resource" && part.resource && typeof part.resource.text === "string") {
  parts.push({ type: "input_text", text: part.resource.text });
  continue;
}

Additionally, the transport layer should not blindly fall back to (see attached image) for unknown content types. It should either extract text from known types or pass through an error/unknown-type indicator.

Environment

  • OpenClaw version: 2026.4.27
  • Platform: macOS
  • Memory backend: QMD
  • API provider: Xiaomi MiMo (openai-completions)
  • Node.js: v22.22.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:data-lossCan lose, corrupt, or silently drop user/session/config data.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions