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
{{ message }}
Repository navigation
Client: allow an async JsonSchemaValidator for tool output validation #2971
The jsonSchemaValidator option of @modelcontextprotocol/client (2.2.0) is synchronous: getValidator(schema) returns (input) => {valid, data, errorMessage}, and callTool calls it right after await this.request(...). A server-side process that must bound validation time (an untrusted remote schema or instance) has to run ajv in a worker thread. With a sync interface, that means blocking the event loop on Atomics.wait until the worker answers. One slow schema then stalls every in-flight request in the process.
Request: accept a validator whose function may return Promise<{valid, data, errorMessage}> (and an async getValidator), and await it in callTool before the result is returned. The sync form keeps working unchanged.
Workaround we use today: a Client subclass whose callTool runs super.callTool inside an AsyncLocalStorage slot. The sync hook records an async validation there, and the override awaits it and throws the same ProtocolError. That works, but it depends on the call site staying inside callTool.
I traced the current v2 client implementation for this request. The async boundary is wider than the final callTool validation line:
packages/core-internal/src/validators/types.ts currently requires synchronous getValidator(schema)and a synchronous returned check function.
Client._compileOutputValidator() currently catches synchronous compile throws and returns a per-tool error variant. A rejecting async compiler would not be captured in the same way.
ClientResponseCache.outputValidator() takes a synchronous compile callback and memoizes its result by tools/list stamp. Client.callTool() expects a callable compiled validator and checks validationResult.valid without awaiting.
A global widening of the shared interface also affects fromJsonSchema and its currently synchronous consumer contract; it may be safer to isolate async support to client tool-output validation rather than silently changing server/Standard Schema behavior.
There is also an overlapping open PR #2998 that makes compilation lazy per called tool. Async support needs to compose with its cache invalidation semantics (list_changed, reconnect, caller-supplied toolDefinition) and avoid treating a pending or rejected Promise as a ready validator.
A bounded acceptance set seems useful: (1) sync providers remain unchanged; (2) async compile and async check both work; (3) rejected compile fails before tools/call with the existing typed error; (4) rejected/mismatched async validation preserves typed errors; (5) concurrent calls don't block; (6) refreshed catalogs cannot reinstall validators compiled for an old stamp.
Design question for maintainers: Would you prefer a client-specific async output-validator option/adapter, keeping the shared jsonSchemaValidator contract synchronous, or widening the shared interface with explicit sync/async treatment across all consumers? I have a small private source-shaped contract reproducing the naive-global-widening failure (3/3 demonstrations), not an SDK integration test or an implementation PR. Happy to help with a narrowly scoped fix once the preferred API boundary and #2998 integration order are clear.
Implementation update: I opened PR #3011 for this issue: #3011
It keeps the shared jsonSchemaValidator/fromJsonSchema contract synchronous, while allowing the Client option to await async validator compilation and async result checks. It preserves the typed compile-failure path and explicit toolDefinition isolation, with focused regression tests. The current PR head 71ee58c has all four upstream GitHub Actions workflows green (build/typecheck/lint, Node 20/22/24, e2e, conformance, runtimes, examples).
The PR is ready for review. I flagged the interaction with #2998's lazy/stamp-keyed cache as the main integration consideration; feedback on that boundary is welcome.
Activity
karaaslanz commentedon Oct 11, 2026
I traced the current v2 client implementation for this request. The async boundary is wider than the final
callToolvalidation line:packages/core-internal/src/validators/types.tscurrently requires synchronousgetValidator(schema)and a synchronous returned check function.Client._compileOutputValidator()currently catches synchronous compile throws and returns a per-tool error variant. A rejecting async compiler would not be captured in the same way.ClientResponseCache.outputValidator()takes a synchronous compile callback and memoizes its result bytools/liststamp.Client.callTool()expects a callable compiled validator and checksvalidationResult.validwithout awaiting.fromJsonSchemaand its currently synchronous consumer contract; it may be safer to isolate async support to client tool-output validation rather than silently changing server/Standard Schema behavior.There is also an overlapping open PR #2998 that makes compilation lazy per called tool. Async support needs to compose with its cache invalidation semantics (
list_changed, reconnect, caller-suppliedtoolDefinition) and avoid treating a pending or rejected Promise as a ready validator.A bounded acceptance set seems useful: (1) sync providers remain unchanged; (2) async compile and async check both work; (3) rejected compile fails before
tools/callwith the existing typed error; (4) rejected/mismatched async validation preserves typed errors; (5) concurrent calls don't block; (6) refreshed catalogs cannot reinstall validators compiled for an old stamp.Design question for maintainers: Would you prefer a client-specific async output-validator option/adapter, keeping the shared
jsonSchemaValidatorcontract synchronous, or widening the shared interface with explicit sync/async treatment across all consumers? I have a small private source-shaped contract reproducing the naive-global-widening failure (3/3 demonstrations), not an SDK integration test or an implementation PR. Happy to help with a narrowly scoped fix once the preferred API boundary and #2998 integration order are clear.karaaslanz commentedon Oct 13, 2026
Implementation update: I opened PR #3011 for this issue: #3011
It keeps the shared jsonSchemaValidator/fromJsonSchema contract synchronous, while allowing the Client option to await async validator compilation and async result checks. It preserves the typed compile-failure path and explicit toolDefinition isolation, with focused regression tests. The current PR head 71ee58c has all four upstream GitHub Actions workflows green (build/typecheck/lint, Node 20/22/24, e2e, conformance, runtimes, examples).
The PR is ready for review. I flagged the interaction with #2998's lazy/stamp-keyed cache as the main integration consideration; feedback on that boundary is welcome.