Skip to content

Commit 22595b9

Browse files
v2 backwards compat: specTypeSchema exported - synchronous StandardSchemaV1 (#2047)
1 parent 2c0c481 commit 22595b9

8 files changed

Lines changed: 41 additions & 19 deletions

File tree

.changeset/spec-type-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
'@modelcontextprotocol/server': minor
44
---
55

6-
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.
6+
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.

docs/migration-SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Notes:
9999
| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) |
100100

101101
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
102-
`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.
102+
`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.
103103

104104
### Error class changes
105105

@@ -468,8 +468,8 @@ If a `*Schema` constant was used for **runtime validation** (not just as a `requ
468468
| -------------------------------------------------- | -------------------------------------------------------------------------------------- |
469469
| `CallToolResultSchema.safeParse(value).success` | `isSpecType.CallToolResult(value)` |
470470
| `<TypeName>Schema.safeParse(value).success` | `isSpecType.<TypeName>(value)` |
471-
| `<TypeName>Schema.parse(value)` | `await specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result`, not the value) |
472-
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1<In, Out>`) |
471+
| `<TypeName>Schema.parse(value)` | `specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result` synchronously, not the value) |
472+
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1Sync<In, Out>`) |
473473

474474
`isCallToolResult(value)` still works, but `isSpecType` covers every spec type by name.
475475

docs/migration.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -505,12 +505,12 @@ if (isSpecType.CallToolResult(value)) {
505505
}
506506
const blocks = mixed.filter(isSpecType.ContentBlock);
507507

508-
// v2: or get the StandardSchemaV1 validator object directly
508+
// v2: or get the StandardSchemaV1Sync validator object directly
509509
import { specTypeSchemas } from '@modelcontextprotocol/client';
510-
const result = await specTypeSchemas.CallToolResult['~standard'].validate(value);
510+
const result = specTypeSchemas.CallToolResult['~standard'].validate(value);
511511
```
512512

513-
`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.
513+
`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.
514514

515515
### Client list methods return empty results for missing capabilities
516516

packages/core/src/exports/public/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/
140140
// Validator types and classes
141141
export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema.js';
142142
export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema.js';
143-
export type { StandardSchemaV1, StandardSchemaWithJSON } from '../../util/standardSchema.js';
143+
export type { StandardSchemaV1, StandardSchemaV1Sync, StandardSchemaWithJSON } from '../../util/standardSchema.js';
144144
export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js';
145145
export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js';
146146
// fromJsonSchema is intentionally NOT exported here — the server and client packages

packages/core/src/types/specTypeSchema.examples.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ declare const untrusted: unknown;
1313
declare const value: unknown;
1414
declare const mixed: unknown[];
1515

16-
async function specTypeSchemas_basicUsage() {
16+
function specTypeSchemas_basicUsage() {
1717
//#region specTypeSchemas_basicUsage
18-
const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
18+
const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
1919
if (result.issues === undefined) {
2020
// result.value is CallToolResult
2121
}

packages/core/src/types/specTypeSchema.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
OpenIdProviderDiscoveryMetadataSchema,
1414
OpenIdProviderMetadataSchema
1515
} from '../shared/auth.js';
16-
import type { StandardSchemaV1 } from '../util/standardSchema.js';
16+
import type { StandardSchemaV1, StandardSchemaV1Sync } from '../util/standardSchema.js';
1717
import * as schemas from './schemas.js';
1818

1919
/**
@@ -235,7 +235,7 @@ type SpecTypeInputs = {
235235
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaFor<K> extends z.ZodType ? z.input<SchemaFor<K>> : never;
236236
};
237237

238-
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]> };
238+
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1Sync<SpecTypeInputs[K], SpecTypes[K]> };
239239
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };
240240

241241
const _specTypeSchemas: Record<string, StandardSchemaV1> = {};
@@ -265,7 +265,7 @@ for (const [key, schema] of Object.entries(authSchemas)) {
265265
*
266266
* @example
267267
* ```ts source="./specTypeSchema.examples.ts#specTypeSchemas_basicUsage"
268-
* const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
268+
* const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
269269
* if (result.issues === undefined) {
270270
* // result.value is CallToolResult
271271
* }

packages/core/src/util/standardSchema.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,28 @@ export namespace StandardSchemaWithJSON {
114114
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
115115
}
116116

117+
/**
118+
* Narrowing of {@linkcode StandardSchemaV1} whose `validate` is guaranteed synchronous.
119+
*
120+
* The Zod schemas backing `specTypeSchemas` contain no async refinements or transforms,
121+
* so every entry satisfies this interface. Consumers can call `validate()` and access
122+
* `.issues` / `.value` on the result without `await`.
123+
*
124+
* `StandardSchemaV1Sync` is assignable to `StandardSchemaV1` — it is a strict subtype.
125+
*/
126+
export interface StandardSchemaV1Sync<Input = unknown, Output = Input> extends StandardSchemaV1<Input, Output> {
127+
readonly '~standard': StandardSchemaV1Sync.Props<Input, Output>;
128+
}
129+
130+
export namespace StandardSchemaV1Sync {
131+
export interface Props<Input = unknown, Output = Input> extends StandardSchemaV1.Props<Input, Output> {
132+
readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => StandardSchemaV1.Result<Output>;
133+
}
134+
135+
export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
136+
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
137+
}
138+
117139
// Type guards
118140

119141
export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 {

packages/core/test/types/specTypeSchema.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ import type {
1616
} from '../../src/types/types.js';
1717

1818
describe('specTypeSchemas', () => {
19-
it('returns a StandardSchemaV1 validator that accepts valid values', () => {
19+
it('returns a StandardSchemaV1Sync validator that accepts valid values', () => {
2020
const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x', version: '1.0.0' });
21-
expect((result as { issues?: unknown }).issues).toBeUndefined();
21+
expect(result.issues).toBeUndefined();
2222
});
2323

2424
it('returns a validator that rejects invalid values with issues', () => {
2525
const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x' });
26-
expect((result as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0);
26+
expect(result.issues?.length).toBeGreaterThan(0);
2727
});
2828

2929
it('rejects unknown names at compile time and is undefined at runtime', () => {
@@ -33,14 +33,14 @@ describe('specTypeSchemas', () => {
3333

3434
it('covers JSON-RPC envelope types', () => {
3535
const ok = specTypeSchemas.JSONRPCRequest['~standard'].validate({ jsonrpc: '2.0', id: 1, method: 'ping' });
36-
expect((ok as { issues?: unknown }).issues).toBeUndefined();
36+
expect(ok.issues).toBeUndefined();
3737
});
3838

3939
it('covers OAuth types from shared/auth.ts', () => {
4040
const ok = specTypeSchemas.OAuthTokens['~standard'].validate({ access_token: 'x', token_type: 'Bearer' });
41-
expect((ok as { issues?: unknown }).issues).toBeUndefined();
41+
expect(ok.issues).toBeUndefined();
4242
const bad = specTypeSchemas.OAuthTokens['~standard'].validate({ token_type: 'Bearer' });
43-
expect((bad as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0);
43+
expect(bad.issues?.length).toBeGreaterThan(0);
4444
});
4545
});
4646

0 commit comments

Comments
 (0)