Skip to content

MCP tool responses silently truncated to 10KB before large-output-to-file mechanism can save them #1732

@danielp370-msft

Description

@danielp370-msft

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 textResultForLlm

Downstream 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

  1. Connect an MCP server that can return large content (e.g., Agent365 SharePointOneDrive via readSmallBinaryFile, or any MCP tool returning >10KB of text)
  2. Call a tool that returns a file as base64, e.g.: readSmallBinaryFile on a ~330KB PDF
  3. Observe the response is truncated with <output too long - dropped N characters from the middle> inserted
  4. The base64 is corrupted and cannot be decoded
  5. 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:

  • SharePointOneDrivereadSmallBinaryFile and readSmallTextFile return file content inline as base64/text. Even "small" files (the API caps at 5MB) easily exceed 10KB when encoded.
  • MailToolsDownloadAttachment returns email attachment content as base64. A typical PDF receipt or document attachment is 100KB-1MB+.
  • TeamsServerListChatMessages and ListChannelMessages can 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions