Saphan StudioDocs
MCP server

The response budget

Five stacked mechanisms so a fleet read fits a client's tool-output limit - and a cap that errors rather than truncates.

This section exists because of a measured failure: fleet_show with no arguments, on a real 68-stream record, exceeded the MCP client's tool-output limit — the response was dropped to a file instead of reaching the model. The projection was correct; the budget did not exist. MCP offers no mechanism for a client to declare its budget to a server, so the server must choose defaults and choose well. Five mechanisms now stack, each with production precedent behind it:

Default live horizon. fleet_show and wave_show default to horizon: "live" — terminal streams (merged, closed-without-merge) are excluded. On the measured record that was 83% of the payload: dead streams carrying expired liveness blocks and week-old gate prose. The view is named fleet_show; the fleet is what flies. History is one explicit argument away (horizon: "all" — byte-identical to a pre-narrowing read), and a defaulted response names the default that fired rather than narrowing silently:

This query used the default live-only horizon because "horizon" was not provided
(merged/closed-without-merge streams are excluded). If results seem incomplete,
pass horizon:"all" to include them.

Honest summary. detail: "summary" (the default) carries the stream-identity and state fields, a four-field last_gate (no justification prose — that lives in stream_detail and full), and a trimmed liveness block. detail: "full" remains complete; the budget moves data behind an explicit request, it never removes data. The descriptors enumerate both levels exhaustively.

Cursor pagination as tool arguments. limit defaults to 25, maximum 100 — a value above the maximum is rejected with limit must be an integer between 1 and 100, never silently clamped. A truncated response is a successful bounded response (isError: false): it carries next_after, which names the exact next call (after: <cursor>), and total_count still answers how many streams exist unfiltered — a truncated list can never be mistaken for the complete set. (MCP protocol pagination deliberately does not cover tools/call; these are ordinary arguments read by the model from the descriptor, which is why the descriptor discipline above is load-bearing.)

Tabular formats. format: "csv" / "tsv" return rows only, a fixed narrow column set — measured at roughly 29% fewer tokens than JSON for this row shape. This changes the read contract (a table, not the envelope) and is therefore opt-in.

A hard byte cap as an error, never a silence. After all narrowing has run, a response at or above 200,000 bytes is refused, class result-too-large, with the exact next move:

result-too-large: fleet_show response is <n> bytes, exceeds the 200000-byte cap —
narrow with a state/streams filter, lower limit below its current value, or omit
detail:full (summary, the default, is far smaller)

The reasoning is recorded with its precedents: clients demonstrably truncate oversized tool results silently, at boundaries the server did not choose, and the model then answers with full confidence from a fragment. If the server does not cut explicitly, something below will cut implicitly. Additionally, fleet_show / wave_show declare _meta["anthropic/maxResultSizeChars"] = 200000 — a transitional client-side allowance above the cap, documented in the code as "never a substitute for the narrowing".

The regression test for this class is deliberate: an artificial record of ≥200 streams must fit the default query's budget — the test closes the class, not the day's instance.