MCP structured output is a contract, and most servers keep half of it

18 August 2026

MCP Structured Output Validator The contract only the server has to keep. Runs in your browser.

Declaring an outputSchema on an MCP tool is not documentation and it is not a hint. It is a promise, and the specification uses the strongest word it has:

If an output schema is provided:

  • Servers MUST provide structured results that conform to this schema.
  • Clients SHOULD validate structured results against this schema.

That asymmetry is the whole subject. The server is required to conform, and the client is only advised to check. So a server that declares a schema and drifts from it produces no error anywhere: the client passes the result along, the model reads something shaped differently from what the schema promised, and the failure surfaces much later as a program doing the wrong thing with a field it thought it understood.

The three parts, and what each is for

A tool result can carry two things at once, and the names are easy to confuse.

content is the unstructured half, and it is always present. It is an array of blocks: text, image, audio, resource links, embedded resources. This is what the model reads.

structuredContent is the structured half. It is JSON returned in its own field, so that something downstream of the model can rely on the shape of an answer rather than parsing prose.

outputSchema is the JSON Schema on the tool definition describing what structuredContent will look like. Note the exact relationship, because it is the thing people get backwards: outputSchema describes structuredContent, not the result. It says nothing about content, about isError, or about the envelope around them.

One more distinction the specification calls out explicitly, because the naming collides with a different feature entirely:

structuredContent is server-produced result data and is unrelated to LLM “structured outputs” (schema-constrained model generation).

If you arrived here looking for how to make a model emit JSON matching a schema, this is not that. This is a server describing its own return value.

The half nobody implements

Here is the rule most implementations skip:

For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.

It is a SHOULD rather than a MUST, so nothing is broken by ignoring it, which is exactly why it gets ignored. The consequence is not subtle. A client that does not read structuredContent, because it was written against an earlier revision or simply never implemented it, sees a tool result whose content array is empty or nearly so. The tool ran, the data came back, and the model was handed nothing.

The specification’s own example does it properly, and it is worth copying literally:

{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"temperature\": 22.5, \"conditions\": \"Partly cloudy\", \"humidity\": 65}"
      }
    ],
    "structuredContent": {
      "temperature": 22.5,
      "conditions": "Partly cloudy",
      "humidity": 65
    }
  }
}

The same data twice, deliberately. Once for a client that understands the structured field, once for everything else.

isError is not a protocol error, and the difference matters

MCP has two ways to report that something went wrong, and choosing the wrong one produces an agent that gives up when it should retry, or loops when it should stop.

Protocol errorTool execution error
ShapeJSON-RPC error objectNormal result with isError: true
MeansThe request was malformed or unroutableThe tool ran and the operation failed
ExamplesUnknown tool, invalid argumentsRate limited, record not found, business rule rejected it
Reaches the modelNoYes, as content it can read and act on

That last row is why this matters. A protocol error is handled by the client and the model usually never learns why. A tool execution error is delivered to the model as content, so it can read “the rate limit resets in 30 seconds” and behave accordingly.

The practical rule: if the tool did its job and the answer is “no”, that is isError: true with an explanation in content. Reserve JSON-RPC errors for requests you could not process at all. A server returning a protocol error for a not-found record has taken the information away from the only participant who could have used it.

Worth noting for anyone matching codes: the specification’s example for an unknown tool uses -32602, InvalidParams, rather than MethodNotFound. The method was tools/call and it exists. The tool name inside it was wrong, and that is a parameter problem.

How this shows up

Rarely as an exception. The reports look like this:

  • A workflow reads result.structuredContent.items[0].id and gets undefined intermittently, because the server omits the field when a list is empty while the schema marks it required.
  • A tool works perfectly in one client and returns “nothing” in another, which is the missing TextContent block.
  • An agent retries a permanent failure forever, because the server returned a protocol error and the model was told only that the call failed.
  • A field changes from number to string in a patch release and nothing fails until a comparison starts behaving oddly.

Every one of those is a contract violation nobody is enforcing, because the enforcement is a SHOULD on the client and the client did not.

Where the ecosystem actually is

Structured output arrived with the 2025-06-18 revision, which is recent enough that “the specification says so” and “your stack does it” are different claims. Three things are worth knowing before you design around it.

Client and framework support is uneven. Support for reading structuredContent and validating against outputSchema has been requested and tracked in mainstream agent frameworks well after the revision shipped, for example in the Vercel AI SDK. If your agent runs through a framework rather than a raw SDK, check what that framework does with the field before assuming the contract is enforced anywhere.

The guidance itself is still being clarified. There is an open specification issue asking exactly when a server should use structuredContent versus content, SEP-1624, which is a fair signal that the answer is not obvious from the text alone. If you have read the spec twice and remain unsure whether your case wants both, you are not misreading it.

There is a real design argument against using it by default. A discussion in the specification repository makes the case that structured output pulls MCP back toward being an API protocol, where the point of returning text was that a model could read it and adapt. That argument is worth understanding rather than dismissing, because it predicts the failure mode: a tool whose output is rigidly typed and whose content block is empty is useful to your code and useless to the model that has to decide what to do next.

The practical reading of all three: declare an output schema when a program consumes the fields, keep the text block for the model, and do not assume any particular client is checking your conformance for you.

What to do about it

If you write servers, three things:

  1. Only declare outputSchema if you intend to conform to it. A tool with no output schema and honest text is more useful than one with a schema it drifts from, because the second invites downstream code to trust a shape.
  2. Validate your own output against the schema you published, in your own tests. The client is not obliged to catch this and mostly will not.
  3. Emit the TextContent block as well. It costs a JSON.stringify and it is the difference between working everywhere and working in the clients you happened to test.

If you write clients or agents, two:

  1. Validate structuredContent against the declared schema, and treat a mismatch as a server defect rather than as data. The specification says you SHOULD, and the cost of skipping it lands a long way from the cause.
  2. Do not assume structuredContent exists. It appears only when the tool declares a schema, so the path where it is absent is the normal one.

The structured output validator runs the checklist above against a result you paste, including the conformance check the client is only advised to do. The tool schema validator covers the neighbouring problem on the input side: which JSON Schema keywords a client actually enforces when it hands a schema to a model. Constraints such as minimum, pattern and maxLength are frequently dropped rather than applied, so a schema can be perfectly valid and still constrain nothing. The same instinct applies on the output side. A schema is a description, and something has to enforce it.

A checklist you can run against a real tool

Take one tool that declares an outputSchema, call it, and check these in order. Every one of them is a defect that produces no error message.

  1. Is structuredContent present at all? If the schema is declared and the field is missing, the server is already outside the MUST.
  2. Does it validate against the declared schema? Run the actual result through a JSON Schema validator, not by eye. Required fields that are omitted when a collection is empty are the most common miss.
  3. Are the types stable across cases? Call it with a result, with an empty result, and with an error. A field that is a number normally and a string in the empty case will pass a casual read and break a comparison later.
  4. Is content non-empty? If the only place the data appears is structuredContent, any client that does not read that field gets nothing.
  5. Does the text block contain the same data? Not a summary of it. The backwards-compatibility rule is about the serialized JSON, so a client reading only content sees the same values.
  6. Is a failed operation isError: true rather than a JSON-RPC error? Force one: request a record that does not exist and look at which shape comes back.
  7. Does the schema describe the structured field only? A schema that tries to describe the whole result, with content and isError in it, is describing the envelope rather than the payload and will never match.

When this advice is wrong

If your tool returns prose, do not invent a schema for it. A summariser, a search tool that returns snippets, an explainer: these have text as their actual output, and wrapping it in {"result": "..."} adds a contract without adding structure. outputSchema earns its place when a program downstream of the model consumes the fields.

If you control both ends, the backwards-compatibility block is optional. The argument for duplicating the JSON into content is interoperability with clients you did not write. In a closed system where you ship the server and the client together and pin both, it is duplicated tokens on every call for a compatibility case that cannot arise. Skip it deliberately, and write down that you did, because the day somebody else connects to that server it becomes a bug.

And if you are on a client that predates structured content, none of this helps today. Check what your client actually reads before designing around a field it ignores, and the fastest way to find out is to return both and see which one reaches the model.