Skip to content

get_file_contents double-base64-encodes binary files, corrupting all images #3097

Description

@jukasper

Describe the bug

get_file_contents double-base64-encodes every binary file it returns. The blob field on the wire contains base64(base64(bytes)) instead of base64(bytes).

The practical effect is that no image fetched via get_file_contents is usable. Clients that forward the blob to a model API get a hard rejection, because after the single (correct) decode the payload is still ASCII base64 text rather than image bytes. Against the Copilot API this surfaces as:

CAPIError: 400 The image data you provided does not represent a valid image.
Please check your input and try again with one of the supported image formats
['image/jpeg', 'image/png', 'image/gif', 'image/webp'].

In agentic clients this is worse than a one-off failure: the corrupt payload persists in conversation history, so every subsequent turn re-sends it and fails identically. The session becomes unusable and the only escape is discarding the turn. We have multiple internal users hitting this, one describing it as "you've poisoned the entire session and you can no longer move forward."

Affected version

Reproduced against the hosted server at https://api.githubcopilot.com/mcp/ on 2026-08-18. The defect is present in current main (pkg/github/repositories.go).

Steps to reproduce the behavior

Call get_file_contents on any binary file — this example uses a public 330-byte PNG:

curl -s -X POST https://api.githubcopilot.com/mcp/ \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_file_contents",
       "arguments":{"owner":"anomalyco","repo":"opencode",
       "path":"packages/identity/mark-512x512.png"}}}'

Returned blob (truncated):

aVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQWdBQUFBSUFBZ01BQUFDSkZqeHBB…

Decoding it:

decode once  → b'iVBORw0KGgoAAAAN'        ← ASCII text, not image bytes
decode twice → b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR'   ← the actual PNG

The file is 330 bytes, but the blob is 588 characters — consistent with base64 being applied twice.

Expected vs actual behavior

Expected: a single base64 decode of blob yields the raw file bytes.

Actual: two decodes are required. Any spec-compliant client decoding once receives ASCII base64 text labeled image/png.

Root cause

pkg/github/repositories.go (~line 1105):

// Binary content - encode as base64 blob
blobContent := base64.StdEncoding.EncodeToString(contentBytes)
result := &mcp.ResourceContents{
    URI:      resourceURI,
    Blob:     []byte(blobContent),   // ← already base64, then encoded again
    MIMEType: contentType,
}

mcp.ResourceContents.Blob is typed []byte:

// modelcontextprotocol/go-sdk v1.7.0 — mcp/content.go:302
type ResourceContents struct {
	URI      string `json:"uri"`
	MIMEType string `json:"mimeType,omitempty"`
	Text     string `json:"text,omitempty"`
	Blob     []byte `json:"blob,omitzero"`
	Meta     Meta   `json:"_meta,omitempty"`
}

Go's encoding/json marshals a []byte field as base64 automatically, and there is no custom MarshalJSON on ResourceContents. So the manual EncodeToString is applied first, and the marshaler encodes that ASCII string a second time.

This looks like a migration artifact: under mark3labs/mcp-go, Blob was a string, so manually encoding was correct. Blob has been []byte since modelcontextprotocol/go-sdk v1.0.0, which turned the same line into double encoding.

Suggested fix

Hand the marshaler the raw bytes and let it do the single encode:

result := &mcp.ResourceContents{
    URI:      resourceURI,
    Blob:     contentBytes,
    MIMEType: contentType,
}

Worth adding a regression test that round-trips a known PNG through the tool and asserts that a single decode of blob starts with the PNG magic bytes 89 50 4E 47. A test asserting only "blob is non-empty" or comparing against a re-encoded string will pass while double-encoded.

Note on a red herring

We originally chased this through a file that happens to be a git symlink (sdks/vscode/images/icon.png../../../packages/identity/mark-512x512.png). The symlink is not the cause. The Contents API resolves symlinks transparently, and http.DetectContentType correctly identifies the resolved PNG. Both the symlink path and the real file return byte-identical double-encoded blobs. Any binary file reproduces this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions