An MCP tool returns structured output by declaring an outputSchema in its definition and putting the JSON value in the structuredContent field of the result, rather than by formatting a table and hoping the reader gets it back out. A tool result carries two things at once: a list of unstructured content blocks, which is what the model reads, and an optional structuredContent value, which is what a program reads. Declaring the schema is what turns the second one from a convention into something a client can check.
A tool result in MCP has three fields worth knowing. The content array holds the unstructured blocks — text, images and so on — and it is always present. The structuredContent field is an optional JSON value that represents the structured result, and it conforms to the tool's outputSchema when the tool declares one. The isError flag says whether the call ended in failure. Nothing in that shape forces a caller to parse prose, which is the whole point of it.
Why servers still send the serialised copy
Servers returning structuredContent are advised to also return the same JSON serialised into a text content block, and the reason is compatibility rather than redundancy. A client that predates structured output, or one that simply ignores the field, still gets the answer because the answer is also sitting in the content array where it has always been. The cost is duplication in the payload; the benefit is that adding structure to a tool does not break anybody who was reading it the old way.
The compatibility argument gets sharper for non-object results. The specification's structuredContent can be any JSON value — an object, an array, a string, a number, a boolean or null — and a client built against the older, object-only definition will not know what to do with an array. Emitting the serialised text fallback is what keeps such a tool usable by both, which is worth doing while old clients are still in the field.
What declaring an outputSchema costs
Declaring an outputSchema is a promise, and promises constrain what you can change later. When a tool provides an output schema, the server has to make its results conform to it and clients are expected to validate against it, so a field you quietly stop returning is no longer a small edit — it is a result that fails validation at somebody else's end. That is the same trade as any published interface, and it is worth making deliberately rather than by writing the schema first and discovering the constraint later.
The schema itself has become more expressive than it once was. The proposal that loosened these fields keeps inputSchema rooted at an object type while allowing the rest of JSON Schema's vocabulary for validation, and lets outputSchema be any valid JSON Schema, so a tool that returns a list of records can describe it as an array of objects instead of wrapping it in a single-key container purely to satisfy the older rule.
Errors that come from the tool itself belong inside the result with isError set to true, not as a protocol-level error response, and the reason is stated plainly in the specification: otherwise the model cannot see that anything went wrong and cannot correct itself. Protocol errors are reserved for the exceptional cases — the tool could not be found, the server does not support tool calls at all. A quota that ran out or an address that failed validation is the tool talking, and the model is the one who needs to hear it.
Plenty of working servers declare no outputSchema and simply serialise their answer into a text block, and this product's own MCP server is one of them: every tool returns a pretty-printed JSON object inside a single text content block. That shape works with every client, gives the model something readable, and gives a program nothing to validate against. It is the right trade while the consumers are all language models and the wrong one the moment a piece of software starts depending on a particular field being present.