Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/guides/agent-first-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ so a prefix cannot masquerade as a complete result. The log tools
but reaching it is a cutoff: the result is `ok: true` with `outputTruncated: true`,
`data.cutoff` set to the `bytes` bound, and the events parsed before the cut. See
[MCP logs contract](../reference/mcp-logs.md). Run the equivalent CLI command when
full output is needed. The budget bounds captured bytes, not total process memory
or JSON response size.
full output is needed. The budget bounds captured bytes, not total process memory.
A separate limit keeps each serialized result under 9.5 MiB, below the 10 MiB at
which SDK clients close the connection. See
[Result size limit](../reference/mcp-tools.md#result-size-limit).

The [MCP tool contract](../reference/mcp-tools.md) lists every tool with its
annotations, inputs, the CLI command it runs and its result shape.
Expand Down
22 changes: 22 additions & 0 deletions docs/reference/mcp-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ non-zero. Use `ok` and `data.cutoff` to read the result, not `exitCode`.
`data.stop_reason` (tail only) keeps its earlier values: `eof`, `max_events`,
`timeout`, `output_limit`, `cancelled` or `interrupted`.

The whole result is also limited to 9.5 MiB, because SDK clients drop the
connection above 10 MiB. See [Result size limit](mcp-tools.md#result-size-limit).
Over that limit, `content` becomes a smaller view (below), and `stdout` in
`structuredContent` is cut before any event. Events go next, from the end, always
as whole events. `outputTruncated` is `true` whenever `structuredContent` was
cut. When events were removed from a successful collection:

- `data.cutoff` is `{ "bound": "bytes", "limit": 9961472 }`, replacing a `null`
or an earlier cutoff. A collection that completed is reported as cut, because
the result no longer holds all of it.
- `data.count` is the number of events kept.
- `data.stop_reason` still says why collection stopped, so it can be `eof` beside
a byte cutoff.

A backend failure stays a failure: `ok` is `false`, `error` is unchanged and
`data.cutoff` stays `null`, even if events were removed.

The `content` text is JSON with the same shape as `structuredContent`, in a
256 KiB view. It keeps fewer events, and when it removes events its own
`data.cutoff.limit` is `262144`. A client should read `structuredContent` when it
can.

A backend failure is not a truncated success. It is reported when the stream
contains an `error` event or an `end` event with a reason other than `eof` or
`closed`, or when the child exits non-zero at the end of the stream. The error
Expand Down
25 changes: 24 additions & 1 deletion docs/reference/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Every result has the same structured content:
| `exitCode` | The child's exit code (1 for a refusal) |
| `stdout`, `stderr` | Captured child output (withheld for the env tools; `stdout` withheld for instances) |
| `data` | Parsed or projected result, when the tool defines one |
| `outputTruncated` | `true` when output reached the 8 MiB capture limit |
| `outputTruncated` | `true` when output reached the 8 MiB capture limit, or this payload was cut to the result size limit |
| `spawned` | `false` only on a refusal: no child, no audit line |
| `error` | Closed refusal or backend error: `code`, `reason`, `message` |

Expand All @@ -103,6 +103,29 @@ When `ok` is `false`, the result also has `isError: true`. Output above the
never presented as a complete result. The log tools are the exception, as their
contract describes.

### Result size limit

Every tool's serialized result is limited to 9.5 MiB of UTF-8, measured after
JSON escaping. MCP SDK clients on stdio close the connection when a message passes
10 MiB. The rest of that margin covers the JSON-RPC envelope. The 8 MiB capture
limit cannot do this job, because it counts raw output, and a result repeats that
output and escapes it.

A result within the limit is unchanged. Above it, the server removes data in this
order:

1. `content` text. It becomes JSON of at most 256 KiB with the same shape as
`structuredContent`, carrying less output and its own `outputTruncated: true`.
If `structuredContent` now fits, it is complete and has no such flag.
2. For the log tools, `stdout`, then whole events from the end. See the
[logs contract](mcp-logs.md). For other tools, `data`, then the end of `stdout`.
3. The end of `stderr`, past its first 16 KiB, which step 2 cannot take.

A payload that was cut has `outputTruncated: true`, and its `stderr` ends with a
fixed line saying to use the CLI for full output. A cut never splits a code point.
`ok` and `exitCode` still describe the command, so a command that succeeded stays
`ok` with `data` omitted. Run the equivalent CLI command when full output is needed.

### Refusals

A refusal happens before any child process exists. Its `error.reason` is one of
Expand Down
8 changes: 6 additions & 2 deletions src/mcp/logs-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* A collection ends in exactly one outcome:
* - `complete`: the backend reached end of stream and exited 0.
* - `cutoff`: the MCP server stopped collection at one of its own bounds. This
* is a success; `data.cutoff` names the bound and its effective limit.
* is a success; `data.cutoff` names the bound and its effective limit. The
* result can also gain or replace a byte cutoff after classification, when
* `boundMcpToolResult` removes events to fit the message limit.
* - `backend_error`: the log backend reported failure (a stream `error` event,
* a failing `end` event, or a non-zero exit the server did not cause). This
* is a closed error, never a truncated success.
Expand All @@ -32,7 +34,9 @@ export type McpLogStopReason =
/** The bound that ended a successful, incomplete collection. */
export type McpLogCutoff = {
/** `events` counts `type: "log"` events only; `bytes` is the shared stdout
* plus stderr capture budget; `time` is the collection window. */
* plus stderr capture budget, or the serialized result limit when
* `boundMcpToolResult` removed events after collection; `time` is the
* collection window. */
readonly bound: "events" | "bytes" | "time";
readonly limit: number;
};
Expand Down
Loading
Loading