Repository navigation
Image content blocks >99 bytes fail with "illegal base64 at byte 0" #3123
Description
Activity
Minimal Reproduction Server:
#!/usr/bin/env python3 """ Minimal MCP Server to Reproduce 100-Byte Image Content Block Bug This server demonstrates the issue where image content blocks >99 bytes fail client-side decode with "illegal base64 data at input byte 0". Usage: python minimal-repro-server.py Then connect with Claude Code and run: test_image_size(test_variant=0) # 48 bytes - ✅ Works test_image_size(test_variant=1) # 102 bytes - ❌ Fails """ import sys import json import base64 from PIL import Image import io from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent, ImageContent # Create server server = Server("image-size-test") def create_webp_image(size_variant: int) -> bytes: """ Create a WebP image of specific size. Variant 0: 4x4 = ~48 bytes (passes 100-byte threshold) Variant 1: 6x6 = ~102 bytes (fails 100-byte threshold) """ sizes = { 0: (4, 4), # ~48 bytes WebP 1: (6, 6), # ~102 bytes WebP } img_size = sizes.get(size_variant, (4, 4)) img = Image.new('RGB', img_size, color=(128, 128, 128)) buf = io.BytesIO() img.save(buf, format='WEBP', quality=100, method=6) return buf.getvalue() @server.list_tools() async def list_tools() -> list[Tool]: """List available tools.""" return [ Tool( name="test_image_size", description="Test image content blocks at different sizes to reproduce 100-byte threshold bug", inputSchema={ "type": "object", "properties": { "test_variant": { "type": "integer", "description": "0 = 48 bytes (passes), 1 = 102 bytes (fails)", "default": 0, "minimum": 0, "maximum": 1 } } } ) ] @server.call_tool() async def call_tool(name: str, arguments: dict): """Handle tool calls.""" if name != "test_image_size": raise ValueError(f"Unknown tool: {name}") test_variant = arguments.get("test_variant", 0) # Create WebP image webp_bytes = create_webp_image(test_variant) # Encode to base64 b64_data = base64.b64encode(webp_bytes).decode("utf-8") # Ensure proper padding (though b64encode already handles this) missing_padding = len(b64_data) % 4 if missing_padding: b64_data += '=' * (4 - missing_padding) # Log wire-level details to stderr (won't interfere with stdio protocol) sys.stderr.write( f"[WIRE] test_variant={test_variant}, " f"bytes={len(webp_bytes)}, " f"b64_len={len(b64_data)}, " f"b64_len%4={len(b64_data) % 4}, " f"first_char='{b64_data[0]}', " f"last_char='{b64_data[-1]}'\n" ) sys.stderr.flush() # Return image content block + metadata return [ ImageContent( type="image", data=b64_data, mimeType="image/webp" ), TextContent( type="text", text=json.dumps({ "success": True, "test_variant": test_variant, "bytes_emitted": len(webp_bytes), "b64_length": len(b64_data), "b64_padding_check": len(b64_data) % 4, "first_char": b64_data[0], "last_char": b64_data[-1], "expected_result": "✅ Renders" if test_variant == 0 else "❌ Fails with 'illegal base64 at byte 0'" }) ) ] async def main(): """Run the server.""" sys.stderr.write("MCP Image Size Test Server Starting\n") sys.stderr.write("This server reproduces the 100-byte threshold bug:\n") sys.stderr.write(" test_variant=0: 48 bytes (should render)\n") sys.stderr.write(" test_variant=1: 102 bytes (fails with 'illegal base64 at byte 0')\n\n") sys.stderr.flush() async with stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, server.create_initialization_options() ) if __name__ == "__main__": import asyncio asyncio.run(main())
Requirements:
# Minimal Requirements for MCP Image Bug Reproduction mcp>=1.28.1 Pillow>=10.0.0Usage:
pip install -r requirements-repro.txt python minimal-repro-server.py
Then connect with Claude Code and run:
test_image_size(test_variant=0)→ ✅ 48 bytes, renderstest_image_size(test_variant=1)→ ❌ 102 bytes, fails with 'illegal base64 at byte 0'
Temporary Workaround Implemented
While awaiting fix, we've deployed a hybrid thumbnail delivery system:
Implementation:
- Images ≤99 bytes: inline base64 (works, optimal)
- Images ≥100 bytes: return file path instead (sidesteps bug)
Code:
if len(thumb_bytes) <= 99: # Inline mode (works fine) return { 'delivery_mode': 'inline', '_mcp_content_blocks': [{ 'type': 'image', 'data': base64_string, 'mimeType': 'image/webp' }] } else: # File path mode (workaround) return { 'delivery_mode': 'file_path', 'thumbnail_path': '/path/to/thumbnail.webp', 'workaround_note': 'MCP SDK issue #3123' }
Results:
- ✅ Small thumbnails render inline (fast)
- ✅ Large thumbnails accessible via file path
- ✅ No size limits
- ✅ Automatic failover
This workaround will be removed once SDK fix is deployed.
✅ Root Cause Identified: Entropy-Based Redaction Pre-Decode
After 7 test rounds over 6 days with discriminating experiments, root cause confirmed:
Claude.ai's tool-result pipeline applies high-entropy string redaction to base64 image payloads inside ImageContent and EmbeddedResource blocks BEFORE image decoding, replacing valid data and causing "illegal base64 data at input byte 0" errors.
Discriminating Test Results (2026-07-17)
Built entropy-discriminating test with Shannon entropy calculation:
Pattern b64 Length Entropy (bits/char) Distinct Chars Result Solid PNG cl=0 1,136 chars 0.714 37/65 ✅ RENDERS Solid PNG cl=0 4,232 chars 0.272 39/65 ✅ RENDERS Gradient WebP q95 112 chars 4.891 44/65 ❌ Byte 0 error Gradient WebP q95 196 chars 5.434 58/65 ❌ Byte 0 error Key finding: 4,232-char low-entropy (0.27 bits/char) payload renders successfully. 112-char high-entropy (4.89 bits/char) payload fails at byte 0.
Size is irrelevant. Entropy is everything.
Entropy Threshold
- Passes: ≤0.714 bits/char (solid gray PNG, compress_level=0)
- Fails: ≥4.891 bits/char (compressed gradient WebP)
- Untested: 0.72–4.89 bits/char middle range
For reference: typical compressed images (JPEG/WebP/PNG compress_level=6-9) have ~5.5-6.0 bits/char Shannon entropy.
Error Source Attribution
Error phrasing "illegal base64 data at input byte N" is Go's encoding/base64 exact format (not Python binascii, not TypeScript, not MCP SDK error messages).
This points to Anthropic's Claude.ai server-side ingestion layer (Go-based), not the MCP Python SDK.
The bug manifests when:
- MCP server emits valid base64 ImageContent/EmbeddedResource block (verified via server-side diagnostics)
- Claude.ai ingestion pipeline detects high-entropy string in tool result
- Redaction filter replaces base64 blob with
"[REDACTED: high_entropy_string]" - Image decoder receives
"["as first character - Decoder fails at byte 0: "illegal base64 data at input byte 0"
This explains all observations across 7 test rounds and matches the known behavior of Claude.ai redacting high-entropy strings in data URIs.
Server-Side Verification
Wire-level logging confirmed server emits perfect, valid base64:
[DELIVERY-TEST] b64_data type: <class 'str'> [DELIVERY-TEST] b64_data[:24]: 'UklGRmwAAABXRUJQVlA4IGAA' [DELIVERY-TEST] first_char: 'U' [DELIVERY-TEST] ✓ base64 valid, decodes to 116 bytes [DELIVERY-TEST] Shannon entropy: 5.434 bits/charYet client receives:
"illegal base64 data at input byte 0"Bug is definitively downstream of MCP SDK stdout — in Claude.ai's ingestion/redaction pipeline.
Impact
- All compressed images fail (WebP, JPEG, PNG with standard compression)
- Only artificially low-entropy images work (solid colors, compress_level=0)
- Real-world thumbnails/charts/photos inherently have high entropy when compressed
- Defeats the purpose of MCP's image content block feature
Workaround (Being Validated)
Potential: Uncompressed PNG (compress_level=0) of real content if pixel redundancy keeps Shannon entropy <0.7 bits/char.
Testing realistic chart/thumbnail content to validate if workaround is viable for production use.
Recommendation
This is a Claude.ai client-side bug, not an MCP SDK bug. Should be escalated to Anthropic's Claude.ai tool ingestion team:
Redaction filter should NOT run on image data fields inside MCP content blocks. These are already typed as
type: "image"withmimeType— there's no ambiguity requiring entropy-based heuristics.
Test Code
Entropy-discriminating test available at: https://github.com/PeeperFrog/noisk-ai-mcp/blob/18919c7b/src/NOISK_AI_server.py#L14006-14133
Shannon entropy calculation:
def shannon_entropy(s: str) -> float: from collections import Counter import math if not s: return 0.0 counts = Counter(s) length = len(s) return -sum((count/length) * math.log2(count/length) for count in counts.values())
Solid pattern (low entropy):
img = Image.new('RGB', (16, 16), color=(128, 128, 128)) img.save(buf, format='PNG', compress_level=0) # Stored/raw deflate
Status: Root cause identified with reproducible test. First successful image deliveries achieved. Awaiting Anthropic fix or workaround validation for real content.
✅ Threshold Measured: 4.52 ± 0.04 bits/char Global Shannon Entropy
After systematic bisection testing, the exact redaction threshold has been determined:
Threshold: 4.52 ± 0.04 bits/char global Shannon entropy over the full payload (consistent with 4.5 exactly)
Filter Mechanism Confirmed
The redaction filter computes global Shannon entropy over the entire base64 payload.
Alternative hypotheses eliminated:
-
Window-based filtering: ❌ Eliminated
- Test payload with sliding-window max 4.65 bits/char but global 0.64 → ✅ Passed
- Payload with sliding max 2.25 but global 5.007 → ❌ Failed
-
Run-based filtering: ❌ Eliminated
- Payload with longest run 32 characters but global entropy 5.007 → ❌ Failed
- Long repeated runs don't defeat filter if global entropy exceeds threshold
-
Distinct-character count: ❌ Eliminated
- Payloads with identical distinct-character counts split pass/fail based on global entropy alone
- 36 distinct @ 5.007 entropy → ❌ Failed
- 49 distinct @ 0.147 entropy → ✅ Passed
Conclusion: Global Shannon entropy is the single determining factor.
Production Workaround: Palette-Mode PNG
Since all standard compression fails (PNG deflate at any level produces 5.24-5.75 bits/char), the optimization lever is representation, not compression.
Solution: Indexed-palette PNG, compress_level=0
Benefits:
- Indexed mode: 1 byte/pixel vs 3-4 bytes/pixel for RGB
- Payload size: 5.6× smaller than RGB cl=0
- Shannon entropy: ~0.6-0.8 bits/char (~7× safety margin below threshold)
- 256×256 thumbnail: ~47 KB base64 (acceptable for MCP protocol)
Supported content:
- ✅ Charts, infographics, diagrams
- ✅ UI mockups, wireframes
- ✅ QR codes, logos, icons
- ❌ Photos/natural images: 5.77 bits/char even at palette cl=0 (blocked on Anthropic fix)
Test Results Summary
Encoding Entropy (bits/char) Result RGB cl=0 0.147 (chart) ✅ Pass Palette cl=0 ~0.6-0.8 (chart) ✅ Pass PNG cl=1 5.750 ❌ Fail PNG cl=9 5.236 ❌ Fail WebP lossless 5.393 ❌ Fail Palette cl=0 (photo) 5.77 ❌ Fail Threshold location: Between 4.48 and 4.56 bits/char
Best estimate: 4.52 ± 0.04 (consistent with 4.5 exactly)
Scope Limitation
Natural images (photos, AI-generated art) remain blocked even with optimal representation. The inherent pixel variation in natural imagery produces entropy above threshold regardless of encoding.
Recommendation: Anthropic should exempt
type: "image"content blocks withmimeTypefrom high-entropy redaction. These are already typed as image data, not potential secrets requiring heuristic detection.
Implementation Status
- ✅ Threshold measured via systematic bisection
- ✅ Filter mechanism confirmed (global Shannon entropy)
- ✅ Production workaround validated (palette PNG, 5.6× size reduction)
- ✅ Entropy monitoring added to production path (drift detection)
- ⏳ Awaiting Anthropic fix for unrestricted image delivery
Repository: https://github.com/PeeperFrog/noisk-ai-mcp (Test 7 series, commits 3cbf27ef-416dfed9)
-
Corroborating from the SDK side — I audited the python-sdk source for anything that could alter content-block data:
- There is no entropy-based redaction or sanitization anywhere in this SDK. The only base64-wrapping code (the
=?base64?...?=sentinel insrc/mcp/shared/inbound.py) applies to HTTP header round-trips, not content blocks. - The
Imagehelper encodes with newline-freebase64.b64encode— so the SDK emitting clean base64 matches your wire logs exactly.
One additional fingerprint supporting your diagnosis: your measured threshold of 4.52 ± 0.04 bits/char is a near-exact match for the default base64 entropy limit in
detect-secrets(4.5) — the most widely deployed secrets-scanning heuristic. That strongly suggests a stock secrets-redaction pass running over tool results downstream of this SDK, consistent with everything you measured.+1 to your recommendation: content blocks already typed
"type": "image"with amimeTypeshouldn't be subject to entropy-based secret heuristics at all. This likely belongs on Anthropic's Claude Code / claude.ai feedback tracker rather than this repo — but leaving the corroboration here so the next reader doesn't re-derive it.- There is no entropy-based redaction or sanitization anywhere in this SDK. The only base64-wrapping code (the
There's no size- or entropy-based redaction in this SDK —
ImageContent.datais a plainstrwritten to the transport untouched — so the alteration is downstream in the client's tool-result ingestion; for Claude Code that's https://github.com/anthropics/claude-code. Closing this as part of a wider backlog cleanup following the v2 launch. Feel free to reopen if this is still relevant.
Image Content Blocks >99 Bytes Fail Client-Side Decode in stdio Mode
Summary
Image content blocks returned via
_mcp_content_blocksfail 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:
mcp==1.28.1Client:
encoding/base64(based on error message format)Dependencies:
Issue Description
Observed Behavior
Images returned in MCP content blocks exhibit a binary size threshold at exactly 100 bytes (decoded image data):
Test Results
Pass Class (renders successfully):
Fail Class (decode error):
Additional Finding (SDK Version Dependent)
With SDK 1.26.0, payloads >4KB exhibited a second error class:
binasciierror)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:
Analysis:
['type', 'data', 'mimeType']'U'(valid character)'='where expectedReproduction
Minimal Server Code
MCP Tool Registration
Steps to Reproduce
test_image_size(test_variant=0)→ ✅ Image renderstest_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:
{"type": "image", "data": "<base64>", "mimeType": "image/webp"}Actual Behavior
Images >99 decoded bytes fail with:
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:
Server-side is correct:
Client-side has size threshold:
encoding/base64(not Python)Hypothesis:
Impact
Diagnostic Data Available
We have comprehensive test results including:
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:
CC: @anthropics/mcp-team
Test server available: We have a fully instrumented test server with wire-level logging if needed for debugging.