mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-05-04 10:09:47 +00:00
55 lines
2.1 KiB
Markdown
55 lines
2.1 KiB
Markdown
|
|
# Payload budgeting
|
|||
|
|
|
|||
|
|
Hosts cap tool-result text. claude.ai and Claude Desktop truncate at roughly
|
|||
|
|
**150,000 characters**; Claude Code at ~25k tokens. When a tool result exceeds
|
|||
|
|
the cap, the host substitutes a file-pointer string in place of your JSON. The
|
|||
|
|
widget then receives non-JSON in `ontoolresult`, `JSON.parse` throws, and the
|
|||
|
|
user sees something like *"Bad payload: SyntaxError: Unexpected token 'E'"* —
|
|||
|
|
with no hint that size was the cause.
|
|||
|
|
|
|||
|
|
## Symptom → cause
|
|||
|
|
|
|||
|
|
| Symptom | Likely cause |
|
|||
|
|
|---|---|
|
|||
|
|
| Widget shows a JSON parse error on `content[0].text` | Result over the host cap; host swapped in a file-pointer string |
|
|||
|
|
| Works for one query, breaks for "all of X" | Row count × column count crossed the cap |
|
|||
|
|
| Works in MCP Inspector, breaks in Desktop | Inspector has no cap; Desktop does |
|
|||
|
|
|
|||
|
|
## Strategy
|
|||
|
|
|
|||
|
|
Cap your own payload at ~130KB and degrade in order:
|
|||
|
|
|
|||
|
|
1. **Ship full rows** when `JSON.stringify(rows).length` is under the cap.
|
|||
|
|
2. **Prune columns** to those the rendering spec actually references. Walk the
|
|||
|
|
spec for both `field: "..."` keys *and* `datum.X` / `datum['X']` inside
|
|||
|
|
expression strings — if the spec aliases a column via a `calculate`
|
|||
|
|
transform, the alias appears as `field:` but the source column only appears
|
|||
|
|
as `datum.X`, and dropping it leaves the widget with NaN.
|
|||
|
|
3. **Truncate rows** as a last resort and include `{ truncated: N }` in the
|
|||
|
|
payload so the widget can label it.
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
const MAX = 130_000;
|
|||
|
|
let out = rows;
|
|||
|
|
if (JSON.stringify(out).length > MAX) {
|
|||
|
|
const keep = referencedFields(spec); // field: + datum.X refs
|
|||
|
|
out = rows.map((r) => pick(r, keep));
|
|||
|
|
if (JSON.stringify(out).length > MAX) {
|
|||
|
|
const per = JSON.stringify(out[0] ?? {}).length || 1;
|
|||
|
|
out = out.slice(0, Math.floor(MAX / per));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Heavy assets go via `callServerTool`, not the result
|
|||
|
|
|
|||
|
|
Geometry, image bytes, or any blob the widget needs but Claude doesn't should
|
|||
|
|
be served by a separate tool the widget calls after mount:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
const topo = await app.callServerTool({ name: "get-topojson", arguments: { level } });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Mark that helper tool with `_meta.ui.visibility: ["app"]` so it doesn't appear
|
|||
|
|
in Claude's tool list.
|