openclaw - ✅(Solved) Fix HTTP MCP gateway strips image/audio/resource fields from tool results (normalizeToolCallContent) [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#68037Fetched 2026-04-18 05:54:16
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
cross-referenced ×2referenced ×2

normalizeToolCallContent in the HTTP MCP gateway rewrites every content block to {type, text} before returning tools/call results to the MCP client. For non-text blocks (image, audio, resource, resource_link) the payload fields (data, mimeType, resource, uri, name) are dropped. Downstream Zod validation against the MCP content-block schema then fails on every image/audio/resource-producing tool.

Root Cause

src/gateway/mcp-http.handlers.ts, function normalizeToolCallContent:

if (Array.isArray(content)) return content.map((block) => ({
    type: block.type ?? "text",
    text: block.text ?? (typeof block === "string" ? block : JSON.stringify(block))
}));

The .map only preserves type and text. Everything else (data, mimeType, resource, uri, name, annotations, ...) is discarded. Image blocks therefore arrive as {type: "image", text: undefined} and fail the ImageContentSchema check on data/mimeType.

Fix Action

Fixed

PR fix notes

PR #68043: fix(gateway): preserve non-text MCP content blocks in HTTP gateway tool-call results

Description (problem / solution / changelog)

Summary

Fixes #68037 — the HTTP MCP gateway was dropping every non-text field from tools/call results, breaking browser.screenshot and every other tool that returns an image/audio/resource block.

Root cause

normalizeToolCallContent in src/gateway/mcp-http.handlers.ts mapped every returned content block to {type: block.type ?? "text", text: block.text ?? …}:

```ts return content.map((block: { type?: string; text?: string }) => ({ type: (block.type ?? "text") as "text", text: block.text ?? (typeof block === "string" ? block : JSON.stringify(block)), })); ```

Image/audio/resource blocks have no text field, so they came out as {type: "image", text: undefined} and lost data, mimeType, uri, name, resource, annotations, etc. Downstream Zod validation against the MCP content-block schema then failed with "expected string, received undefined" on data/mimeType.

Fix

Pass typed blocks through unmodified; only synthesize a text block when the entry is a primitive string or an object without a recognized type field. Widen the return type from McpTextContent[] to an open McpContentBlock union so the pass-through survives TypeScript's inference.

```ts return content.map((block) => { if (block && typeof block === "object" && typeof (block as { type?: unknown }).type === "string") { return block as McpContentBlock; } return { type: "text", text: typeof block === "string" ? block : JSON.stringify(block), }; }); ```

This is the fix the reporter proposed (pass-through for valid typed blocks, fallback for everything else). If stricter validation is preferred later, the union can be narrowed to an explicit discriminated schema.

Test plan

  • Added six unit tests in `src/gateway/mcp-http.test.ts` covering:
    • image block pass-through with `data` + `mimeType` preserved
    • `resource_link` pass-through with `uri` + `name` preserved
    • audio block pass-through with `data` + `mimeType` preserved
    • existing text blocks untouched
    • untyped entries (primitive string, plain object) wrapped into text
    • fallback when `result.content` is absent entirely
  • `npx tsc --noEmit` — 247 baseline on `main`, 247 on branch (no new errors).
  • `pnpm exec oxlint` on touched files — 0 warnings, 0 errors.
  • Existing `mcp-http.test.ts` integration cases untouched.

Local full vitest is blocked by pre-existing `test/non-isolated-runner.ts` drift (reproduces on `main`) — CI will exercise the new tests normally.

Security / behavior notes

  • The fix trusts any block with a string `type` field. That matches what the MCP protocol-level schema validates client-side anyway: bad payloads fail Zod at the client with a cleaner error than today's silent field-drop.
  • No existing text-block behavior changes — strings and untyped plain objects still get wrapped, and a non-array `result` still collapses to a single text block.

Changed files

  • src/gateway/mcp-http.handlers.ts (modified, +15/-9)
  • src/gateway/mcp-http.test.ts (modified, +59/-0)

PR #68046: fix: pass through image/audio/resource content blocks in MCP HTTP gateway

Description (problem / solution / changelog)

Summary

Fixes the HTTP MCP gateway stripping image, audio, and resource fields from tool results.

Root Cause

The normalizeToolCallContent function was mapping all content blocks to only {type, text}, discarding other fields like data, mimeType, resource, uri, etc. This caused downstream Zod validation to fail for any tool returning non-text content.

Fix

  • Pass through valid content blocks directly to preserve all fields
  • Only apply fallback transformation for strings or malformed blocks
  • Updated type definition to include all MCP content block types

Test Plan

  • Added comprehensive test coverage for all content block types (text, image, audio, resource, resource_link)
  • All 9 new tests pass
  • Manually verified that image blocks now retain data and mimeType fields

Closes openclaw#68037

Changed files

  • src/gateway/mcp-http.handlers.test.ts (added, +130/-0)
  • src/gateway/mcp-http.handlers.ts (modified, +22/-9)

Code Example

[{"code":"invalid_union","path":["content",0],"message":"Invalid input", ...
  "expected":"string","path":["data"],"message":"Invalid input: expected string, received undefined"
  "expected":"string","path":["mimeType"],"message":"Invalid input: expected string, received undefined"
  "values":["resource_link"],"path":["type"], ...
}]

---

if (Array.isArray(content)) return content.map((block) => ({
    type: block.type ?? "text",
    text: block.text ?? (typeof block === "string" ? block : JSON.stringify(block))
}));

---

if (Array.isArray(content)) return content.map((block) => {
    if (block && typeof block === "object" && typeof block.type === "string") return block;
    return { type: "text", text: typeof block === "string" ? block : JSON.stringify(block) };
});
RAW_BUFFERClick to expand / collapse

Summary

normalizeToolCallContent in the HTTP MCP gateway rewrites every content block to {type, text} before returning tools/call results to the MCP client. For non-text blocks (image, audio, resource, resource_link) the payload fields (data, mimeType, resource, uri, name) are dropped. Downstream Zod validation against the MCP content-block schema then fails on every image/audio/resource-producing tool.

Environment

  • OpenClaw version: 2026.4.15
  • File: dist/mcp-http-LDZ2tv6U.js (bundled, source at src/gateway/mcp-http.handlers.ts)
  • Transport: HTTP MCP gateway (openclaw-gateway)

Repro

  1. Connect an MCP client to the HTTP gateway.
  2. Call the browser tool with action: "screenshot" (also reproduces with snapshot + labels: true, and any tool returning a non-text block).
  3. Client-side validation fails:
[{"code":"invalid_union","path":["content",0],"message":"Invalid input", ...
  "expected":"string","path":["data"],"message":"Invalid input: expected string, received undefined"
  "expected":"string","path":["mimeType"],"message":"Invalid input: expected string, received undefined"
  "values":["resource_link"],"path":["type"], ...
}]

Root cause

src/gateway/mcp-http.handlers.ts, function normalizeToolCallContent:

if (Array.isArray(content)) return content.map((block) => ({
    type: block.type ?? "text",
    text: block.text ?? (typeof block === "string" ? block : JSON.stringify(block))
}));

The .map only preserves type and text. Everything else (data, mimeType, resource, uri, name, annotations, ...) is discarded. Image blocks therefore arrive as {type: "image", text: undefined} and fail the ImageContentSchema check on data/mimeType.

Proposed fix (two-line change, pass-through for valid blocks)

if (Array.isArray(content)) return content.map((block) => {
    if (block && typeof block === "object" && typeof block.type === "string") return block;
    return { type: "text", text: typeof block === "string" ? block : JSON.stringify(block) };
});

This keeps the fallback for strings and malformed blocks, while trusting blocks that already declare a valid type. If stricter validation is preferred, swap the identity return for a per-type whitelist (text, image, audio, resource, resource_link) that copies the expected fields.

Impact

  • Completely blocks browser.screenshot over HTTP MCP.
  • Blocks any tool returning image, audio, resource, or resource_link blocks (TTS output, PDF captures, resource links, etc.).
  • Visual audits, screenshot-driven agents, and multimodal tool pipelines are unusable on the HTTP transport.

Happy to open a PR with the pass-through version plus a regression test if that's welcome.

extent analysis

TL;DR

The proposed fix involves modifying the normalizeToolCallContent function to pass through valid blocks without discarding their properties, ensuring that non-text blocks are properly handled.

Guidance

  • Verify that the issue is indeed caused by the normalizeToolCallContent function discarding properties of non-text blocks by checking the content array before and after the map operation.
  • Apply the proposed two-line change to the normalizeToolCallContent function to pass through valid blocks.
  • Consider adding a regression test to ensure that the fix does not introduce new issues.
  • If stricter validation is preferred, implement a per-type whitelist to copy the expected fields for trusted block types.

Example

if (Array.isArray(content)) return content.map((block) => {
    if (block && typeof block === "object" && typeof block.type === "string") return block;
    return { type: "text", text: typeof block === "string" ? block : JSON.stringify(block) };
});

Notes

The proposed fix assumes that the type property of a block is a reliable indicator of its validity. If this is not the case, additional validation may be necessary.

Recommendation

Apply the proposed workaround by modifying the normalizeToolCallContent function to pass through valid blocks, as it directly addresses the issue and ensures that non-text blocks are properly handled.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix HTTP MCP gateway strips image/audio/resource fields from tool results (normalizeToolCallContent) [2 pull requests, 1 participants]