Skip to content

Commit 8f3d23b

Browse files
refactor(specTypeSchema): switch isSpecType/specTypeSchema to string-arg function form
Replaces the Record-of-predicates exports with two generic functions: isSpecType('TypeName', value) and specTypeSchema('TypeName'). Drops the register() helper, the per-key closure creation (161 closures at module init), the SchemaRecord/GuardRecord types, and both Object.freeze(...) as unknown as casts. The single remaining cast is a narrow Record<SpecTypeName, z.ZodType> at construction so indexing by SpecTypeName is non-undefined under noUncheckedIndexedAccess. Trade-off: arr.filter needs an arrow wrapper (mixed.filter(v => isSpecType('ContentBlock', v))). TypeScript's inferred type predicates still narrow the result to ContentBlock[].
1 parent f6b744e commit 8f3d23b

7 files changed

Lines changed: 88 additions & 93 deletions

File tree

.changeset/spec-type-schema.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
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 `StandardSchemaV1`, `SpecTypeName`, and `SpecTypes` types.
6+
Export `isSpecType` and `specTypeSchema` for runtime validation of any MCP spec type by name. `isSpecType('ContentBlock', value)` is a type predicate; `specTypeSchema('ContentBlock')` returns a `StandardSchemaV1<ContentBlock>` validator. Also export the `StandardSchemaV1`,
7+
`SpecTypeName`, and `SpecTypes` types.

docs/migration-SKILL.md

Lines changed: 8 additions & 8 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 `specTypeSchema('TypeName')` for the `StandardSchemaV1` validator object. The first argument is typed as `SpecTypeName`, a literal union of all spec type names.
103103

104104
### Error class changes
105105

@@ -437,14 +437,14 @@ const tool = await client.callTool({ name: 'my-tool', arguments: {} });
437437

438438
Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResultSchema`, `ElicitResultSchema`, `CreateMessageResultSchema`, etc., when they were only used in `request()`/`send()`/`callTool()` calls.
439439

440-
If a `*Schema` constant was used for **runtime validation** (not just as a `request()` argument), replace with `isSpecType` / `specTypeSchemas`:
440+
If a `*Schema` constant was used for **runtime validation** (not just as a `request()` argument), replace with `isSpecType` / `specTypeSchema`:
441441

442-
| v1 pattern | v2 replacement |
443-
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
444-
| `CallToolResultSchema.safeParse(value).success` | `isSpecType.CallToolResult(value)` |
445-
| `<TypeName>Schema.safeParse(value).success` | `isSpecType.<TypeName>(value)` |
446-
| `<TypeName>Schema.parse(value)` | `await specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result`, not the value) |
447-
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1<In, Out>`) |
442+
| v1 pattern | v2 replacement |
443+
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
444+
| `CallToolResultSchema.safeParse(value).success` | `isSpecType('CallToolResult', value)` |
445+
| `<TypeName>Schema.safeParse(value).success` | `isSpecType('<TypeName>', value)` |
446+
| `<TypeName>Schema.parse(value)` | `await specTypeSchema('<TypeName>')['~standard'].validate(value)` (returns a `Result`, not the value) |
447+
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchema('<TypeName>')` (a `StandardSchemaV1<In, Out>`) |
448448

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

docs/migration.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30
137137

138138
Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are now first-class in `@modelcontextprotocol/express`.
139139

140-
Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. See the [examples](../examples/server/src/) for a working demo with `better-auth`.
140+
Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. See the [examples](../examples/server/src/) for
141+
a working demo with `better-auth`.
141142

142143
Note: `AuthInfo` has moved from `server/auth/types.ts` to the core types and is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`.
143144

@@ -445,7 +446,7 @@ const result = await client.callTool({ name: 'my-tool', arguments: {} });
445446

446447
The return type is now inferred from the method name via `ResultTypeMap`. For example, `client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult | CreateTaskResult>`.
447448

448-
If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), use `isSpecType` or `specTypeSchemas`:
449+
If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), use `isSpecType` or `specTypeSchema`:
449450

450451
```typescript
451452
// v1: runtime validation with Zod schema
@@ -454,19 +455,20 @@ if (CallToolResultSchema.safeParse(value).success) {
454455
/* ... */
455456
}
456457

457-
// v2: keyed type predicate
458+
// v2: type predicate by name
458459
import { isSpecType } from '@modelcontextprotocol/client';
459-
if (isSpecType.CallToolResult(value)) {
460+
if (isSpecType('CallToolResult', value)) {
460461
/* ... */
461462
}
462-
const blocks = mixed.filter(isSpecType.ContentBlock);
463+
const blocks = mixed.filter(v => isSpecType('ContentBlock', v));
463464

464465
// v2: or get the StandardSchemaV1 validator object directly
465-
import { specTypeSchemas } from '@modelcontextprotocol/client';
466-
const result = await specTypeSchemas.CallToolResult['~standard'].validate(value);
466+
import { specTypeSchema } from '@modelcontextprotocol/client';
467+
const result = await specTypeSchema('CallToolResult')['~standard'].validate(value);
467468
```
468469

469-
`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.
470+
The first argument to `isSpecType` and `specTypeSchema` is a `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchema(name)` returns a `StandardSchemaV1<In, Out>`, which composes with any
471+
Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works.
470472

471473
### Client list methods return empty results for missing capabilities
472474

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/
138138

139139
// Validator types and classes
140140
export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema.js';
141-
export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema.js';
141+
export { isSpecType, specTypeSchema } from '../../types/specTypeSchema.js';
142142
export type { StandardSchemaV1, StandardSchemaWithJSON } from '../../util/standardSchema.js';
143143
export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js';
144144
export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js';

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,32 @@
77
* @module
88
*/
99

10-
import { isSpecType, specTypeSchemas } from './specTypeSchema.js';
10+
import { isSpecType, specTypeSchema } from './specTypeSchema.js';
1111

1212
declare const untrusted: unknown;
1313
declare const value: unknown;
1414
declare const mixed: unknown[];
1515

16-
async function specTypeSchemas_basicUsage() {
17-
//#region specTypeSchemas_basicUsage
18-
const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
16+
async function specTypeSchema_basicUsage() {
17+
//#region specTypeSchema_basicUsage
18+
const result = await specTypeSchema('CallToolResult')['~standard'].validate(untrusted);
1919
if (result.issues === undefined) {
2020
// result.value is CallToolResult
2121
}
22-
//#endregion specTypeSchemas_basicUsage
22+
//#endregion specTypeSchema_basicUsage
2323
void result;
2424
}
2525

2626
function isSpecType_basicUsage() {
27-
/* eslint-disable unicorn/no-array-callback-reference -- showcasing the guard-as-callback pattern */
2827
//#region isSpecType_basicUsage
29-
if (isSpecType.ContentBlock(value)) {
28+
if (isSpecType('ContentBlock', value)) {
3029
// value is ContentBlock
3130
}
3231

33-
const blocks = mixed.filter(isSpecType.ContentBlock);
32+
const blocks = mixed.filter(v => isSpecType('ContentBlock', v));
3433
//#endregion isSpecType_basicUsage
35-
/* eslint-enable unicorn/no-array-callback-reference */
3634
void blocks;
3735
}
3836

39-
void specTypeSchemas_basicUsage;
37+
void specTypeSchema_basicUsage;
4038
void isSpecType_basicUsage;

packages/core/src/types/specTypeSchema.ts

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -235,62 +235,59 @@ 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]> };
239-
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };
240-
241-
const _specTypeSchemas: Record<string, z.ZodTypeAny> = {};
242-
const _isSpecType: Record<string, (value: unknown) => boolean> = {};
243-
function register(key: string, schema: z.ZodTypeAny): void {
244-
const name = key.slice(0, -'Schema'.length);
245-
_specTypeSchemas[name] = schema;
246-
_isSpecType[name] = (v: unknown) => schema.safeParse(v).success;
247-
}
238+
// Populated for every SpecTypeName by the loops below; the cast lets `allSchemas[name]` be
239+
// non-undefined under `noUncheckedIndexedAccess` when `name` is a SpecTypeName.
240+
const allSchemas = {} as Record<SpecTypeName, z.ZodType>;
248241
for (const key of SPEC_SCHEMA_KEYS) {
249242
// eslint-disable-next-line import/namespace -- key is constrained to keyof typeof schemas via the satisfies clause above
250-
register(key, schemas[key]);
243+
allSchemas[key.slice(0, -'Schema'.length) as SpecTypeName] = schemas[key];
251244
}
252245
for (const [key, schema] of Object.entries(authSchemas)) {
253-
register(key, schema);
246+
allSchemas[key.slice(0, -'Schema'.length) as SpecTypeName] = schema;
254247
}
255248

256249
/**
257-
* Runtime validators for every MCP spec type, keyed by type name.
250+
* Returns the runtime validator for the named MCP spec type.
258251
*
259252
* Use this when you need to validate a spec-defined shape at a boundary the SDK does not own, for
260253
* example an extension's custom-method payload that embeds a `CallToolResult`, or a value read from
261254
* storage that should be a `Tool`.
262255
*
263-
* Each entry implements the Standard Schema interface, so it composes with any
256+
* The returned validator implements the Standard Schema interface, so it composes with any
264257
* Standard-Schema-aware library. For a simple boolean check, use {@linkcode isSpecType} instead.
265258
*
266259
* @example
267-
* ```ts source="./specTypeSchema.examples.ts#specTypeSchemas_basicUsage"
268-
* const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
260+
* ```ts source="./specTypeSchema.examples.ts#specTypeSchema_basicUsage"
261+
* const result = await specTypeSchema('CallToolResult')['~standard'].validate(untrusted);
269262
* if (result.issues === undefined) {
270263
* // result.value is CallToolResult
271264
* }
272265
* ```
273266
*/
274-
export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as unknown as SchemaRecord;
267+
export function specTypeSchema<K extends SpecTypeName>(name: K): StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]>;
268+
export function specTypeSchema(name: SpecTypeName): StandardSchemaV1 {
269+
return allSchemas[name];
270+
}
275271

276272
/**
277-
* Type predicates for every MCP spec type, keyed by type name.
273+
* Type predicate for the named MCP spec type.
278274
*
279275
* Returns `true` if the value satisfies the schema's input type (`z.input<>`, before defaults and
280276
* transforms are applied), and narrows to that input type. For schemas with `.default()` or
281277
* `.preprocess()`, this may accept values that do not structurally match the named output type;
282-
* for example `isSpecType.CallToolResult({})` is `true` because `content` has a default. Use
283-
* `specTypeSchemas.X['~standard'].validate(value)` when you need the validated output value.
284-
*
285-
* Each guard is a standalone function, so it can be passed directly as a callback.
278+
* for example `isSpecType('CallToolResult', {})` is `true` because `content` has a default. Use
279+
* `specTypeSchema(name)['~standard'].validate(value)` when you need the validated output value.
286280
*
287281
* @example
288282
* ```ts source="./specTypeSchema.examples.ts#isSpecType_basicUsage"
289-
* if (isSpecType.ContentBlock(value)) {
283+
* if (isSpecType('ContentBlock', value)) {
290284
* // value is ContentBlock
291285
* }
292286
*
293-
* const blocks = mixed.filter(isSpecType.ContentBlock);
287+
* const blocks = mixed.filter(v => isSpecType('ContentBlock', v));
294288
* ```
295289
*/
296-
export const isSpecType: GuardRecord = Object.freeze(_isSpecType) as unknown as GuardRecord;
290+
export function isSpecType<K extends SpecTypeName>(name: K, value: unknown): value is SpecTypeInputs[K];
291+
export function isSpecType(name: SpecTypeName, value: unknown): boolean {
292+
return allSchemas[name].safeParse(value).success;
293+
}

0 commit comments

Comments
 (0)