MCP Tool Schema: Inputs, Structured Output, and Errors
Learn how MCP tool input and output schemas work, how errors differ from successful results, and how to test the contract a server actually advertises.
Version and test detailsSchema behavior was checked September 15, 2026 against MCP 2026-07-28 and the Python SDK 2.2.0 fixture. Other SDKs may generate different schemas.
On this page
Version verifiedChecked September 15, 2026 against MCP 2026-07-28. Review method.
The short version
- inputSchema describes the arguments a client may send; outputSchema describes structured data a successful tool result may return.
- structuredContent is server-produced tool output, not a constraint on text generated by a language model.
- Inspect and test the advertised schema because type annotations, SDK defaults, and runtime validation do not always enforce the contract you intended.
An MCP tool schema is a contract between a server and its callers. inputSchema describes the arguments the tool accepts. outputSchema, when present, describes its structured successful result. The actual call still travels inside an MCP request, and failures do not all use the same path.
This page assumes you already know when to choose a tool. If that decision is unresolved, start with tools versus resources versus prompts. If you want a complete working server, use the tested Python build guide.
The tool definition and the call are different objects
Clients discover a tool definition before calling it. Under the current tools specification, that definition can include a name, title, description, icons, inputSchema, outputSchema, annotations, and metadata S123.
A later tools/call request identifies the tool and supplies arguments. A successful result may contain human-readable content, structuredContent, resource links, embedded resources, or media. The definition describes what should be sent and returned; it is not the returned value itself.
For the build-guide example, the intended input is:
{
"type": "object",
"properties": {
"service_id": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]{0,39}$"
}
},
"required": ["service_id"]
}
The intended structured output is:
{
"type": "object",
"properties": {
"service_id": { "type": "string" },
"team": { "type": "string" },
"runbook_path": { "type": "string" }
},
"required": ["service_id", "team", "runbook_path"],
"additionalProperties": false
}
Input constraints should match the real boundary
Use the narrowest input that completes the task. A service catalog lookup needs a service identifier; it does not need an arbitrary URL, file path, SQL statement, or command. This improves validation and makes the permission boundary easier to explain.
Required fields, types, length limits, patterns, enums, and object closure are separate decisions. Do not assume a language type annotation supplies all of them.
The tested Python SDK 2.2.0 fixture illustrates why inspection matters. Its Annotated[str, Field(pattern=...)] parameter produced the required string and pattern, but the generated top-level object omitted additionalProperties: false. An extra top-level field was ignored rather than rejected. That is an observed SDK behavior for this registration path, not a universal MCP rule.
If rejecting undeclared fields is consequential, verify that the SDK advertises and enforces that rule, or choose a lower-level registration path where you own the complete schema and validation. Do not rely on ignored input as a security control.
Output schema and structured content belong together
outputSchema describes structuredContent produced by the server. It does not constrain a language model’s answer. Those mechanisms may be used in the same application, but they have different producers and different failure modes.
When a tool declares an output schema, test the returned object against it. In the local fixture, the ServiceOwner Pydantic model rejects undeclared output fields and requires all three named properties. The successful checkout result is:
{
"service_id": "checkout",
"team": "commerce-platform",
"runbook_path": "/runbooks/checkout"
}
Keep human-readable content consistent with structured content when returning both. A polished sentence cannot repair an invalid structured result.
Validation errors and execution errors are not successes
Three failures deserve separate tests:
- Invalid arguments: a missing field, wrong type, or failed pattern should be rejected before the handler performs work.
- Expected execution failure: a well-formed request for an absent record should return a tool error that the caller can understand and correct.
- Unexpected crash or protocol failure: internal details should remain in server logs while the caller receives a bounded failure, not a plausible success payload.
The Python v2 SDK uses ToolError for an anticipated tool failure. Returning a string such as "not found" would leave isError false and misrepresent the outcome. The MCP tools specification separates successful results, error results, and request-level errors S123.
A practical schema test checklist
Before depending on a tool contract, record and check:
- the exact SDK and protocol versions;
- the schema returned by tool discovery;
- one valid call for each meaningful result branch;
- missing, wrong-type, extra, boundary, and hostile-looking arguments;
- a known domain failure;
- a deliberately invalid output object;
- whether error text leaks paths, credentials, queries, or tracebacks;
- whether the same call works through the intended transport; and
- the negotiated protocol revision.
The local lookup_service_owner fixture passed 11 automated checks through an in-memory client and a real stdio subprocess. That receipt applies to the pinned example only. It does not prove that another SDK, host, transport, or server version behaves the same way.
Version the contract, not just the article
The current site explanations use MCP 2026-07-28 S054. Older clients and SDK branches may negotiate earlier revisions and expose different lifecycle or result behavior. Keep the protocol revision, installed SDK version, generated schema, and observed transport together in the test record.
When any of those changes, rerun the fixtures before updating a “tested” label. For a broader account of version negotiation, use MCP protocol versions.