Skip to content

Client: allow an async JsonSchemaValidator for tool output validation #2971

Open

Description

@Alexander-Ollman

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.

Activity

  1. added
    v1Issues / PRs related to v1.x
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    on Oct 9, 2026
  2. karaaslanz commented on Oct 11, 2026

    @karaaslanz

    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.

  3. karaaslanz commented on Oct 13, 2026

    @karaaslanz

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v1Issues / PRs related to v1.xv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions