diff --git a/.changeset/spec-type-schema.md b/.changeset/spec-type-schema.md index 187ad768c..7bcc977e4 100644 --- a/.changeset/spec-type-schema.md +++ b/.changeset/spec-type-schema.md @@ -3,4 +3,4 @@ '@modelcontextprotocol/server': minor --- -Export `isSpecType` and `specTypeSchemas` records for runtime validation of any MCP spec type by name. `isSpecType.ContentBlock(value)` is a type predicate; `specTypeSchemas.ContentBlock` is a `StandardSchemaV1` validator. Guards are standalone functions, so `arr.filter(isSpecType.ContentBlock)` works. Also export the `SpecTypeName` and `SpecTypes` types. +Export `isSpecType` and `specTypeSchemas` records for runtime validation of any MCP spec type by name. `isSpecType.ContentBlock(value)` is a type predicate; `specTypeSchemas.ContentBlock` is a `StandardSchemaV1Sync` validator — `validate()` returns the result synchronously. Guards are standalone functions, so `arr.filter(isSpecType.ContentBlock)` works. Also export the `SpecTypeName`, `SpecTypes`, and `StandardSchemaV1Sync` types. diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index 9cff719bb..dbe6a4e9f 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -99,7 +99,7 @@ Notes: | `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) | All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names. **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) are no longer part of the public API — they are internal to the SDK. For runtime validation, use -`isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or `specTypeSchemas.TypeName` for the `StandardSchemaV1` validator object. The keys are typed as `SpecTypeName`, a literal union of all spec type names. +`isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or `specTypeSchemas.TypeName` for the `StandardSchemaV1Sync` validator object. The keys are typed as `SpecTypeName`, a literal union of all spec type names. ### Error class changes @@ -468,8 +468,8 @@ If a `*Schema` constant was used for **runtime validation** (not just as a `requ | -------------------------------------------------- | -------------------------------------------------------------------------------------- | | `CallToolResultSchema.safeParse(value).success` | `isSpecType.CallToolResult(value)` | | `Schema.safeParse(value).success` | `isSpecType.(value)` | -| `Schema.parse(value)` | `await specTypeSchemas.['~standard'].validate(value)` (returns a `Result`, not the value) | -| Passing `Schema` as a validator argument | `specTypeSchemas.` (a `StandardSchemaV1`) | +| `Schema.parse(value)` | `specTypeSchemas.['~standard'].validate(value)` (returns a `Result` synchronously, not the value) | +| Passing `Schema` as a validator argument | `specTypeSchemas.` (a `StandardSchemaV1Sync`) | `isCallToolResult(value)` still works, but `isSpecType` covers every spec type by name. diff --git a/docs/migration.md b/docs/migration.md index fecf18599..cd3da6dcd 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -505,12 +505,12 @@ if (isSpecType.CallToolResult(value)) { } const blocks = mixed.filter(isSpecType.ContentBlock); -// v2: or get the StandardSchemaV1 validator object directly +// v2: or get the StandardSchemaV1Sync validator object directly import { specTypeSchemas } from '@modelcontextprotocol/client'; -const result = await specTypeSchemas.CallToolResult['~standard'].validate(value); +const result = specTypeSchemas.CallToolResult['~standard'].validate(value); ``` -`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1`, which composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works. +`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1Sync` — `validate()` returns the result synchronously, so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works. ### Client list methods return empty results for missing capabilities diff --git a/packages/core/src/exports/public/index.ts b/packages/core/src/exports/public/index.ts index 5c1689ca6..e305f32a4 100644 --- a/packages/core/src/exports/public/index.ts +++ b/packages/core/src/exports/public/index.ts @@ -140,7 +140,7 @@ export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/ // Validator types and classes export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema.js'; export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema.js'; -export type { StandardSchemaV1, StandardSchemaWithJSON } from '../../util/standardSchema.js'; +export type { StandardSchemaV1, StandardSchemaV1Sync, StandardSchemaWithJSON } from '../../util/standardSchema.js'; export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js'; export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js'; // fromJsonSchema is intentionally NOT exported here — the server and client packages diff --git a/packages/core/src/types/specTypeSchema.examples.ts b/packages/core/src/types/specTypeSchema.examples.ts index 05b706748..8e991d4f9 100644 --- a/packages/core/src/types/specTypeSchema.examples.ts +++ b/packages/core/src/types/specTypeSchema.examples.ts @@ -13,9 +13,9 @@ declare const untrusted: unknown; declare const value: unknown; declare const mixed: unknown[]; -async function specTypeSchemas_basicUsage() { +function specTypeSchemas_basicUsage() { //#region specTypeSchemas_basicUsage - const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted); + const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted); if (result.issues === undefined) { // result.value is CallToolResult } diff --git a/packages/core/src/types/specTypeSchema.ts b/packages/core/src/types/specTypeSchema.ts index cde3555d0..477d61a55 100644 --- a/packages/core/src/types/specTypeSchema.ts +++ b/packages/core/src/types/specTypeSchema.ts @@ -13,7 +13,7 @@ import { OpenIdProviderDiscoveryMetadataSchema, OpenIdProviderMetadataSchema } from '../shared/auth.js'; -import type { StandardSchemaV1 } from '../util/standardSchema.js'; +import type { StandardSchemaV1, StandardSchemaV1Sync } from '../util/standardSchema.js'; import * as schemas from './schemas.js'; /** @@ -235,7 +235,7 @@ type SpecTypeInputs = { [K in SchemaKey as StripSchemaSuffix]: SchemaFor extends z.ZodType ? z.input> : never; }; -type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1 }; +type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1Sync }; type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] }; const _specTypeSchemas: Record = {}; @@ -265,7 +265,7 @@ for (const [key, schema] of Object.entries(authSchemas)) { * * @example * ```ts source="./specTypeSchema.examples.ts#specTypeSchemas_basicUsage" - * const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted); + * const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted); * if (result.issues === undefined) { * // result.value is CallToolResult * } diff --git a/packages/core/src/util/standardSchema.ts b/packages/core/src/util/standardSchema.ts index ee1a63067..b938885de 100644 --- a/packages/core/src/util/standardSchema.ts +++ b/packages/core/src/util/standardSchema.ts @@ -114,6 +114,28 @@ export namespace StandardSchemaWithJSON { export type InferOutput = StandardTypedV1.InferOutput; } +/** + * Narrowing of {@linkcode StandardSchemaV1} whose `validate` is guaranteed synchronous. + * + * The Zod schemas backing `specTypeSchemas` contain no async refinements or transforms, + * so every entry satisfies this interface. Consumers can call `validate()` and access + * `.issues` / `.value` on the result without `await`. + * + * `StandardSchemaV1Sync` is assignable to `StandardSchemaV1` — it is a strict subtype. + */ +export interface StandardSchemaV1Sync extends StandardSchemaV1 { + readonly '~standard': StandardSchemaV1Sync.Props; +} + +export namespace StandardSchemaV1Sync { + export interface Props extends StandardSchemaV1.Props { + readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => StandardSchemaV1.Result; + } + + export type InferInput = StandardTypedV1.InferInput; + export type InferOutput = StandardTypedV1.InferOutput; +} + // Type guards export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 { diff --git a/packages/core/test/types/specTypeSchema.test.ts b/packages/core/test/types/specTypeSchema.test.ts index be8c41922..198e104f9 100644 --- a/packages/core/test/types/specTypeSchema.test.ts +++ b/packages/core/test/types/specTypeSchema.test.ts @@ -16,14 +16,14 @@ import type { } from '../../src/types/types.js'; describe('specTypeSchemas', () => { - it('returns a StandardSchemaV1 validator that accepts valid values', () => { + it('returns a StandardSchemaV1Sync validator that accepts valid values', () => { const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x', version: '1.0.0' }); - expect((result as { issues?: unknown }).issues).toBeUndefined(); + expect(result.issues).toBeUndefined(); }); it('returns a validator that rejects invalid values with issues', () => { const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x' }); - expect((result as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0); + expect(result.issues?.length).toBeGreaterThan(0); }); it('rejects unknown names at compile time and is undefined at runtime', () => { @@ -33,14 +33,14 @@ describe('specTypeSchemas', () => { it('covers JSON-RPC envelope types', () => { const ok = specTypeSchemas.JSONRPCRequest['~standard'].validate({ jsonrpc: '2.0', id: 1, method: 'ping' }); - expect((ok as { issues?: unknown }).issues).toBeUndefined(); + expect(ok.issues).toBeUndefined(); }); it('covers OAuth types from shared/auth.ts', () => { const ok = specTypeSchemas.OAuthTokens['~standard'].validate({ access_token: 'x', token_type: 'Bearer' }); - expect((ok as { issues?: unknown }).issues).toBeUndefined(); + expect(ok.issues).toBeUndefined(); const bad = specTypeSchemas.OAuthTokens['~standard'].validate({ token_type: 'Bearer' }); - expect((bad as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0); + expect(bad.issues?.length).toBeGreaterThan(0); }); });