Skip to content

Commit 94abeea

Browse files
fix(core): explicit allowlist for SpecTypeName; changeset minor; type-checked @example snippets
- Replace import-* derivation with explicit SPEC_SCHEMA_KEYS tuple (150 entries with a matching public type in types.ts). Excludes internal helper schemas (ListChangedOptionsBase, BaseRequestParams, NotificationsParams, Client/ServerTasksCapability, ResourceTemplate name mismatch). - Add test asserting internals are not in SpecTypeName. - Bump changeset patch -> minor (5 new public exports). - Move @example code into specTypeSchema.examples.ts and source via sync:snippets, per repo convention.
1 parent 452a903 commit 94abeea

4 files changed

Lines changed: 239 additions & 26 deletions

File tree

.changeset/spec-type-schema.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
'@modelcontextprotocol/client': patch
3-
'@modelcontextprotocol/server': patch
2+
'@modelcontextprotocol/client': minor
3+
'@modelcontextprotocol/server': minor
44
---
55

66
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.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Type-checked examples for `specTypeSchema.ts`.
3+
*
4+
* These examples are synced into JSDoc comments via the sync-snippets script.
5+
* Each function's region markers define the code snippet that appears in the docs.
6+
*
7+
* @module
8+
*/
9+
10+
import { isSpecType, specTypeSchemas } from './specTypeSchema.js';
11+
12+
declare const untrusted: unknown;
13+
declare const value: unknown;
14+
declare const mixed: unknown[];
15+
16+
async function specTypeSchemas_basicUsage() {
17+
//#region specTypeSchemas_basicUsage
18+
const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
19+
if (result.issues === undefined) {
20+
// result.value is CallToolResult
21+
}
22+
//#endregion specTypeSchemas_basicUsage
23+
void result;
24+
}
25+
26+
function isSpecType_basicUsage() {
27+
/* eslint-disable unicorn/no-array-callback-reference -- showcasing the guard-as-callback pattern */
28+
//#region isSpecType_basicUsage
29+
if (isSpecType.ContentBlock(value)) {
30+
// value is ContentBlock
31+
}
32+
33+
const blocks = mixed.filter(isSpecType.ContentBlock);
34+
//#endregion isSpecType_basicUsage
35+
/* eslint-enable unicorn/no-array-callback-reference */
36+
void blocks;
37+
}
38+
39+
void specTypeSchemas_basicUsage;
40+
void isSpecType_basicUsage;

packages/core/src/types/specTypeSchema.ts

Lines changed: 188 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,169 @@ import {
1616
import type { StandardSchemaV1 } from '../util/standardSchema.js';
1717
import * as schemas from './schemas.js';
1818

19+
/**
20+
* Explicit allowlist of protocol Zod schemas that correspond to a public spec type in `types.ts`.
21+
*
22+
* This intentionally excludes internal helper schemas exported from `schemas.ts` that have no
23+
* matching public type (e.g. `ListChangedOptionsBaseSchema`, `BaseRequestParamsSchema`,
24+
* `NotificationsParamsSchema`, `ClientTasksCapabilitySchema`, `ServerTasksCapabilitySchema`,
25+
* `ResourceTemplateSchema` whose public type was renamed to `ResourceTemplateType`). Keeping the
26+
* list explicit means new public spec types must be added here deliberately, and internals never
27+
* leak into `SpecTypeName`.
28+
*/
29+
const SPEC_SCHEMA_KEYS = [
30+
'AnnotationsSchema',
31+
'AudioContentSchema',
32+
'BaseMetadataSchema',
33+
'BlobResourceContentsSchema',
34+
'BooleanSchemaSchema',
35+
'CallToolRequestSchema',
36+
'CallToolRequestParamsSchema',
37+
'CallToolResultSchema',
38+
'CancelledNotificationSchema',
39+
'CancelledNotificationParamsSchema',
40+
'CancelTaskRequestSchema',
41+
'CancelTaskResultSchema',
42+
'ClientCapabilitiesSchema',
43+
'ClientNotificationSchema',
44+
'ClientRequestSchema',
45+
'ClientResultSchema',
46+
'CompatibilityCallToolResultSchema',
47+
'CompleteRequestSchema',
48+
'CompleteRequestParamsSchema',
49+
'CompleteResultSchema',
50+
'ContentBlockSchema',
51+
'CreateMessageRequestSchema',
52+
'CreateMessageRequestParamsSchema',
53+
'CreateMessageResultSchema',
54+
'CreateMessageResultWithToolsSchema',
55+
'CreateTaskResultSchema',
56+
'CursorSchema',
57+
'ElicitationCompleteNotificationSchema',
58+
'ElicitationCompleteNotificationParamsSchema',
59+
'ElicitRequestSchema',
60+
'ElicitRequestFormParamsSchema',
61+
'ElicitRequestParamsSchema',
62+
'ElicitRequestURLParamsSchema',
63+
'ElicitResultSchema',
64+
'EmbeddedResourceSchema',
65+
'EmptyResultSchema',
66+
'EnumSchemaSchema',
67+
'GetPromptRequestSchema',
68+
'GetPromptRequestParamsSchema',
69+
'GetPromptResultSchema',
70+
'GetTaskPayloadRequestSchema',
71+
'GetTaskPayloadResultSchema',
72+
'GetTaskRequestSchema',
73+
'GetTaskResultSchema',
74+
'IconSchema',
75+
'IconsSchema',
76+
'ImageContentSchema',
77+
'ImplementationSchema',
78+
'InitializedNotificationSchema',
79+
'InitializeRequestSchema',
80+
'InitializeRequestParamsSchema',
81+
'InitializeResultSchema',
82+
'JSONArraySchema',
83+
'JSONObjectSchema',
84+
'JSONRPCErrorResponseSchema',
85+
'JSONRPCMessageSchema',
86+
'JSONRPCNotificationSchema',
87+
'JSONRPCRequestSchema',
88+
'JSONRPCResponseSchema',
89+
'JSONRPCResultResponseSchema',
90+
'JSONValueSchema',
91+
'LegacyTitledEnumSchemaSchema',
92+
'ListPromptsRequestSchema',
93+
'ListPromptsResultSchema',
94+
'ListResourcesRequestSchema',
95+
'ListResourcesResultSchema',
96+
'ListResourceTemplatesRequestSchema',
97+
'ListResourceTemplatesResultSchema',
98+
'ListRootsRequestSchema',
99+
'ListRootsResultSchema',
100+
'ListTasksRequestSchema',
101+
'ListTasksResultSchema',
102+
'ListToolsRequestSchema',
103+
'ListToolsResultSchema',
104+
'LoggingLevelSchema',
105+
'LoggingMessageNotificationSchema',
106+
'LoggingMessageNotificationParamsSchema',
107+
'ModelHintSchema',
108+
'ModelPreferencesSchema',
109+
'MultiSelectEnumSchemaSchema',
110+
'NotificationSchema',
111+
'NumberSchemaSchema',
112+
'PaginatedRequestSchema',
113+
'PaginatedRequestParamsSchema',
114+
'PaginatedResultSchema',
115+
'PingRequestSchema',
116+
'PrimitiveSchemaDefinitionSchema',
117+
'ProgressSchema',
118+
'ProgressNotificationSchema',
119+
'ProgressNotificationParamsSchema',
120+
'ProgressTokenSchema',
121+
'PromptSchema',
122+
'PromptArgumentSchema',
123+
'PromptListChangedNotificationSchema',
124+
'PromptMessageSchema',
125+
'PromptReferenceSchema',
126+
'ReadResourceRequestSchema',
127+
'ReadResourceRequestParamsSchema',
128+
'ReadResourceResultSchema',
129+
'RelatedTaskMetadataSchema',
130+
'RequestSchema',
131+
'RequestIdSchema',
132+
'RequestMetaSchema',
133+
'ResourceSchema',
134+
'ResourceContentsSchema',
135+
'ResourceLinkSchema',
136+
'ResourceListChangedNotificationSchema',
137+
'ResourceRequestParamsSchema',
138+
'ResourceTemplateReferenceSchema',
139+
'ResourceUpdatedNotificationSchema',
140+
'ResourceUpdatedNotificationParamsSchema',
141+
'ResultSchema',
142+
'RoleSchema',
143+
'RootSchema',
144+
'RootsListChangedNotificationSchema',
145+
'SamplingContentSchema',
146+
'SamplingMessageSchema',
147+
'SamplingMessageContentBlockSchema',
148+
'ServerCapabilitiesSchema',
149+
'ServerNotificationSchema',
150+
'ServerRequestSchema',
151+
'ServerResultSchema',
152+
'SetLevelRequestSchema',
153+
'SetLevelRequestParamsSchema',
154+
'SingleSelectEnumSchemaSchema',
155+
'StringSchemaSchema',
156+
'SubscribeRequestSchema',
157+
'SubscribeRequestParamsSchema',
158+
'TaskSchema',
159+
'TaskAugmentedRequestParamsSchema',
160+
'TaskCreationParamsSchema',
161+
'TaskMetadataSchema',
162+
'TaskStatusSchema',
163+
'TaskStatusNotificationSchema',
164+
'TaskStatusNotificationParamsSchema',
165+
'TextContentSchema',
166+
'TextResourceContentsSchema',
167+
'TitledMultiSelectEnumSchemaSchema',
168+
'TitledSingleSelectEnumSchemaSchema',
169+
'ToolSchema',
170+
'ToolAnnotationsSchema',
171+
'ToolChoiceSchema',
172+
'ToolExecutionSchema',
173+
'ToolListChangedNotificationSchema',
174+
'ToolResultContentSchema',
175+
'ToolUseContentSchema',
176+
'UnsubscribeRequestSchema',
177+
'UnsubscribeRequestParamsSchema',
178+
'UntitledMultiSelectEnumSchemaSchema',
179+
'UntitledSingleSelectEnumSchemaSchema'
180+
] as const satisfies readonly (keyof typeof schemas)[];
181+
19182
const authSchemas = {
20183
OAuthClientInformationFullSchema,
21184
OAuthClientInformationSchema,
@@ -28,20 +191,19 @@ const authSchemas = {
28191
OAuthTokensSchema,
29192
OpenIdProviderDiscoveryMetadataSchema,
30193
OpenIdProviderMetadataSchema
31-
};
194+
} as const;
32195

33-
type SchemaModule = typeof schemas & typeof authSchemas;
196+
type ProtocolSchemaKey = (typeof SPEC_SCHEMA_KEYS)[number];
197+
type AuthSchemaKey = keyof typeof authSchemas;
198+
type SchemaKey = ProtocolSchemaKey | AuthSchemaKey;
34199

35-
type StripSchemaSuffix<K> = K extends `${infer N}Schema` ? N : never;
200+
type SchemaFor<K extends SchemaKey> = K extends ProtocolSchemaKey
201+
? (typeof schemas)[K]
202+
: K extends AuthSchemaKey
203+
? (typeof authSchemas)[K]
204+
: never;
36205

37-
/** Keys of `schemas.ts` that end in `Schema` and hold a Standard Schema value. */
38-
type SchemaKey = {
39-
[K in keyof SchemaModule]: K extends `${string}Schema`
40-
? SchemaModule[K] extends { readonly '~standard': unknown }
41-
? K
42-
: never
43-
: never;
44-
}[keyof SchemaModule];
206+
type StripSchemaSuffix<K> = K extends `${infer N}Schema` ? N : never;
45207

46208
/**
47209
* Union of every named type in the SDK's protocol and OAuth schemas (e.g. `'CallToolResult'`,
@@ -56,7 +218,7 @@ export type SpecTypeName = StripSchemaSuffix<SchemaKey>;
56218
* `SpecTypes['CallToolResult']` is equivalent to importing the `CallToolResult` type directly.
57219
*/
58220
export type SpecTypes = {
59-
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaModule[K] extends z.ZodType ? z.output<SchemaModule[K]> : never;
221+
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaFor<K> extends z.ZodType ? z.output<SchemaFor<K>> : never;
60222
};
61223

62224
/**
@@ -65,23 +227,25 @@ export type SpecTypes = {
65227
* resulting output type.
66228
*/
67229
type SpecTypeInputs = {
68-
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaModule[K] extends z.ZodType ? z.input<SchemaModule[K]> : never;
230+
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaFor<K> extends z.ZodType ? z.input<SchemaFor<K>> : never;
69231
};
70232

71233
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]> };
72234
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };
73235

74236
const _specTypeSchemas: Record<string, z.ZodTypeAny> = {};
75237
const _isSpecType: Record<string, (value: unknown) => boolean> = {};
76-
for (const source of [schemas, authSchemas]) {
77-
for (const [key, value] of Object.entries(source)) {
78-
if (key.endsWith('Schema') && value !== null && typeof value === 'object') {
79-
const name = key.slice(0, -'Schema'.length);
80-
const schema = value as z.ZodTypeAny;
81-
_specTypeSchemas[name] = schema;
82-
_isSpecType[name] = (v: unknown) => schema.safeParse(v).success;
83-
}
84-
}
238+
function register(key: string, schema: z.ZodTypeAny): void {
239+
const name = key.slice(0, -'Schema'.length);
240+
_specTypeSchemas[name] = schema;
241+
_isSpecType[name] = (v: unknown) => schema.safeParse(v).success;
242+
}
243+
for (const key of SPEC_SCHEMA_KEYS) {
244+
// eslint-disable-next-line import/namespace -- key is constrained to keyof typeof schemas via the satisfies clause above
245+
register(key, schemas[key]);
246+
}
247+
for (const [key, schema] of Object.entries(authSchemas)) {
248+
register(key, schema);
85249
}
86250

87251
/**
@@ -95,7 +259,7 @@ for (const source of [schemas, authSchemas]) {
95259
* Standard-Schema-aware library. For a simple boolean check, use {@linkcode isSpecType} instead.
96260
*
97261
* @example
98-
* ```ts
262+
* ```ts source="./specTypeSchema.examples.ts#specTypeSchemas_basicUsage"
99263
* const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
100264
* if (result.issues === undefined) {
101265
* // result.value is CallToolResult
@@ -111,7 +275,7 @@ export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as
111275
* function, so it can be passed directly as a callback.
112276
*
113277
* @example
114-
* ```ts
278+
* ```ts source="./specTypeSchema.examples.ts#isSpecType_basicUsage"
115279
* if (isSpecType.ContentBlock(value)) {
116280
* // value is ContentBlock
117281
* }

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ describe('isSpecType', () => {
5858
expect(isSpecType['NotASpecType']).toBeUndefined();
5959
});
6060

61+
it('excludes internal helper schemas (no matching public type)', () => {
62+
// @ts-expect-error - ListChangedOptionsBase is internal-only
63+
expect(isSpecType['ListChangedOptionsBase']).toBeUndefined();
64+
// @ts-expect-error - BaseRequestParams is internal-only
65+
expect(specTypeSchemas['BaseRequestParams']).toBeUndefined();
66+
// @ts-expect-error - NotificationsParams is internal-only
67+
expect(isSpecType['NotificationsParams']).toBeUndefined();
68+
});
69+
6170
it('narrows the value type', () => {
6271
const v: unknown = { name: 'x', version: '1.0.0' };
6372
if (isSpecType.Implementation(v)) {

0 commit comments

Comments
 (0)