Truncating a bounded MCP tool result hands the model invalid JSON #114

Closed
opened 2026-08-11 23:03:58 +00:00 by coilyco-ops · 0 comments
Member

mcpToolSession.Call returns json.Marshal(result) of the entire CallToolResult envelope (internal/community/mcp.go:139 at 3812935). boundToolResult then caps that string at maxToolResultBytes and appends a marker (internal/community/proxy.go:453-462).

Cutting a marshalled JSON document at an arbitrary offset produces invalid JSON. The model receives a blob that ends mid-structure followed by [truncated by the runtime], so a large tool result degrades the turn silently rather than failing loudly.

This fires in practice. The bound exists because "four parallel Eco calls inflated a 6k prompt past 47k" (proxy.go:22-24), so results well past 8KB are the reason the cap was added in the first place.

Fix

Shape the result before bounding rather than after:

  • Extract the text content parts into a string, which is what the model can actually use.
  • Carry isError as a distinct signal instead of a field buried inside a blob the model has to parse.
  • Bound inside that text, so truncation lands mid-sentence rather than mid-structure.

Second defect in the same function

boundToolResult compares bytes (len(result)) but slices runes (runes[:maxToolResultBytes]). A result of multibyte content can therefore land at up to roughly four times the intended byte budget, which is the opposite of what the cap is for.

Found while surveying the MCP surface for first-class support. Sibling issues cover degradation, connection lifecycle, roster placement, and capability breadth.

`mcpToolSession.Call` returns `json.Marshal(result)` of the entire `CallToolResult` envelope (`internal/community/mcp.go:139` at `3812935`). `boundToolResult` then caps that string at `maxToolResultBytes` and appends a marker (`internal/community/proxy.go:453-462`). Cutting a marshalled JSON document at an arbitrary offset produces **invalid JSON**. The model receives a blob that ends mid-structure followed by `[truncated by the runtime]`, so a large tool result degrades the turn silently rather than failing loudly. This fires in practice. The bound exists because "four parallel Eco calls inflated a 6k prompt past 47k" (`proxy.go:22-24`), so results well past 8KB are the reason the cap was added in the first place. ## Fix Shape the result before bounding rather than after: * Extract the text content parts into a string, which is what the model can actually use. * Carry `isError` as a distinct signal instead of a field buried inside a blob the model has to parse. * Bound inside that text, so truncation lands mid-sentence rather than mid-structure. ## Second defect in the same function `boundToolResult` compares **bytes** (`len(result)`) but slices **runes** (`runes[:maxToolResultBytes]`). A result of multibyte content can therefore land at up to roughly four times the intended byte budget, which is the opposite of what the cap is for. Found while surveying the MCP surface for first-class support. Sibling issues cover degradation, connection lifecycle, roster placement, and capability breadth.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo#114
No description provided.