Structured Output (structuredContent + outputSchema)
Structured Output — structuredContent + outputSchema
Section titled “Structured Output — structuredContent + outputSchema”Available since: v4.5.26 (2026-07-11)
The four I/O core tools (read_file, write_file, edit_file, multi_edit)
now publish an MCP outputSchema and return a typed structuredContent payload
alongside the byte-identical text fallback. This makes the server interoperate
with any MCP client, not just Claude with a project CLAUDE.md.
Why this matters
Section titled “Why this matters”Until v4.5.25, the contract for what a tool returned lived in the project’s
CLAUDE.md (and an internal help_content.go). Third-party MCP clients —
LangGraph agents, VSCode Copilot Chat, custom UIs, test harnesses — had no
machine-readable contract:
- No schema — clients had to scrape text trailers (
# content_hash:) to find the OCC token. - No typed fields — every value was a string inside a multi-line block.
- No interop — pipelines, JSON-RPC routers, and dashboards couldn’t
react to specific outcomes (e.g. “did the edit hit
risk_warning == HIGH?”).
v4.5.26 closes that gap with two MCP-spec features:
outputSchema— a JSON Schema published once per session viatools/list. Describes the shape of every successful response.structuredContent— a JSON object in the response that conforms to that schema. Carries the same information as the text block but addressable by field.
Claude with this project’s CLAUDE.md (or with the docs site) keeps
reading the text fallback — there’s no behavior change for it.
Which tools got it (and which didn’t)
Section titled “Which tools got it (and which didn’t)”| Tool | outputSchema | structuredContent | Notes |
|---|---|---|---|
read_file | ✅ | ✅ | content_hash omitted on multi-file reads. |
write_file | ✅ | ✅ | Also fixed the silent auto-OCC trip on write→edit chains (now records the post-write hash). |
edit_file | ✅ | ✅ | Adds parent_backup_id for step-through undo. |
multi_edit | ✅ | ✅ | Same shape as edit_file plus successful_edits/total_edits. |
list_directory | ❌ | ❌ | Already had output_format: json — equivalent contract, different mechanism. |
search_files | ❌ | ❌ | output_format: json / count_only cover the same need. |
| All other 14 tools | ❌ | ❌ | Reserved for FASE 2+ (per docs/FASE1-STRUCTURED-OUTPUTSCHEMA.md). |
Schemas
Section titled “Schemas”Schemas live in output_schemas.go (one json.RawMessage per tool).
The descriptions on every property are the interop contract for
third-party clients — treat them as the canonical English-language spec.
The four schemas below are the exact JSON published by tools/list.
read_file — readFileOutputSchema
Section titled “read_file — readFileOutputSchema”{ "type": "object", "properties": { "content": { "type": "string", "description": "File body (possibly truncated; truncation is annotated inline)" }, "content_hash": { "type": "string", "description": "FNV-1a 8-hex hash of the FULL file on disk. Pass as expected_hash on a subsequent edit_file/multi_edit to detect concurrent external changes (OCC). Absent on multi-file reads." } }, "required": ["content"]}write_file — writeFileOutputSchema
Section titled “write_file — writeFileOutputSchema”{ "type": "object", "properties": { "path": {"type": "string", "description": "Absolute normalized path the file was written to"}, "bytes_written": {"type": "integer"}, "content_hash": { "type": "string", "description": "FNV-1a 8-hex hash of the file as written. Pass as expected_hash on a subsequent edit_file/multi_edit to chain operations without re-reading." }, "backup_id": { "type": "string", "description": "Present when a safety backup was auto-created (adaptive guard). Full ID for backup(action:'restore') or backup(action:'undo_last')." }, "feedback": { "type": "string", "description": "Non-blocking warning (truncation/inflation/rewrite heuristics)" }, "message": { "type": "string", "description": "Human-readable summary, identical to the text content block" } }, "required": ["path", "bytes_written", "message"]}edit_file — editFileOutputSchema
Section titled “edit_file — editFileOutputSchema”{ "type": "object", "properties": { "path": {"type": "string"}, "replacements": {"type": "integer", "description": "Number of replacements applied"}, "lines_added": {"type": "integer"}, "lines_removed": {"type": "integer"}, "total_lines": {"type": "integer", "description": "Total lines in the file after the edit"}, "content_hash": { "type": "string", "description": "Post-edit FNV-1a 8-hex hash. Pass as expected_hash on the NEXT edit to chain edits without re-reading." }, "backup_id": {"type": "string", "description": "Full backup ID for backup(action:'restore') / undo"}, "parent_backup_id": { "type": "string", "description": "Previous backup in the undo chain (step-through undo)" }, "risk_warning": {"type": "string"}, "structure_warning": { "type": "string", "description": "Delimiter/balance warning introduced by this edit" }, "integrity": { "type": "string", "description": "Post-edit integrity verification result (HIGH/CRITICAL ops)" }, "external_change": { "type": "string", "description": "Auto-OCC notice: file changed on disk since last session read/write" }, "message": { "type": "string", "description": "Human-readable summary incl. diff, identical to the text content block" } }, "required": ["path", "replacements", "lines_added", "lines_removed", "total_lines", "message"]}multi_edit — multiEditOutputSchema
Section titled “multi_edit — multiEditOutputSchema”{ "type": "object", "properties": { "path": {"type": "string"}, "successful_edits": {"type": "integer"}, "total_edits": {"type": "integer"}, "lines_added": {"type": "integer"}, "lines_removed": {"type": "integer"}, "total_lines": {"type": "integer"}, "content_hash": { "type": "string", "description": "Post-edit FNV-1a 8-hex hash (OCC token for the next edit)" }, "backup_id": {"type": "string"}, "parent_backup_id": {"type": "string"}, "risk_warning": {"type": "string"}, "structure_warning": {"type": "string"}, "integrity": {"type": "string"}, "message": {"type": "string"} }, "required": ["path", "successful_edits", "total_edits", "message"]}Worked example
Section titled “Worked example”read_file then edit_file on the same file, with the OCC token flowing
through structuredContent end-to-end:
# Pseudocode for a Python MCP client (mcp >= 1.0)async with mcp_client.session() as s: # 1) Read r = await s.call_tool("read_file", {"path": "main.go"}) body = r.structuredContent["content"] h0 = r.structuredContent["content_hash"] # "1a2b3c4d"
# 2) Edit using h0 as the OCC token r2 = await s.call_tool("edit_file", { "path": "main.go", "old_text": "v3.0.0", "new_text": "v4.0.0", "expected_hash": h0, # bound to read_file's content_hash }) h1 = r2.structuredContent["content_hash"] # post-edit hash parent = r2.structuredContent.get("parent_backup_id")
# 3) Step-through undo via the structured parent id (no text scraping) if parent: await s.call_tool("backup", { "action": "undo_last", "file_path": "main.go", })Why the four fields moved (and the rest stayed text-only)
Section titled “Why the four fields moved (and the rest stayed text-only)”| Field | Why structured | Why not yet |
|---|---|---|
content_hash | The OCC token must round-trip from read_file → edit_file reliably. The v4.5.x text trailer # content_hash: is indistinguishable from Markdown content (see Bug B1). Anchoring edits on a # content_hash: trailer caused downstream consumers to lose atomic batches — moving it into a typed field makes the contract machine-checkable. | |
content (read) | Lets clients skip re-parsing the multi-line body and apply start_line/end_line slicing on the wire. | |
bytes_written, replacements, lines_added/removed, total_lines | Counts that downstream agents and dashboards want to react to (e.g. abort on risk_warning == "HIGH"). | |
backup_id, parent_backup_id | Walk the undo chain without scraping the chain: segment from the text. | |
risk_warning, structure_warning, integrity, external_change, feedback | Pre-classified strings — third-party dashboards route on them rather than re-running regex over the human summary. | |
message | Identical to the text block. Lets clients that prefer JSON still get a human fallback for log files. |
The other 14 tools keep their text-only responses because either they
already expose a structured alternative (list_directory.output_format,
search_files.output_format) or they’re rarely consumed programmatically
(minify_js, wsl, git). FASE 2 of the rollout (docs/FASE1-STRUCTURED-OUTPUTSCHEMA.md)
will revisit them.
For authors of MCP clients
Section titled “For authors of MCP clients”- Discover the schema with
tools/list— every tool that supports structured output carries anoutputSchemafield on the tool descriptor. - Read the typed fields from
result.structuredContent(TypeScript SDK) orresult.contentblocks (older SDKs — both shapes are present). - Fall back to text when a tool you call has no schema: the text response is still the canonical source of truth for humans and log scraping.
- Schema coherence is enforced by
output_schema_test.go— if you add a payload field, you MUST add it to the correspondingoutput_schemas.goentry, and vice versa. Tests fail on drift in either direction.
See Also
Section titled “See Also”- Core Tools —
read_file/write_file/edit_file/multi_editreference - Safe Editing — REGLA 4 explains the OCC chain using
content_hash - Changelog — full v4.5.26 entry
output_schemas.go— the source of truth (canonical JSON)output_schema_test.go— schema↔payload coherence tests- MCP spec —
outputSchemaandstructuredContent(2025-11-25 revision)