Skip to content

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.


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:

  1. outputSchema — a JSON Schema published once per session via tools/list. Describes the shape of every successful response.
  2. 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.


TooloutputSchemastructuredContentNotes
read_filecontent_hash omitted on multi-file reads.
write_fileAlso fixed the silent auto-OCC trip on write→edit chains (now records the post-write hash).
edit_fileAdds parent_backup_id for step-through undo.
multi_editSame shape as edit_file plus successful_edits/total_edits.
list_directoryAlready had output_format: json — equivalent contract, different mechanism.
search_filesoutput_format: json / count_only cover the same need.
All other 14 toolsReserved for FASE 2+ (per docs/FASE1-STRUCTURED-OUTPUTSCHEMA.md).

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.

{
"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"]
}
{
"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"]
}
{
"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"]
}
{
"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"]
}

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)”
FieldWhy structuredWhy not yet
content_hashThe OCC token must round-trip from read_fileedit_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_linesCounts that downstream agents and dashboards want to react to (e.g. abort on risk_warning == "HIGH").
backup_id, parent_backup_idWalk the undo chain without scraping the chain: segment from the text.
risk_warning, structure_warning, integrity, external_change, feedbackPre-classified strings — third-party dashboards route on them rather than re-running regex over the human summary.
messageIdentical 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.


  • Discover the schema with tools/list — every tool that supports structured output carries an outputSchema field on the tool descriptor.
  • Read the typed fields from result.structuredContent (TypeScript SDK) or result.content blocks (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 corresponding output_schemas.go entry, and vice versa. Tests fail on drift in either direction.