Skip to content

MCP tool results are pretty-printed, and every consumer of them is a model #2350

Description

@erikdarlingdata

Every MCP tool result both SKUs return is serialized with WriteIndented = true, and the only consumer of an MCP tool result is a language model. We are paying to indent JSON for a reader that does not care.

It is one line in one shared file: PerformanceMonitor.Common/Mcp/McpHelpers.cs:32

public static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };

Used by 30 of the 74 Darling MCP files and 24 of the 31 Lite ones. It lives in Common, so one change moves both SKUs together — no parity gap to manage. Two readers (DarlingAgReader, DarlingFleetReader) declare their own options with the same flag and would need the same treatment.

Size

Measured on a 15-field record-array result shaped like get_blocking:

rows indented compact indentation's share
10 2,977 2,297 22.8%
30 8,757 6,757 22.8%
100 29,082 22,462 22.8%

These are bytes, not tokens, and the token saving is smaller than the byte saving — BPE tokenizers pack runs of spaces efficiently, so indentation is cheaper in tokens than it looks in bytes. Anyone quoting this should measure tokens before claiming a number. The direction is not in doubt, though, and the change costs nothing.

Why bother if it is only whitespace

Because it compounds with the thing we actually care about. Tool results are the bulk of what fills an agent's context when it works a real incident, and the fleet-wide reads are the widest results we return. A result that no longer fits is the difference between an agent reasoning over 100 blocking events and over 40.

Provenance

This came out of reviewing #2286, which proposed an opt-in third-party wire format and benchmarked it against our indented output. Separating the two matters: some part of the win reported there is a serializer flag rather than the format, and the honest baseline for evaluating any compact format is compact JSON. Credit to @blackwell-systems for the measurement work that surfaced it — this half of it carries no new dependency and no new failure mode, so it stands on its own regardless of what happens to that PR.

Not before 3.5.0

Deliberately not folding this into the release currently staged. It changes the bytes every MCP consumer sees, and the release is already carrying the Aurora fix, the watermark bound, the install-location diagnostic and the peer disclosure. This goes on dev after the tag.

Check before changing

Confirm nothing parses our own tool output positionally or by line. A grep for tests asserting on serialized MCP output with embedded newlines should come first — CrossAppMcpToolInventoryPinTests parses the instructions census, not tool results, but it is worth proving rather than assuming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions