Summary
get_file_contents irreversibly corrupts every binary file in a repository (PDF, PNG/JPG, ZIP, XLSX/DOCX, fonts, compiled artifacts). The GitLab API returns file content as Base64, but getFileContents() force-decodes it into a UTF-8 string. Any byte sequence that is not valid UTF-8 is replaced with U+FFFD (EF BF BD), so the original bytes are permanently lost before the content ever reaches the MCP client.
Reported originally as "fetching a PDF loses data" — the file is not merely unusable, the byte stream is destroyed and cannot be reconstructed client-side.
Affected versions
- Confirmed on
@zereight/mcp-gitlab@2.1.30
- Still present on
main (926d42c) / 2.1.46 — the code is unchanged
Root cause
index.ts#L2054-L2058
// Decode Base64-encoded file content to UTF-8
if (!Array.isArray(parsedData) && parsedData.content) {
parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8");
parsedData.encoding = "utf8";
}
This conversion is unconditional — there is no binary detection and no way to opt out.
Note that GITLAB_REPO_FILE_ENCODING / --repo-file-encoding does not help here. It is only referenced in the write paths (encodeRepoFilePayloadContent() at L4783, plus L4821 and L4907); getFileContents() never consults it. So a user hitting this has no configuration-level workaround.
Reproduction
No GitLab instance required — this reproduces the exact transformation the server applies:
const header = Buffer.from('%PDF-1.7\n', 'latin1');
const binaryStream = Buffer.from([
0x25,0xC7,0xEC,0x8F,0xA2,0x0A,
0x78,0x9C,0xED,0x9A,0x4B,0x6F,0xDB,0x30,
0x0C,0xC7,0xEF,0xFE,0x8A,0x82,0x9E,0x0A,
0xFF,0xD8,0xFF,0xE0,0x00,0x10,0x4A,0x46
]);
const footer = Buffer.from('\n%%EOF\n', 'latin1');
const pdf = Buffer.concat([header, binaryStream, footer]);
const apiContent = pdf.toString('base64'); // what GitLab returns
// index.ts:2057
const afterMcp = Buffer.from(apiContent, 'base64').toString('utf8');
const restored = Buffer.from(afterMcp, 'utf8'); // best a client can do
console.log('original bytes :', pdf.length); // 46
console.log('restored bytes :', restored.length); // 73
console.log('identical :', pdf.equals(restored)); // false
console.log('U+FFFD count :', (afterMcp.match(/\uFFFD/g)||[]).length); // 14
Output:
original bytes : 46
restored bytes : 73
identical : false
U+FFFD count : 14
Byte-level diff:
original: 255044462d312e370a 25 c7 ec8fa2 0a 78 9c ed 9a 4b6f db 300c c7 ef fe 8a 82 9e 0a ffd8 ffe0 00104a460a2525454f460a
restored: 255044462d312e370a 25 efbfbd ec8fa2 0a 78 efbfbd efbfbd 4b6f efbfbd 300c efbfbd efbfbd efbfbd efbfbd efbfbd efbfbd 0a efbfbd efbfbd efbfbd efbfbd 00104a460a2525454f460a
Corruption is partial and unpredictable: ec8fa2 survives because it happens to form a valid UTF-8 sequence, while c7, 9c, ed, ff each collapse into efbfbd. This is why the symptom presents as "the PDF lost some data" rather than a clean failure.
Expected behaviour
Binary content should reach the client losslessly. A backward-compatible fix is to only decode when the round-trip is lossless, and otherwise pass the Base64 through with encoding left as base64 so the client can decode it itself:
if (!Array.isArray(parsedData) && parsedData.content) {
const buf = Buffer.from(parsedData.content, "base64");
const asText = buf.toString("utf8");
if (Buffer.from(asText, "utf8").equals(buf)) {
parsedData.content = asText; // text file — unchanged behaviour
parsedData.encoding = "utf8";
} else {
parsedData.encoding = "base64"; // binary — preserve bytes
}
}
This keeps the current behaviour for all valid-UTF-8 files, so existing text workflows are unaffected.
Related: the write path has the same weakness
encodeRepoFilePayloadContent() does Buffer.from(content) with no explicit encoding, which defaults to UTF-8. Uploading binary content through create_or_update_file therefore corrupts it in the same way. Worth addressing together, since a round-trip (read then write) currently destroys data twice.
Summary
get_file_contentsirreversibly corrupts every binary file in a repository (PDF, PNG/JPG, ZIP, XLSX/DOCX, fonts, compiled artifacts). The GitLab API returns file content as Base64, butgetFileContents()force-decodes it into a UTF-8 string. Any byte sequence that is not valid UTF-8 is replaced withU+FFFD(EF BF BD), so the original bytes are permanently lost before the content ever reaches the MCP client.Reported originally as "fetching a PDF loses data" — the file is not merely unusable, the byte stream is destroyed and cannot be reconstructed client-side.
Affected versions
@zereight/mcp-gitlab@2.1.30main(926d42c) /2.1.46— the code is unchangedRoot cause
index.ts#L2054-L2058This conversion is unconditional — there is no binary detection and no way to opt out.
Note that
GITLAB_REPO_FILE_ENCODING/--repo-file-encodingdoes not help here. It is only referenced in the write paths (encodeRepoFilePayloadContent()at L4783, plus L4821 and L4907);getFileContents()never consults it. So a user hitting this has no configuration-level workaround.Reproduction
No GitLab instance required — this reproduces the exact transformation the server applies:
Output:
Byte-level diff:
Corruption is partial and unpredictable:
ec8fa2survives because it happens to form a valid UTF-8 sequence, whilec7,9c,ed,ffeach collapse intoefbfbd. This is why the symptom presents as "the PDF lost some data" rather than a clean failure.Expected behaviour
Binary content should reach the client losslessly. A backward-compatible fix is to only decode when the round-trip is lossless, and otherwise pass the Base64 through with
encodingleft asbase64so the client can decode it itself:This keeps the current behaviour for all valid-UTF-8 files, so existing text workflows are unaffected.
Related: the write path has the same weakness
encodeRepoFilePayloadContent()doesBuffer.from(content)with no explicit encoding, which defaults to UTF-8. Uploading binary content throughcreate_or_update_filetherefore corrupts it in the same way. Worth addressing together, since a round-trip (read then write) currently destroys data twice.