FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

[Server] Validate tool output against outputSchema by soyuka · Pull Request #515 · modelcontextprotocol/php-sdk · GitHub

Repository navigation

[Server] Validate tool output against outputSchema - #515

Merged
chr-hertel merged 2 commits into
modelcontextprotocol:mainfrom
soyuka:feat/validate-output-schema
Oct 6, 2026
Merged

chr-hertel merged 2 commits into
modelcontextprotocol:mainfrom
soyuka:feat/validate-output-schema

Conversation

soyuka commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Problem

The specification states that a server must produce structured results that conform to a declared outputSchema:

Tools may also provide an output schema for validation of structured results. If an output schema is provided:

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

The wording is the same in 2025-11-25, 2026-07-28 and the draft.

CallToolHandler validated the arguments against inputSchema, then sent the result without checking it against outputSchema. A tool could return non-conforming structuredContent. A strict client then rejected the call, and the server logged nothing about why.

Change

CallToolHandler::validateStructuredContent() runs on the result just before it is returned. It covers a value the SDK wrapped and a CallToolResult the tool built itself.

On a mismatch the handler logs at error level and answers with a tool execution error:

{
  "content": [
    {
      "type": "text",
      "text": "Invalid structured output for tool 'get_weather': Missing required properties: `temperature`."
    }
  ],
  "isError": true
}

A tool execution error keeps the reason visible to the model, which can then fall back to content. A protocol error would hide it.

The check is skipped in three cases:

  • The tool declares no outputSchema.
  • The result carries no structuredContent. CallToolHandler already logs a warning for that case, and docs/servers/tools.md documents the omission as normal for a list served under a revision before SEP-2106. Turning it into an error would contradict that table.
  • The result is already marked isError: true. Its content is a failure message, not the declared output.

The error summary code that built the inputSchema message is now summarizeValidationErrors(), shared by both paths instead of duplicated.

How the other SDKs behave

SDK Validates On failure
TypeScript, packages/server/src/server/mcp.ts:266-297 Always, when outputSchema is set CallToolResult with isError: true
Java, McpAsyncServer.java:378-461 Always, when outputSchema is set CallToolResult with isError: true
Python, func_metadata.py:91-123 High-level layer only, opt-out structured_output=False CallToolResult with isError: true
Go, mcp/server.go:388-402 Typed AddTool[In, Out] only JSON-RPC error

TypeScript and Java both skip the check on an isError result, and both validate a result the tool built itself. This PR follows them.

SEP-2140 proposes the same behavior in item 4: report output validation failures as tool execution errors. Its tool-resolution half already landed in this SDK. Note that the output-validation wording is not yet in the draft specification, so this PR follows the SEP as a precedent, not as binding text.

Compatibility

A server whose tool emits non-conforming structuredContent today gets a successful result. After this change it gets an error result. The CHANGELOG entry is marked [BC Break] for that reason. Tell me if you want the tag dropped, because the earlier behavior violated the specification.

Tests

Six tests in tests/Unit/Server/Handler/Request/CallToolHandlerTest.php, each written to fail first:

  • A required property is missing.
  • A property has the wrong type.
  • Conforming structured content is sent unchanged.
  • A tool with no outputSchema is not validated.
  • A CallToolResult the tool built itself is validated.
  • An isError result is returned untouched.

make cs, make phpstan and the touched test files pass. tests/Inspector/Stdio/StdioEnvVariablesTest.php passes too, because examples/server/env-variables/EnvToolHandler.php is the one fixture that declares an outputSchema end to end.

chr-hertel added the Server Issues & PRs related to the Server component label Oct 5, 2026
return null;
}

$validationErrors = $this->schemaValidator->validateAgainstJsonSchema($result->structuredContent, $tool->outputSchema);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

$result->structuredContent could also be an empty list - which is valid as result, but validateAgainstJsonSchema turns that into new \stdClass()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

or does that work against an array schema still 🤔 shouldn't right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

good catch, the coercion now only applies to arguments (always an object), [] is validated as an array. While at it an empty object result was sent as [] on the wire, it's now kept as {}.

chr-hertel previously approved these changes Oct 6, 2026

chr-hertel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks @soyuka!

soyuka added 2 commits October 6, 2026 23:37
The specification requires a server to produce structured results that
conform to a declared outputSchema (2025-11-25 server/tools.mdx:340), but
the SDK only validated arguments against inputSchema. A tool could ship
non-conforming structuredContent and strict clients would reject the call
with no diagnostic on the server side.

A mismatch is now answered with a CallToolResult carrying isError: true,
so the model reads the reason and can fall back to content, as the
TypeScript, Python and Java SDKs do. Validation is skipped when the tool
declares no outputSchema, when the result carries no structuredContent
(already warned about), and when the result is already an error.
An empty PHP array was coerced to an object before schema validation,
so `[]` failed an array outputSchema although it is sent as `[]`. The
coercion now only applies to tool arguments, which are always an
object. An empty object result is kept as stdClass so it is sent as
`{}` rather than `[]`.
chr-hertel force-pushed the feat/validate-output-schema branch from 61f57f9 to 99014a8 Compare October 6, 2026 21:37
chr-hertel added the improves spec compliance Improves consistency with other SDKs such as TyepScript label Oct 6, 2026
chr-hertel merged commit 873804c into modelcontextprotocol:main Oct 6, 2026
27 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improves spec compliance Improves consistency with other SDKs such as TyepScript Server Issues & PRs related to the Server component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants


Back | FazBrowse Home | New Git URL