Skip to content

fix(vision): forward MCP tool result images to vision-capable models#79

Merged
ltmoerdani merged 1 commit into
mainfrom
fix/issue-77-mcp-image-tool-result
Jul 20, 2026
Merged

fix(vision): forward MCP tool result images to vision-capable models#79
ltmoerdani merged 1 commit into
mainfrom
fix/issue-77-mcp-image-tool-result

Conversation

@ltmoerdani

Copy link
Copy Markdown
Owner

What

Fixes #77.

MCP tool result images (e.g. chrome-devtools-mcp screenshots) were silently dropped during serialization — vision-capable models like Kimi K2.7 honestly reported "I cannot see the image". Pasted image attachments worked because they hit a different code path; only images nested inside LanguageModelToolResultPart.content were lost.

Why

partToText() handled TextPart / ToolCallPart / internal DataPart / string, but image DataParts nested inside tool results fell through to the catch-all return "". The model received an empty tool message.

How

  • convertMessage() now walks part.content and emits a multimodal OpenAiContentPart[] on the tool message when any image is present. Text-only tool results stay byte-identical to before.
  • Per-transport builders updated for Anthropic (tool_result.content: AnthropicContentBlock[]), Google Gemini (functionResponse.parts: [{inlineData}]), and Responses API (degrades to placeholder note — API is string-only).
  • Added MAX_TOOL_RESULT_IMAGE_BYTES = 1_000_000 size guard after manual testing revealed agent screenshot loops producing 4.6 MB payloads that triggered upstream 400.

Drive-by fixes

  • provideLanguageModelChatInformation: replaced per-model log spam (22 lines × ~3 calls/sec) with one summary line per invocation.
  • fetchModels: replaced modal showWarningMessage on transient upstream 400/503 with Output-channel log. The bundled fallbackModels snapshot keeps the picker functional, so the popup was just noise — especially on auto-registered opencodezen-agent that the user may not actively use.

Verification

  • tsc -p ./ clean
  • node --test out/test/**/*.test.js — 107/107 pass, no regression
  • Manual test with chrome-devtools-mcp + OpenCode Go model: model read and described the screenshot

Docs

  • docs/issues/34-20260720-mcp-tool-result-image-dropped.md — root-cause investigation
  • docs/features/12-20260720-mcp-tool-result-image-support.md — architecture & per-transport behavior
  • CHANGELOG.md [Unreleased]

Notes for review

  • Text-only tool results should be byte-identical to the previous behavior — the multimodal array only kicks in when an image is actually present. Worth a sanity check on that invariant.
  • Responses API path can't carry images (API limit). Only affects gpt-5-codex today; placeholder note is intentional.

Images nested inside LanguageModelToolResultPart.content (e.g. screenshots
returned by chrome-devtools-mcp, playwright-mcp) were silently dropped by
convertMessage(): partToText() returned "" for any image DataPart via its
catch-all fallback. The result was an empty tool message sent to the model,
so vision-capable models like Kimi K2.7 Code honestly reported they could
not see the image — even though the same model worked fine when the image
was pasted directly into chat.

Root cause: partToText() handled TextPart / ToolCallPart / internal DataPart
/ string, but image DataParts nested inside tool results fell through to the
catch-all return "". Top-level image attachments worked because they hit a
separate handler in convertMessage() that explicitly checked
part.mimeType.startsWith("image/").

Fix:
- convertMessage(): walk part.content and partition each item into either
  text (via partToText) or an OpenAiContentPart image part (via
  dataPartToBase64). Emit a multimodal OpenAiContentPart[] on the tool
  message when any image is present; otherwise emit a plain string so
  text-only tool results stay byte-identical to the previous behavior.
- AnthropicToolResultBlock.content type widened from string to
  string | AnthropicContentBlock[]. New anthropicToolResultContent() returns
  the string form when there are no images and the array form (text + image
  blocks) only when an image is present.
- responsesInputItemsFromMessage() tool branch: new responsesToolOutput()
  helper that degrades images to a placeholder note (Responses API
  function_call_output.output is string-only).
- googleContentsFromMessages() tool branch: new
  googleFunctionResponseContent() that emits parts:[{text},{inlineData}]
  when an image is present.
- New MAX_TOOL_RESULT_IMAGE_BYTES = 1_000_000 constant. Oversized images
  (>1 MB raw bytes) are replaced with an actionable placeholder note so a
  single full-page MCP screenshot can't push the request payload past the
  upstream limit. Manual testing revealed agent screenshot loops producing
  4.6 MB payloads that triggered upstream 400 "Upstream request failed".

Drive-by log noise reduction (pre-existing, surfaced during testing):
- provideLanguageModelChatInformation: replaced per-model log spam (22 lines
  per call x ~3 calls/sec) with one summary line per invocation.
- fetchModels: replaced vscode.window.showWarningMessage on transient
  upstream 400/503 (common from OpenCode's shared gateway, especially on
  auto-registered AGENT_* variants the user may not actively use) with an
  Output-channel log. Bundled fallbackModels snapshot keeps the picker
  functional.

Verification:
- tsc -p ./ exit 0 (TypeScript strict, no errors)
- node --test out/test/**/*.test.js — 107/107 pass, 0 regression
- Manual test: chrome-devtools-mcp + OpenCode Go model successfully read
  and described the returned screenshot

Docs:
- docs/issues/34-20260720-mcp-tool-result-image-dropped.md (root-cause
  investigation + timeline + follow-up bugs uncovered during testing)
- docs/features/12-20260720-mcp-tool-result-image-support.md (architecture,
  per-transport behavior, code locations, limitations)
- CHANGELOG.md [Unreleased] section
- docs/devlog.md session entry

Fixes #77
@ltmoerdani
ltmoerdani merged commit 742f899 into main Jul 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Models connected via extension cannot access images returned by MCP tools

1 participant