Skip to content

Commit 452a903

Browse files
fix(core): address review on isSpecType/specTypeSchemas
- Explicit auth-schema allowlist (excludes SafeUrl/OptionalSafeUrl/IdJagTokenExchangeResponse helpers) - Guard predicate types value as schema input (z.input), not output, since safeParse only proves input shape - SchemaRecord typed as StandardSchemaV1<In, Out> so validate() output is the spec type - JSDoc/migration examples await validate() (Result | Promise<Result>); drop stale setCustom* refs
1 parent 4bef047 commit 452a903

3 files changed

Lines changed: 50 additions & 16 deletions

File tree

docs/migration-SKILL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,12 @@ Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResu
439439

440440
If a `*Schema` constant was used for **runtime validation** (not just as a `request()` argument), replace with `isSpecType` / `specTypeSchemas`:
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)` | `specTypeSchemas.<TypeName>['~standard'].validate(value)` |
447-
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (returns `StandardSchemaV1<T>`) |
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>`) |
448448

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

docs/migration.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -461,11 +461,10 @@ const blocks = mixed.filter(isSpecType.ContentBlock);
461461

462462
// v2: or get the StandardSchemaV1 validator object directly
463463
import { specTypeSchemas } from '@modelcontextprotocol/client';
464-
const result = specTypeSchemas.CallToolResult['~standard'].validate(value);
464+
const result = await specTypeSchemas.CallToolResult['~standard'].validate(value);
465465
```
466466

467-
`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<T>`, which composes with any Standard-Schema-aware library and is accepted
468-
by `setCustomRequestHandler`/`sendCustomRequest`. The pre-existing `isCallToolResult(value)` guard still works.
467+
`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.
469468

470469
### Client list methods return empty results for missing capabilities
471470

packages/core/src/types/specTypeSchema.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,35 @@
11
import type * as z from 'zod/v4';
22

3-
import * as authSchemas from '../shared/auth.js';
3+
import {
4+
OAuthClientInformationFullSchema,
5+
OAuthClientInformationSchema,
6+
OAuthClientMetadataSchema,
7+
OAuthClientRegistrationErrorSchema,
8+
OAuthErrorResponseSchema,
9+
OAuthMetadataSchema,
10+
OAuthProtectedResourceMetadataSchema,
11+
OAuthTokenRevocationRequestSchema,
12+
OAuthTokensSchema,
13+
OpenIdProviderDiscoveryMetadataSchema,
14+
OpenIdProviderMetadataSchema
15+
} from '../shared/auth.js';
416
import type { StandardSchemaV1 } from '../util/standardSchema.js';
517
import * as schemas from './schemas.js';
618

19+
const authSchemas = {
20+
OAuthClientInformationFullSchema,
21+
OAuthClientInformationSchema,
22+
OAuthClientMetadataSchema,
23+
OAuthClientRegistrationErrorSchema,
24+
OAuthErrorResponseSchema,
25+
OAuthMetadataSchema,
26+
OAuthProtectedResourceMetadataSchema,
27+
OAuthTokenRevocationRequestSchema,
28+
OAuthTokensSchema,
29+
OpenIdProviderDiscoveryMetadataSchema,
30+
OpenIdProviderMetadataSchema
31+
};
32+
733
type SchemaModule = typeof schemas & typeof authSchemas;
834

935
type StripSchemaSuffix<K> = K extends `${infer N}Schema` ? N : never;
@@ -30,11 +56,20 @@ export type SpecTypeName = StripSchemaSuffix<SchemaKey>;
3056
* `SpecTypes['CallToolResult']` is equivalent to importing the `CallToolResult` type directly.
3157
*/
3258
export type SpecTypes = {
33-
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaModule[K] extends z.ZodType<infer T> ? T : never;
59+
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaModule[K] extends z.ZodType ? z.output<SchemaModule[K]> : never;
60+
};
61+
62+
/**
63+
* Input shape for each {@linkcode SpecTypeName}. For most types this equals {@linkcode SpecTypes},
64+
* but a few schemas apply defaults/preprocessing, so the accepted input may be looser than the
65+
* resulting output type.
66+
*/
67+
type SpecTypeInputs = {
68+
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaModule[K] extends z.ZodType ? z.input<SchemaModule[K]> : never;
3469
};
3570

36-
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1<SpecTypes[K]> };
37-
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypes[K] };
71+
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]> };
72+
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };
3873

3974
const _specTypeSchemas: Record<string, z.ZodTypeAny> = {};
4075
const _isSpecType: Record<string, (value: unknown) => boolean> = {};
@@ -56,12 +91,12 @@ for (const source of [schemas, authSchemas]) {
5691
* example an extension's custom-method payload that embeds a `CallToolResult`, or a value read from
5792
* storage that should be a `Tool`.
5893
*
59-
* Each entry implements the Standard Schema interface (`schema['~standard'].validate(value)`), so it
60-
* composes with any Standard-Schema-aware library.
94+
* Each entry implements the Standard Schema interface, so it composes with any
95+
* Standard-Schema-aware library. For a simple boolean check, use {@linkcode isSpecType} instead.
6196
*
6297
* @example
6398
* ```ts
64-
* const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
99+
* const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
65100
* if (result.issues === undefined) {
66101
* // result.value is CallToolResult
67102
* }

0 commit comments

Comments
 (0)