anthropics / anthropics/claude-agent-sdk-typescript

Large MCP tool result offload: structuredContent produces unnavigable single-line file vs content array's pretty-printed output

Offen
#187 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
bug
Vorherrschende Sprache
Shell
Sterne
1.8k
Forks
226
PR-Merge-Kennzahlen
Keine gemergten PRs in 30 T.

Beschreibung

## Summary

When the SDK offloads a large MCP tool result to a temp file, the serialization path differs between `content` and `structuredContent` in a way that makes `structuredContent`-based results **completely unnavigable** — worse than the `content` array approach, and the opposite of what the MCP spec intends.

## Background

We hit this while investigating why a production agent couldn't navigate a large tool result (~135K chars of structured extraction data). The agent was receiving an offloaded file but couldn't use `Read` with offset/limit or `Grep` effectively.

## Root Cause

The SDK's large-result offload path serializes the tool result differently depending on which field is present:

**`content` array path:**
```
JSON.stringify(result.content, null, 2)
```
Produces a pretty-printed JSON array. Each text block's `"text"` field value is a JSON string, so multi-line text is still collapsed to a single escaped string — but the outer structure has many lines.

**`structuredContent` path:**
```
JSON.stringify(result.structuredContent)
```
No indent argument. Produces a single minified line regardless of the object's complexity.

The `structuredContent` path should use `JSON.stringify(result.structuredContent, null, 2)` to match the intent of the field and produce a navigable file.

## Empirical Validation

We wrote a probe against SDK v0.2.41 that runs a real `query()` with a fake MCP tool returning 150 location objects (~135K chars / ~3,469 lines of pretty-printed JSON). We tested all three result shapes and inspected the resulting offloaded files:

| Return format | File lines | Max line length | Agent-navigable? |
|---|---|---|---|
| `content: [{type: 'text', text: prettyJSON}]` | 5 | 149,500 chars | ❌ |
| `structuredContent: parsedObject` | 1 | 88,566 chars | ❌ (worst) |
| `toolResult: prettyJSON` *(pre-standard, removed)* | 3,468 | 95 chars | ✅ |

**`content` array (5 lines):** The SDK wraps the array with `JSON.stringify(array, null, 2)`. The outer structure is pretty-printed, but the `"text"` field value is a JSON string — its embedded newlines are escaped as `\n`, collapsing the entire payload to a single 149K-char line:

```
L1 (1 char): [
L2 (3 chars): {
L3 (19 chars): "type": "text",
L4 (149,500 chars): "text": "{\n \"document_id\": \"...\",\n \"extractions\": [\n ..."
L5 (3 chars): }
L6 (1 char): ]
```

**`structuredContent` (1 line):** The entire object is minified to a single line. Even larger tools are worse with this format than with the `content` array.

**`toolResult` string (3,468 lines, max 95 chars):** Because `toolResult` is a raw string, the SDK writes it directly to the file without re-encoding. The pretty-printed JSON lands intact and is fully navigable with `Read(offset, limit)` and `Grep`.

## Discovery of the `toolResult` behavior

During this investigation we discovered that the SDK still handles a `toolResult` string field on the result object — routing it through a code path that writes the string directly to the temp file. This produced the only navigable output in our tests.

We understand `toolResult` was a pre-standard attribute that was removed before the MCP spec launched, so we're not proposing it as a solution. We're reporting it here because its behavior (raw string → direct file write → navigable output) clearly demonstrates what the `structuredContent` path should achieve once the missing `null, 2` indent is added.

## Expected Behavior

When a tool returns `structuredContent`, the offloaded file should be pretty-printed:

```
JSON.stringify(result.structuredContent, null, 2)
```

This would produce a file with many short lines (one per JSON key/value), making it navigable with `Read(offset, limit)` and `Grep` — which is exactly the point of offloading to a file in the first place.

## Workaround (Standards-Compliant)

While waiting for a fix, we discovered that returning one `content` text block **per line** of the pretty-printed JSON produces a navigable file. The SDK serializes the array with `JSON.stringify(content, null, 2)` — with many short blocks, each block becomes ~4 short file lines:

```typescript
content: JSON.stringify(data, null, 2).split('\n').map(line => ({ type: 'text', text: line }))
```

Result: 13,877 lines, max 113 chars — fully navigable. Not elegant, but MCP-compliant and effective.

## Environment

- SDK version: `@anthropic-ai/claude-agent-sdk` v0.2.41
- MCP spec reference: 2025-11-25 §tools (structured content)
- Probe payload: 150 location objects, ~135K chars / ~3,469 lines pretty-printed

Beitragsleitfaden

Für dieses Repository ist kein Beitragsleitfaden indexiert

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.