Skip to content

fix: align mcp transform pipeline with Anthropic Claude Code 2.1.128#417

Merged
claude-code-best merged 1 commit intoclaude-code-best:mainfrom
shaleloop:sync/mcp-transform-2.1.128
May 6, 2026
Merged

fix: align mcp transform pipeline with Anthropic Claude Code 2.1.128#417
claude-code-best merged 1 commit intoclaude-code-best:mainfrom
shaleloop:sync/mcp-transform-2.1.128

Conversation

@shaleloop
Copy link
Copy Markdown
Contributor

@shaleloop shaleloop commented May 5, 2026

Aligns the MCP transform pipeline (transformResultContenttransformMCPResultprocessMCPResultcallMCPTool*) with Anthropic's Claude Code 2.1.128. Adds an ImageLimits parameter chain and three small behavior additions that match upstream.

What changed

  • New ImageLimits type in src/utils/imageResizer.ts. maybeResizeAndDownsampleImageBuffer accepts an optional limits?: ImageLimits 4th parameter; when provided, overrides the module-level defaults (IMAGE_TARGET_RAW_SIZE, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT, API_IMAGE_MAX_BASE64_SIZE). When undefined, behavior is unchanged for current callers.

  • limits plumbed through callMCPToolWithUrlElicitationRetrycallMCPToolprocessMCPResulttransformMCPResulttransformResultContent and passed to the image case in the image and resource content branches.

  • _meta preservation in the text-block case of transformResultContent (gated on includeMeta=true). The tool-result path passes includeMeta=true; the prompt-handler call site keeps the default false, preserving prior behavior.

  • skipLargeOutput (5th param of processMCPResult): when true and the content has no images, returns content directly without large-output handling.

  • Unwrap-to-text in processMCPResult: when the large-string format gate is enabled (MCP_TRUNCATION_PROMPT_OVERRIDE env var, or tengu_mcp_subagent_prompt Statsig gate) AND the content is a single bare text block (no annotations, no _meta), unwraps to raw text and switches the format description to "Plain text". Default-off; gate-off behavior is unchanged.

Default behavior

All new parameters are optional with safe defaults; the new unwrap path is gated. Existing callers (imagePaste.ts, FileReadTool, BashTool/utils, imageProcessor.ts, the prompt handler) all work as before because they pass nothing for the new optional parameters.

Validation

  • bunx biome check src/services/mcp/client.ts src/utils/imageResizer.ts — clean
  • bun test — 2323 pass / 189 fail / 119 errors (identical to main baseline; pre-existing bun:bundle resolution issues unrelated to this change)
  • bun run typecheck — no new errors in modified files (pre-existing vite.config.ts errors in packages/remote-control-server are unchanged on main)
  • Verified structurally against the Anthropic 2.1.128 binary: function shapes, the IDE check, gate logic, _meta-unwrap pattern, and imageLimits: plumbing in this PR's implementation match the binary.
Binary marker counts (reviewer-verifiable via strings <binary> | grep -c <marker>)

Stable string literals — anchors that survive minification — provide reviewer-verifiable evidence of structural alignment with 2.1.128:

Marker Count
imageLimits: destructures 12
tengu_mcp_subagent_prompt gate ref 3
MCP_TRUNCATION_PROMPT_OVERRIDE env ref 3
q==="ide" IDE checks 2
!("_meta" in ...) unwrap eligibility 2
"contentArray" type tags 6

Function bodies (extracted by brace-balanced walk) match this PR's implementation modulo minifier identifier names.

Out of scope

The following items exist in the 2.1.128 binary but are not ported in this PR. Each is annotated with the binary symbol so the work can be picked up cleanly later.

  • TQH _meta-stripping pre-pass. Binary's jQ5 does let j = TQH(T) before the unwrap eligibility check. TQH walks the content array and, when any text block has a truthy _meta, returns a new array with _meta stripped from those blocks. The mirror passes content through without this step. With the unwrap gate off (default), no behavioral difference. With the gate on AND a server attaches truthy _meta to a single text block, binary would strip _meta and unwrap to plain text; mirror keeps _meta present and skips the unwrap (more conservative). Worth porting if the divergence shows up in practice.

  • pV7 line-stats + prompt-strategy branching. Anthropic 2.1.128's format helper takes an optional 5th arg lineStats: { count, maxLen } AND branches the model-facing instructions three ways: lineStats === undefined → jq-on-the-file (tree-structured content); lineStats defined but maxLen exceeds the token budget → python character-slicing (read()[A:B] spans); lineStats defined and line-readable → offset/limit chunks of floor(budget/(maxLen+8)) lines. Mirror's getLargeOutputInstructions keeps the original flat strategy. A faithful port requires reverse-engineering pV7's helpers (fqH for token budget, Eh5, CP8(), m9) and updates to mcpOutputStorage.ts — left for follow-up.

  • Three additional named options on vZ8's destructure: toolExecution, taskRegistry, toolUseId. Binary accepts them but the function bodies of vZ8, jQ5, wQ5, and ZZ8 don't reference them — they're likely consumed by sub-functions in the elicitation or analytics chain that weren't traced. Adding accepted-but-unread params would lie about what the function does, so they're left out until the consumer paths are identified.


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by CodeRabbit

Release Notes

  • New Features

    • Added image resizing and optimization for MCP tool results, improving handling of large images.
    • Introduced support for large outputs from MCP tools with automatic handling and metadata persistence.
  • Improvements

    • Enhanced image processing pipeline with flexible per-call configuration for better resource management.

Add ImageLimits type and plumb optional limits through the chain:
callMCPTool/callMCPToolWithUrlElicitationRetry -> processMCPResult ->
transformMCPResult -> transformResultContent -> maybeResizeAndDownsampleImageBuffer.
When provided, limits override the module-level defaults
(IMAGE_TARGET_RAW_SIZE, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT,
API_IMAGE_MAX_BASE64_SIZE) inside maybeResizeAndDownsampleImageBuffer.
When undefined, behavior is unchanged for current callers.

Add _meta preservation in the text-block case of transformResultContent
(only when the caller opts in via includeMeta=true). transformMCPResult
passes includeMeta=true on the tool-result path; the prompt-handler call
site keeps the default false, preserving prior behavior.

Add skipLargeOutput early-return in processMCPResult after the IDE check:
when the caller passes skipLargeOutput=true and the content has no images,
the function returns content directly without large-output handling.

Add unwrap-to-text in processMCPResult for the persisted-content path:
when the large-string format gate is enabled
(MCP_TRUNCATION_PROMPT_OVERRIDE env var, or
tengu_mcp_subagent_prompt Statsig gate), and the content is a single
bare text block (no annotations, no _meta), unwrap to raw text and
switch the format description to 'Plain text'. Default-off; gate-off
behavior is unchanged.

Verified structurally against the 2.1.128 binary: function signatures,
the IDE check, gate logic, _meta-unwrap pattern, and imageLimits
plumbing match this implementation.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

This PR adds image processing and large-output handling to the MCP client by introducing configurable ImageLimits and threading them through the tool result processing pipeline. Image resizing now respects per-call limits instead of hardcoded constants, and large outputs can be unwrapped to plain text and persisted based on feature gates.

Changes

Image Processing and Large-Output Support

Layer / File(s) Summary
Data Shape
src/utils/imageResizer.ts
New ImageLimits interface defines per-call constraints: targetRawSize, maxWidth, maxHeight, maxBase64Size.
Core Image Resizing
src/utils/imageResizer.ts
maybeResizeAndDownsampleImageBuffer refactored to accept optional limits parameter, replacing hardcoded constants with per-call values throughout compression and dimension checks.
Large-Output Utilities
src/services/mcp/client.ts
New helpers: unwrapSingleTextBlock and isLargeStringFormatEnabled gate enable unwrapping single text blocks and persisting large outputs based on feature flag.
Content Transformation
src/services/mcp/client.ts
transformResultContent extended to accept limits and includeMeta parameters; image resizing calls now pass limits, and text blocks can attach metadata when requested.
Result Processing
src/services/mcp/client.ts
transformMCPResult and processMCPResult updated to accept limits and skipLargeOutput flags; large-output logic computes persisted content descriptions and conditionally unwraps text.
Tool Invocation Pipeline
src/services/mcp/client.ts
callMCPTool and callMCPToolWithUrlElicitationRetry signatures extended to thread imageLimits and hasResultSizeAnnotation through the tool call path to enable limits-aware result handling.
Integration & Imports
src/services/mcp/client.ts
Import statement updated to include ImageLimits and maybeResizeAndDownsampleImageBuffer; tool result handler updated to pass limits and annotation flag to processing functions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hops through image frames,
Resizing pixels by the names,
Large outputs shrink with careful grace,
Each limit finds its rightful place. 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: aligning the MCP transform pipeline with a specific version (2.1.128) of Anthropic Claude Code, which matches the primary objective of threading image limits, large-output handling, and content transformation improvements throughout the MCP client pipeline.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/utils/imageResizer.ts (2)

439-445: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Report the configured limit, not a hardcoded 5MB.

This error text still says 5MB even though maxBase64Size is now caller-overridable, so the fallback can return the wrong remediation guidance.

Suggested fix
-        : `Unable to resize image (${formatFileSize(originalSize)} raw, ${formatFileSize(base64Size)} base64). ` +
-            `The image exceeds the 5MB API limit and compression failed. ` +
+        : `Unable to resize image (${formatFileSize(originalSize)} raw, ${formatFileSize(base64Size)} base64). ` +
+            `The image exceeds the ${formatFileSize(maxBase64Size)} base64 API limit and compression failed. ` +
             `Please resize the image manually or use a smaller image.`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/imageResizer.ts` around lines 439 - 445, The error message in the
ImageResizeError thrown in imageResizer.ts uses a hardcoded "5MB" even though
maxBase64Size is now configurable; update the fallback message to report the
configured limit by referencing the maxBase64Size value (formatted with
formatFileSize) instead of "5MB" and ensure the overDim branch still references
maxWidth/maxHeight; adjust the thrown message construction around
ImageResizeError (where originalSize and base64Size are used) to include
formatFileSize(maxBase64Size) so the remediation guidance accurately reflects
the caller-overridable limit.

419-429: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when custom dimension limits can't be verified.

overDim only inspects PNG headers. If getImageProcessor() fails for a JPEG/GIF/WebP and a caller supplied tighter maxWidth/maxHeight, this branch still returns the original buffer whenever the base64 size fits. That bypasses the new per-call dimension contract and can still hand an oversized image to the downstream API.

Suggested guard
     const detected = detectImageFormatFromBuffer(imageBuffer)
     const normalizedExt = detected.slice(6) // Remove 'image/' prefix

     // Calculate the base64 size (API limit is on base64-encoded length)
     const base64Size = Math.ceil((originalSize * 4) / 3)
+    const hasCustomDimensionLimits =
+      limits !== undefined &&
+      (maxWidth !== IMAGE_MAX_WIDTH || maxHeight !== IMAGE_MAX_HEIGHT)

     // Size-under-5MB does not imply dimensions-under-cap. Don't return the
     // raw buffer if the PNG header says it's oversized — fall through to
     // ImageResizeError instead. PNG sig is 8 bytes, IHDR dims at 16-24.
     const overDim =
       imageBuffer.length >= 24 &&
       imageBuffer[0] === 0x89 &&
       imageBuffer[1] === 0x50 &&
       imageBuffer[2] === 0x4e &&
       imageBuffer[3] === 0x47 &&
       (imageBuffer.readUInt32BE(16) > maxWidth ||
         imageBuffer.readUInt32BE(20) > maxHeight)

     // If original image's base64 encoding is within API limit, allow it through uncompressed
-    if (base64Size <= maxBase64Size && !overDim) {
+    if (
+      base64Size <= maxBase64Size &&
+      (!hasCustomDimensionLimits || (detected === 'image/png' && !overDim))
+    ) {
       logEvent('tengu_image_resize_fallback', {
         original_size_bytes: originalSize,
         base64_size_bytes: base64Size,
         error_type: errorType,
       })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/imageResizer.ts` around lines 419 - 429, The current logic only
sets overDim for PNGs, allowing unverified JPEG/GIF/WebP through when base64
size fits; change to "fail closed": if callers pass maxWidth or maxHeight and
you cannot reliably read dimensions (e.g., imageBuffer isn't a PNG header and
getImageProcessor() cannot parse dimensions), set overDim = true so the code
does not bypass resizing. Update the overDim computation to reference
imageBuffer, overDim, base64Size, maxBase64Size and incorporate a check that
attempts to use getImageProcessor() (or other dimension reader) and treats
failures/unsupported formats as overDim = true when maxWidth/maxHeight are
provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/mcp/client.ts`:
- Around line 2497-2508: The text-case transformer currently only copies `_meta`
and thus strips `annotations` before `unwrapSingleTextBlock()` runs; change the
`case 'text'` path (where `block: ContentBlockParam` is constructed from
`resultContent.text`) to also preserve any `resultContent.annotations` by
assigning them onto the block (e.g. if (resultContent.annotations) (block as {
annotations?: unknown }).annotations = resultContent.annotations), so annotated
text blocks are not converted into bare text and lost by the unwrapping logic.

---

Outside diff comments:
In `@src/utils/imageResizer.ts`:
- Around line 439-445: The error message in the ImageResizeError thrown in
imageResizer.ts uses a hardcoded "5MB" even though maxBase64Size is now
configurable; update the fallback message to report the configured limit by
referencing the maxBase64Size value (formatted with formatFileSize) instead of
"5MB" and ensure the overDim branch still references maxWidth/maxHeight; adjust
the thrown message construction around ImageResizeError (where originalSize and
base64Size are used) to include formatFileSize(maxBase64Size) so the remediation
guidance accurately reflects the caller-overridable limit.
- Around line 419-429: The current logic only sets overDim for PNGs, allowing
unverified JPEG/GIF/WebP through when base64 size fits; change to "fail closed":
if callers pass maxWidth or maxHeight and you cannot reliably read dimensions
(e.g., imageBuffer isn't a PNG header and getImageProcessor() cannot parse
dimensions), set overDim = true so the code does not bypass resizing. Update the
overDim computation to reference imageBuffer, overDim, base64Size, maxBase64Size
and incorporate a check that attempts to use getImageProcessor() (or other
dimension reader) and treats failures/unsupported formats as overDim = true when
maxWidth/maxHeight are provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f38ce597-f465-4b23-b2f2-baef9e94c3b6

📥 Commits

Reviewing files that changed from the base of the PR and between 872ee28 and 26ddbda.

📒 Files selected for processing (2)
  • src/services/mcp/client.ts
  • src/utils/imageResizer.ts

Comment on lines +2497 to +2508
case 'text': {
const block: ContentBlockParam = {
type: 'text',
text: resultContent.text,
}
if (includeMeta) {
const meta = resultContent._meta
if (meta) {
;(block as { _meta?: unknown })._meta = meta
}
}
return [block]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve annotations before the single-text unwrap path runs.

unwrapSingleTextBlock() explicitly refuses annotated text blocks, but this transformer only carries _meta. A single annotated MCP text result is therefore converted into a bare text block here and later qualifies for plain-text unwrapping, which drops the annotations on the persisted large-output path.

Suggested fix
     case 'text': {
       const block: ContentBlockParam = {
         type: 'text',
         text: resultContent.text,
       }
+      if ('annotations' in resultContent && resultContent.annotations) {
+        ;(block as { annotations?: unknown }).annotations =
+          resultContent.annotations
+      }
       if (includeMeta) {
         const meta = resultContent._meta
         if (meta) {
           ;(block as { _meta?: unknown })._meta = meta
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/mcp/client.ts` around lines 2497 - 2508, The text-case
transformer currently only copies `_meta` and thus strips `annotations` before
`unwrapSingleTextBlock()` runs; change the `case 'text'` path (where `block:
ContentBlockParam` is constructed from `resultContent.text`) to also preserve
any `resultContent.annotations` by assigning them onto the block (e.g. if
(resultContent.annotations) (block as { annotations?: unknown }).annotations =
resultContent.annotations), so annotated text blocks are not converted into bare
text and lost by the unwrapping logic.

@claude-code-best claude-code-best merged commit c4e9efb into claude-code-best:main May 6, 2026
3 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.

2 participants