You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add ClientSession/Client opt-out for automatic tool-result validation
Motivation:
call_tool() revalidates a successful CallToolResult's structured_content
against the tool's declared output_schema after every call. When the
session's output-schema cache is empty -- which it always is on a
short-lived session, the pattern stateless gateways and proxies use (one
ClientSession per call) -- that revalidation triggers a tools/list request
to discover the schema. This doubles round-trips on every call_tool and,
when the server behind the session is itself an aggregator, adds that
aggregator's slowest-backend tools/list latency to every single call, with
no way to opt out short of subclassing ClientSession.
Approach:
Add a validate_tool_results: bool = True constructor parameter to both
ClientSession and the high-level Client. When False, call_tool skips the
automatic validate_tool_result() call entirely -- on both the direct result
path and the SEP-2133 claimed-extension-result path -- so no tools/list is
issued and no RuntimeError is raised for output that doesn't match a schema
the caller never listed. The default stays True, so existing behavior,
including the tests that rely on a fresh session auto-discovering the
schema via its first validate_tool_result() call, is unchanged.
This is the constructor opt-out shape from the issue's three proposed
options (the alternative of skipping the refresh only on a wholly empty
cache would have changed default behavior on a fresh session, which
several existing tests -- test_validate_tool_result_passes_a_conforming_result
and friends in tests/client/test_session_promotions.py -- deliberately
lock in).
Validation:
- `uv run --frozen pytest tests/client/` -- 782 passed, 1 skipped, 1 xfailed
- `uv run --frozen ruff format --check .` / `ruff check .` -- clean
- `uv run --frozen pyright` on changed files -- 0 errors
- `./scripts/test` (full coverage-gated suite) -- 5970 passed, 100.00%
coverage, strict-no-cover clean
- `uv run --frozen pre-commit run --files <changed>` -- markdownlint and
ruff hooks pass; the pyright hook's only failure
(tests/transports/stdio/test_lifecycle.py:190, os.waitid) is confirmed
pre-existing on a clean main via git stash, unrelated to this change
- Base branch CI (`gh run list --branch main --event push`) is green as of
the last push
Report: #3513
Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
Assisted-by: claude-sonnet-5 (via Claude Code)
Copy file name to clipboardExpand all lines: docs/advanced/low-level-server.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -117,6 +117,8 @@ The `_meta` block is the server's identity stamp: the SDK adds it to every 2026-
117
117
118
118
The server never compares the two fields. This SDK's `Client` does: return `structured_content` that doesn't satisfy the `output_schema` you declared and `call_tool` raises a `RuntimeError` that starts with `Invalid structured content returned by tool search_books` and goes on to quote the `jsonschema` failure. Promising a schema is cheap; keeping it is on you. The whole ladder of return types and schemas is in **[Structured Output](../servers/structured-output.md)**.
119
119
120
+
That check costs a `tools/list` round-trip on a session that hasn't listed tools yet — the client needs a schema to validate against. A session built for exactly one `call_tool` (as stateless gateways and proxies often do) pays that cost every time; pass `validate_tool_results=False` to `Client`/`ClientSession` to skip the check entirely when the caller already validates elsewhere.
121
+
120
122
## The dialect is JSON Schema 2020-12
121
123
122
124
`input_schema` and `output_schema` are JSON Schema, and the [MCP specification](https://modelcontextprotocol.io/specification/latest/basic#json-schema-usage) fixes the dialect: a schema with no `$schema` key is **JSON Schema 2020-12**. The schemas `MCPServer` generates rely on that default (Pydantic writes 2020-12 and omits the key), and a hand-written dict is held to it too, so the full 2020-12 vocabulary is available:
0 commit comments