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

This commit is contained in:
Konstantin Konstantinov 2026-05-15 16:51:48 +03:00 • committed by GitHub
commit 22595b9685
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 41 additions and 19 deletions

View file

@ -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<ContentBlock>` 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<ContentBlock>` validator — `validate()` returns the result synchronously. Guards are standalone functions, so `arr.filter(isSpecType.ContentBlock)` works. Also export the `SpecTypeName`, `SpecTypes`, and `StandardSchemaV1Sync` types.

View file

@ -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)` |
| `<TypeName>Schema.safeParse(value).success` | `isSpecType.<TypeName>(value)` |
| `<TypeName>Schema.parse(value)` | `await specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result`, not the value) |
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1<In, Out>`) |
| `<TypeName>Schema.parse(value)` | `specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result` synchronously, not the value) |
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1Sync<In, Out>`) |
`isCallToolResult(value)` still works, but `isSpecType` covers every spec type by name.

View file

@ -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<In, Out>`, 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<In, Out>` — `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

View file

@ -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

View file

@ -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
}

View file

@ -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<K>]: SchemaFor<K> extends z.ZodType ? z.input<SchemaFor<K>> : never;
};
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]> };
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1Sync<SpecTypeInputs[K], SpecTypes[K]> };
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };
const _specTypeSchemas: Record<string, StandardSchemaV1> = {};
@ -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
* }

View file

@ -114,6 +114,28 @@ export namespace StandardSchemaWithJSON {
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}
/**
* 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<Input = unknown, Output = Input> extends StandardSchemaV1<Input, Output> {
readonly '~standard': StandardSchemaV1Sync.Props<Input, Output>;
}
export namespace StandardSchemaV1Sync {
export interface Props<Input = unknown, Output = Input> extends StandardSchemaV1.Props<Input, Output> {
readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => StandardSchemaV1.Result<Output>;
}
export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}
// Type guards
export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 {

View file

@ -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);
});
});