Skip to content

Image content blocks >99 bytes fail with "illegal base64 at byte 0" #3123

Description

@PeeperFrog

Image Content Blocks >99 Bytes Fail Client-Side Decode in stdio Mode

Summary

Image content blocks returned via _mcp_content_blocks fail client-side base64 decoding when the decoded image bytes exceed 99 bytes, despite the server emitting spec-compliant MCP protocol messages. The issue manifests as "illegal base64 data at input byte 0" errors in the client, while server wire logs confirm correct base64 encoding with proper padding.

Environment

Server:

  • Python SDK: mcp==1.28.1
  • Python: 3.12
  • Transport: stdio (JSON-RPC over stdin/stdout)
  • OS: Linux (Ubuntu-based)

Client:

  • Claude Code (Anthropic's MCP client)
  • Error source: Go encoding/base64 (based on error message format)

Dependencies:

mcp==1.28.1
starlette==1.3.1
pydantic==2.13.4
fastapi==0.139.2

Issue Description

Observed Behavior

Images returned in MCP content blocks exhibit a binary size threshold at exactly 100 bytes (decoded image data):

Decoded Bytes Base64 Chars Result Error
≤99 ≤132 ✅ Renders None
≥100 ≥136 ❌ Fails "illegal base64 data at input byte 0"

Test Results

Pass Class (renders successfully):

  • 48 bytes (64 base64 chars): ✅ Visual confirmation
  • 99 bytes (132 base64 chars): ✅ Visual confirmation

Fail Class (decode error):

  • 102 bytes (136 base64 chars): ❌ "illegal base64 data at input byte 0"
  • 4,412 bytes (5,884 base64 chars): ❌ "illegal base64 data at input byte 0"
  • 5,108 bytes (6,812 base64 chars): ❌ "illegal base64 data at input byte 0"

Additional Finding (SDK Version Dependent)

With SDK 1.26.0, payloads >4KB exhibited a second error class:

  • "Incorrect padding" (Python binascii error)

This was fixed by upgrading to SDK 1.28.1, leaving only the 100-byte threshold issue.

Wire-Level Evidence

Server logs confirm spec-compliant image blocks are emitted:

[ISS-011-WIRE] Image block: data_len=64, first_char='U', last_char='B', 
               has_mimeType=True, block_keys=['type', 'data', 'mimeType']
Result: ✅ RENDERED

[ISS-011-WIRE] Image block: data_len=6812, first_char='U', last_char='=', 
               has_mimeType=True, block_keys=['type', 'data', 'mimeType']
Result: ❌ illegal base64 data at input byte 0

[ISS-011-WIRE] Image block: data_len=5884, first_char='U', last_char='=', 
               has_mimeType=True, block_keys=['type', 'data', 'mimeType']
Result: ❌ illegal base64 data at input byte 0

Analysis:

  • ✅ Correct keys: ['type', 'data', 'mimeType']
  • ✅ Valid base64 start: 'U' (valid character)
  • ✅ Proper padding: '=' where expected
  • ✅ No wrapping, prefixing, or unexpected keys
  • Client receives "illegal at byte 0" despite clean server output

Reproduction

Minimal Server Code

import base64
from PIL import Image
import io

def img_test_base64_transport(test_variant: int = 0):
    """
    Test image content blocks at different sizes.
    Variant 0: 48 bytes (passes)
    Variant 1: 102 bytes (fails)
    """
    if test_variant == 0:
        # Small image: 4x4 solid color
        img = Image.new('RGB', (4, 4), color=(128, 128, 128))
    else:
        # Larger image: ~102 bytes
        img = Image.new('RGB', (6, 6), color=(128, 128, 128))
    
    buf = io.BytesIO()
    img.save(buf, format='WEBP', quality=100, method=6)
    webp_bytes = buf.getvalue()
    
    # Encode to base64
    b64_data = base64.b64encode(webp_bytes).decode("utf-8")
    
    # Ensure padding (though b64encode already does this)
    missing_padding = len(b64_data) % 4
    if missing_padding:
        b64_data += '=' * (4 - missing_padding)
    
    # Return as MCP image content block
    return {
        "success": True,
        "test_variant": test_variant,
        "actual_b64_length": len(b64_data),
        "bytes_emitted": len(webp_bytes),
        "b64_padding_check": len(b64_data) % 4,
        "mime_type": "image/webp",
        "_mcp_content_blocks": [
            {
                "type": "image",
                "data": b64_data,
                "mimeType": "image/webp"
            }
        ]
    }

MCP Tool Registration

@server.call_tool()
async def test_image_size(test_variant: int = 0):
    """Test image at different sizes to reproduce 100-byte threshold bug."""
    result = img_test_base64_transport(test_variant)
    
    # Extract content blocks
    content_blocks = result.pop("_mcp_content_blocks", [])
    
    # Return content blocks + metadata
    return content_blocks + [{"type": "text", "text": json.dumps(result)}]

Steps to Reproduce

  1. Run MCP server with test tool above
  2. Connect with Claude Code client
  3. Call test_image_size(test_variant=0) → ✅ Image renders
  4. Call test_image_size(test_variant=1) → ❌ "illegal base64 data at input byte 0"

Expected Behavior

Image content blocks should render regardless of size, as long as:

  1. Base64 encoding is valid (length % 4 == 0)
  2. Decoded bytes are valid WebP/image data
  3. Block structure follows MCP spec: {"type": "image", "data": "<base64>", "mimeType": "image/webp"}

Actual Behavior

Images >99 decoded bytes fail with:

Image '<uuid>.webp' could not be processed: illegal base64 data at input byte 0

This error indicates the first character received by the client decoder is not valid base64, despite server logs showing first_char='U' (valid base64).

Root Cause Analysis

The evidence suggests:

  1. Server-side is correct:

    • Wire logs show proper base64 encoding
    • First/last characters are valid
    • Padding is correct (len % 4 == 0)
    • No unexpected keys or wrapping
  2. Client-side has size threshold:

    • Exact boundary at 100 decoded bytes
    • Error message from Go encoding/base64 (not Python)
    • Suggests client code path branches on size:
      if len(imageBytes) > 100 {
          // Wrapper/attachment path (breaks base64)
      } else {
          // Inline path (works)
      }
  3. Hypothesis:

    • Client wraps/prefixes large payloads before decoding
    • Wrapper invalidates base64 (illegal first byte)
    • 100 bytes is a human-round-number threshold (not power-of-2)

Impact

  • Thumbnails fail: 128x128 WebP thumbnails are ~3-5KB (always fail)
  • Image preview broken: Cannot display images in MCP responses
  • Workaround required: External file references instead of inline images

Diagnostic Data Available

We have comprehensive test results including:

  • 10 test fixtures spanning 48-6,812 bytes
  • Server-side wire logs for all sizes
  • Exact boundary detection (99 vs 100 bytes)
  • SDK version comparison (1.26.0 vs 1.28.1)

Happy to provide additional test cases or logs if needed.

Related Issues

Proposed Fix

Since server-side emits correct data, the fix likely needs to be in:

  1. Client SDK (Claude Code's MCP harness) - remove size-based branching for image content blocks
  2. Or MCP spec clarification - if >100-byte images require different encoding, document it

CC: @anthropics/mcp-team

Test server available: We have a fully instrumented test server with wire-level logging if needed for debugging.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions