Skip to content

v2 backwards compat: specTypeSchema exported - synchronous StandardSchemaV1 - #2047

Merged
KKonstantinov merged 4 commits into
mainfrom
feature/fix-sync-validate
May 15, 2026
Merged

v2 backwards compat: specTypeSchema exported - synchronous StandardSchemaV1#2047
KKonstantinov merged 4 commits into
mainfrom
feature/fix-sync-validate

Conversation

@KKonstantinov

Copy link
Copy Markdown
Contributor

Add StandardSchemaV1Sync type so specTypeSchemas entries expose synchronous validate

Motivation and Context

specTypeSchemas exposes every MCP spec schema as a StandardSchemaV1. The StandardSchemaV1 interface defines validate as returning Result<Output> | Promise<Result<Output>>, because the Standard Schema spec supports async validation libraries. However, every entry in specTypeSchemas is backed by Zod, which always validates synchronously -- the Promise variant is never returned at runtime.

This causes a real problem for consumers: calling specTypeSchemas.X['~standard'].validate(v) and then accessing .issues or .value on the result produces a type error, because TypeScript thinks the result might be a Promise.

const result = specTypeSchemas.CreateMessageResult['~standard'].validate(data);
// TS error: Property 'issues' does not exist on type 'Result<...> | Promise<Result<...>>'
if (result.issues !== undefined) { ... }

How Has This Been Tested?

  • pnpm typecheck:all -- all packages typecheck cleanly
  • pnpm test:all -- all 1532 tests pass across all packages

Breaking Changes

No. StandardSchemaV1Sync extends StandardSchemaV1, so specTypeSchemas entries remain assignable anywhere a StandardSchemaV1 is expected. The change only narrows the return type of validate from Result | Promise<Result> to Result.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Three files changed (types only, no runtime changes):

  • packages/core/src/util/standardSchema.ts -- defines StandardSchemaV1Sync, which extends StandardSchemaV1 but narrows validate to return StandardSchemaV1.Result<Output> (no Promise variant)
  • packages/core/src/types/specTypeSchema.ts -- SchemaRecord now uses StandardSchemaV1Sync instead of StandardSchemaV1
  • packages/core/src/exports/public/index.ts -- exports StandardSchemaV1Sync so consumers can reference the type

APIs that accept user-provided schemas (e.g. McpServer.tool()) still use StandardSchemaV1 / StandardSchemaWithJSON, so async validation libraries remain fully supported.

@KKonstantinov
KKonstantinov requested a review from a team as a code owner May 11, 2026 15:22
@changeset-bot

changeset-bot Bot commented May 11, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: db32ed5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/client Minor
@modelcontextprotocol/server Minor
@modelcontextprotocol/express Major
@modelcontextprotocol/fastify Major
@modelcontextprotocol/hono Major
@modelcontextprotocol/node Major

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented May 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2047

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2047

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2047

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2047

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2047

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2047

commit: db32ed5

Comment thread packages/core/src/types/specTypeSchema.ts
Comment thread packages/core/src/exports/public/index.ts
…wing

  Replace intersection-based StandardSchemaV1Sync with interface-extends
  approach so TypeScript properly narrows validate() return type.
  Drop async/await from examples and migration docs to match the
  synchronous API this PR introduces.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for addressing the earlier feedback — LGTM. The two remaining inline notes are non-blocking polish (vestigial test casts and the InferInput/InferOutput namespace aliases); fine to take or leave.

Extended reasoning...

Overview

This PR adds a StandardSchemaV1Sync interface (a strict subtype of StandardSchemaV1 whose validate is guaranteed to return synchronously) and retypes specTypeSchemas to use it. After my first-round review on commit 34bccc9, commit 5bdaf6c updated the changeset prose, the .examples.ts snippet, the synced JSDoc @example, and both migration docs to drop await and reference StandardSchemaV1Sync — both prior nits are fully addressed.

Security risks

None. This is a type-level narrowing with zero runtime change: _specTypeSchemas is still populated with the same Zod schemas and frozen the same way. No auth, transport, parsing, or wire behavior is touched.

Level of scrutiny

Low–medium. The only question is soundness of the narrowing, and it holds: StandardSchemaV1Sync extends StandardSchemaV1 and only narrows the return type of validate (covariant position), so every specTypeSchemas entry remains assignable wherever StandardSchemaV1 is expected. The runtime claim is also valid — every entry is a plain Zod spec schema with no async refinements, so ~standard.validate always returns synchronously and the as SchemaRecord cast is sound. The new public type export has a concrete in-tree consumer (SchemaRecord) and is now documented in the changeset and migration guides, satisfying the repo's "every new export is intentional" bar.

Other factors

The two new inline nits (leftover as { issues?: ... } casts in specTypeSchema.test.ts, and the missing InferInput/InferOutput aliases in the StandardSchemaV1Sync namespace) are consistency/cleanup items that don't affect correctness, the published API shape, or backwards compatibility. They can be folded in here or taken as a follow-up.

Comment thread packages/core/src/types/specTypeSchema.ts
Comment thread packages/core/src/util/standardSchema.ts
…rInput/InferOutput aliases

  - Remove unnecessary `as { issues?: ... }` casts in specTypeSchema.test.ts
    that were workarounds for the old async validate return type. The bare
    `result.issues` access now serves as compile-time regression coverage
    for the StandardSchemaV1Sync narrowing.
  - Add InferInput/InferOutput convenience aliases to the StandardSchemaV1Sync
    namespace, matching the pattern of all sibling Standard* namespaces.
@mattzcarey

Copy link
Copy Markdown
Contributor

I'm a bit confused. If we dont await async validators wont we end up with issues. can't we just await all even non async validators.

Comment thread packages/core/src/util/standardSchema.ts Outdated
@KKonstantinov

KKonstantinov commented May 15, 2026

Copy link
Copy Markdown
Contributor Author

I'm a bit confused. If we dont await async validators wont we end up with issues. can't we just await all even non async validators.

Yes, we can await all, this is correct.

However, imagine the migration path.

Someone is having a non-async method. All of a sudden they have to turn it into an async one, which could have secondary and third-order effects to their whole codebase.

Since we know that all of our existing schemas are sync, we are saying - "all of the schema exported by @modelcontextprotocol" are guaranteed to be sync StandardSchemas.

It will be important though to not add any async stuff like .refine() methods on our schemas, but I am not sure that we have such a use case.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — thanks for the JSDoc tightening in db32ed5; all earlier nits are addressed and the type narrowing looks correct.

Extended reasoning...

Overview

This PR introduces StandardSchemaV1Sync, a strict subtype of StandardSchemaV1 whose validate returns Result<Output> synchronously (no | Promise<...> branch), and retypes specTypeSchemas entries to use it. Touches: util/standardSchema.ts (new interface + namespace following the established 4-sibling pattern, with InferInput/InferOutput aliases), types/specTypeSchema.ts (SchemaRecord narrowed), exports/public/index.ts (new type export, re-exported via export * in client/server barrels), plus docs/examples/tests/changeset updated to drop await and reference the sync type.

Security risks

None. This is a pure type-level change with zero runtime delta — no new code paths, no input handling, no auth/transport surface.

Level of scrutiny

Low–medium. The only substantive question is the soundness of the pre-existing as SchemaRecord cast now that it narrows to a sync-only signature. I verified in earlier review rounds that none of the backing schemas in schemas.ts / shared/auth.ts use async refinements/transforms, and the JSDoc now documents that invariant explicitly (db32ed5). The new export is minimal, follows the file's established Standard* interface+namespace pattern, and is needed so consumers can name the type of specTypeSchemas entries. StandardSchemaV1Sync extends StandardSchemaV1, so this is non-breaking.

Other factors

I raised five nits across three review rounds (stale docs/examples, stale changeset prose, vestigial test casts, missing InferInput/InferOutput aliases, over-broad JSDoc premise) — all were addressed in 5bdaf6c, 348d3a2, and db32ed5. The test file now accesses .issues without casts, giving type-level regression coverage for the sync guarantee. The feature being amended (specTypeSchemas) is itself unreleased (pre-mode changeset not yet consumed), so editing its changeset in place was the right call. A reviewer question about "why not just await everywhere" was answered with the migration-path rationale (avoiding forced async coloring of consumer call stacks). No bugs found by the bug-hunting system.

@KKonstantinov
KKonstantinov merged commit 22595b9 into main May 15, 2026
17 checks passed
@KKonstantinov
KKonstantinov deleted the feature/fix-sync-validate branch May 15, 2026 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants