-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Describe the bug
When an MCP tool returns a large text response (e.g., a base64-encoded file from readSmallBinaryFile), the content is silently truncated to ~10KB before the existing "save large output to file" mechanism (yme()) gets a chance to intercept it.
The copilot-cli already has a great mechanism for handling large tool outputs: when textResultForLlm exceeds 30KB, yme() saves the full content to a temp file and returns a small pointer message like:
Output too large to read at once (428.7KB). Saved to: /tmp/...-copilot-tool-output-....txt
However, in invokeToolResponseToToolResult(), the MCP response text is passed through Iw(a, "output") which truncates it to dss = 10 * 1024 (10KB) before the result is returned. By the time yme() checks the output size downstream in callTool(), the content is already truncated to 10KB — well under the 30KB threshold — so yme() never triggers.
This means any MCP tool response larger than ~10KB is silently corrupted. The LLM receives truncated base64, truncated JSON, etc., with no indication that data was lost.
Root Cause
In invokeToolResponseToToolResult():
// 'a' contains the full concatenated text from MCP response
let I = a ? Iw(a, "output") : ""; // Iw() truncates to 10KB (dss)!
return e.isToolError
? { textResultForLlm: I, resultType: "failure", ... }
: { textResultForLlm: I, resultType: "success", sessionLog: I, ... }
// ^ truncated content returned as textResultForLlmDownstream in callTool():
// yme() would save to file if textResultForLlm > 30KB
// But it's already truncated to 10KB, so this never fires
a.resultType === "success" && r.largeOutputOptions && !a.skipLargeOutputProcessing
&& (a = await yme(a, r.largeOutputOptions))One-Line Fix
In invokeToolResponseToToolResult(), use the full content a for textResultForLlm in the success path, and keep Iw() only for sessionLog:
- : { textResultForLlm: I, binaryResultsForLlm: s, resultType: "success", sessionLog: I, ... }
+ : { textResultForLlm: a || "", binaryResultsForLlm: s, resultType: "success", sessionLog: I, ... }This lets yme() detect the large output and save it to a temp file as designed. The LLM then receives the pointer message and can use cat, head, base64 -d, etc. to process the file.
Error results (resultType: "failure") can remain truncated since they're text-based error messages.
Affected version
GitHub Copilot CLI 0.0.420
Steps to reproduce the behavior
- Connect an MCP server that can return large content (e.g., Agent365 SharePointOneDrive via
readSmallBinaryFile, or any MCP tool returning >10KB of text) - Call a tool that returns a file as base64, e.g.:
readSmallBinaryFileon a ~330KB PDF - Observe the response is truncated with
<output too long - dropped N characters from the middle>inserted - The base64 is corrupted and cannot be decoded
- The
yme()"save to file" mechanism never triggers because the content was already truncated to 10KB
Expected behavior
Large MCP tool responses should be saved to a temp file (via the existing yme() mechanism) and the LLM should receive a pointer message like:
Output too large to read at once (428.7 KB). Saved to: /tmp/1772250528560-copilot-tool-output-vdrz7a.txt
This already works correctly for built-in tool results (e.g., bash output). It should work the same way for MCP tool results.
After applying the one-line fix described above, this is exactly what happens — confirmed working with a 329KB PDF download via MCP.
Additional context
- OS: Linux (WSL2 Ubuntu)
- Architecture: x86_64
- Shell: bash
- Node.js: v24.13.0
Patch Script
A sed-based patch script that applies the fix to the installed @github/copilot npm package:
#!/bin/bash
INDEX_JS="$(npm root -g)/@github/copilot/index.js"
cp "$INDEX_JS" "$INDEX_JS.bak"
sed -i 's/textResultForLlm:I,binaryResultsForLlm:s,resultType:"success",sessionLog:I/textResultForLlm:a||"",binaryResultsForLlm:s,resultType:"success",sessionLog:I/' "$INDEX_JS"Impact
This affects all MCP tools that return content >10KB, including but not limited to:
- File downloads (SharePointOneDrive
readSmallBinaryFile,readSmallTextFile) - Email attachment downloads (MailTools
DownloadAttachment) - Any MCP tool returning large JSON, logs, or data
The fix is backward-compatible — small responses (<10KB) are unaffected, and the existing yme() mechanism handles everything correctly once it actually receives the full content.
Real-World Context: Microsoft 365 MCP Servers
This is particularly impactful for enterprise MCP integrations. Microsoft's Agent 365 platform exposes governed MCP servers for Microsoft 365 services (SharePoint/OneDrive, Outlook Mail, Teams, etc.) that are designed to work with copilot-cli. Several of these servers provide tools that routinely return content exceeding 10KB:
- SharePointOneDrive —
readSmallBinaryFileandreadSmallTextFilereturn file content inline as base64/text. Even "small" files (the API caps at 5MB) easily exceed 10KB when encoded. - MailTools —
DownloadAttachmentreturns email attachment content as base64. A typical PDF receipt or document attachment is 100KB-1MB+. - TeamsServer —
ListChatMessagesandListChannelMessagescan return large message histories.
These are legitimate, designed-for-purpose tool calls — the MCP servers intentionally return the content inline because there's no alternative "save to disk" parameter in the MCP protocol. The copilot-cli's existing yme() file-save mechanism is the correct solution; it just needs to receive the full content to work.
Without this fix, copilot-cli users cannot download files, attachments, or large data through any MCP server — a significant limitation for enterprise workflows.