Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/spec-type-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@modelcontextprotocol/client': minor
'@modelcontextprotocol/server': minor
---

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`,
`SpecTypeName`, and `SpecTypes` types.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Include what changed, why, and how to migrate. Search for related sections and g
- **Files**: Lowercase with hyphens, test files with `.test.ts` suffix
- **Imports**: ES module style, include `.js` extension, group imports logically
- **Formatting**: 2-space indentation, semicolons required, single quotes preferred
- **Testing**: Co-locate tests with source files, use descriptive test names
- **Testing**: Place tests under each package's `test/` directory (vitest only includes `test/**/*.test.ts`), use descriptive test names
- **Comments**: JSDoc for public APIs, inline comments for complex logic

### JSDoc `@example` Code Snippets
Expand Down
116 changes: 62 additions & 54 deletions docs/migration-SKILL.md

Large diffs are not rendered by default.

110 changes: 67 additions & 43 deletions docs/migration.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/core/src/exports/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export { isTerminal } from '../../experimental/tasks/interfaces.js';
export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/tasks/stores/inMemory.js';

// Validator types and classes
export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema.js';
export { isSpecType, specTypeSchema } from '../../types/specTypeSchema.js';
export type { StandardSchemaV1, StandardSchemaWithJSON } from '../../util/standardSchema.js';
export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js';
export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js';
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from './enums.js';
export * from './errors.js';
export * from './guards.js';
export * from './schemas.js';
export * from './specTypeSchema.js';
export * from './types.js';
6 changes: 3 additions & 3 deletions packages/core/src/types/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import type {
ResultTypeMap
} from './types.js';

export const JSONValueSchema: z.ZodType<JSONValue> = z.lazy(() =>
export const JSONValueSchema: z.ZodType<JSONValue, JSONValue> = z.lazy(() =>
z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.string(), JSONValueSchema), z.array(JSONValueSchema)])
);
export const JSONObjectSchema: z.ZodType<JSONObject> = z.record(z.string(), JSONValueSchema);
export const JSONArraySchema: z.ZodType<JSONArray> = z.array(JSONValueSchema);
export const JSONObjectSchema: z.ZodType<JSONObject, JSONObject> = z.record(z.string(), JSONValueSchema);
export const JSONArraySchema: z.ZodType<JSONArray, JSONArray> = z.array(JSONValueSchema);
/**
* A progress token, used to associate progress notifications with the original request.
*/
Expand Down
38 changes: 38 additions & 0 deletions packages/core/src/types/specTypeSchema.examples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Type-checked examples for `specTypeSchema.ts`.
*
* These examples are synced into JSDoc comments via the sync-snippets script.
* Each function's region markers define the code snippet that appears in the docs.
*
* @module
*/

import { isSpecType, specTypeSchema } from './specTypeSchema.js';

declare const untrusted: unknown;
declare const value: unknown;
declare const mixed: unknown[];

async function specTypeSchema_basicUsage() {
//#region specTypeSchema_basicUsage
const result = await specTypeSchema('CallToolResult')['~standard'].validate(untrusted);
if (result.issues === undefined) {
// result.value is CallToolResult
}
//#endregion specTypeSchema_basicUsage
void result;
}

function isSpecType_basicUsage() {
//#region isSpecType_basicUsage
if (isSpecType('ContentBlock', value)) {
// value is ContentBlock
}

const blocks = mixed.filter(v => isSpecType('ContentBlock', v));
//#endregion isSpecType_basicUsage
void blocks;
}

void specTypeSchema_basicUsage;
void isSpecType_basicUsage;
293 changes: 293 additions & 0 deletions packages/core/src/types/specTypeSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import type * as z from 'zod/v4';

import {
OAuthClientInformationFullSchema,
OAuthClientInformationSchema,
OAuthClientMetadataSchema,
OAuthClientRegistrationErrorSchema,
OAuthErrorResponseSchema,
OAuthMetadataSchema,
OAuthProtectedResourceMetadataSchema,
OAuthTokenRevocationRequestSchema,
OAuthTokensSchema,
OpenIdProviderDiscoveryMetadataSchema,
OpenIdProviderMetadataSchema
} from '../shared/auth.js';
import type { StandardSchemaV1 } from '../util/standardSchema.js';
import * as schemas from './schemas.js';

/**
* Explicit allowlist of protocol Zod schemas that correspond to a public spec type in `types.ts`.
*
* This intentionally excludes internal helper schemas exported from `schemas.ts` that have no
* matching public type (e.g. `ListChangedOptionsBaseSchema`, `BaseRequestParamsSchema`,
* `NotificationsParamsSchema`, `ClientTasksCapabilitySchema`, `ServerTasksCapabilitySchema`).
* Keeping the list explicit means new public spec types must be added here deliberately, and
* internals never leak into `SpecTypeName`.
*
* `ResourceTemplateSchema` is included; its public type is exported as `ResourceTemplateType`
* (the bare name collides with the server package's `ResourceTemplate` class), so
* `SpecTypes['ResourceTemplate']` is structurally equal to `ResourceTemplateType` rather than to
* a type literally named `ResourceTemplate`.
*/
const SPEC_SCHEMA_KEYS = [
'AnnotationsSchema',
'AudioContentSchema',
'BaseMetadataSchema',
'BlobResourceContentsSchema',
'BooleanSchemaSchema',
'CallToolRequestSchema',
'CallToolRequestParamsSchema',
'CallToolResultSchema',
'CancelledNotificationSchema',
'CancelledNotificationParamsSchema',
'CancelTaskRequestSchema',
'CancelTaskResultSchema',
'ClientCapabilitiesSchema',
'ClientNotificationSchema',
'ClientRequestSchema',
'ClientResultSchema',
'CompatibilityCallToolResultSchema',
'CompleteRequestSchema',
'CompleteRequestParamsSchema',
'CompleteResultSchema',
'ContentBlockSchema',
'CreateMessageRequestSchema',
'CreateMessageRequestParamsSchema',
'CreateMessageResultSchema',
'CreateMessageResultWithToolsSchema',
'CreateTaskResultSchema',
'CursorSchema',
'ElicitationCompleteNotificationSchema',
'ElicitationCompleteNotificationParamsSchema',
'ElicitRequestSchema',
'ElicitRequestFormParamsSchema',
'ElicitRequestParamsSchema',
'ElicitRequestURLParamsSchema',
'ElicitResultSchema',
'EmbeddedResourceSchema',
'EmptyResultSchema',
'EnumSchemaSchema',
'GetPromptRequestSchema',
'GetPromptRequestParamsSchema',
'GetPromptResultSchema',
'GetTaskPayloadRequestSchema',
'GetTaskPayloadResultSchema',
'GetTaskRequestSchema',
'GetTaskResultSchema',
'IconSchema',
'IconsSchema',
'ImageContentSchema',
'ImplementationSchema',
'InitializedNotificationSchema',
'InitializeRequestSchema',
'InitializeRequestParamsSchema',
'InitializeResultSchema',
'JSONArraySchema',
'JSONObjectSchema',
'JSONRPCErrorResponseSchema',
'JSONRPCMessageSchema',
'JSONRPCNotificationSchema',
'JSONRPCRequestSchema',
'JSONRPCResponseSchema',
'JSONRPCResultResponseSchema',
'JSONValueSchema',
'LegacyTitledEnumSchemaSchema',
'ListPromptsRequestSchema',
'ListPromptsResultSchema',
'ListResourcesRequestSchema',
'ListResourcesResultSchema',
'ListResourceTemplatesRequestSchema',
'ListResourceTemplatesResultSchema',
'ListRootsRequestSchema',
'ListRootsResultSchema',
'ListTasksRequestSchema',
'ListTasksResultSchema',
'ListToolsRequestSchema',
'ListToolsResultSchema',
'LoggingLevelSchema',
'LoggingMessageNotificationSchema',
'LoggingMessageNotificationParamsSchema',
'ModelHintSchema',
'ModelPreferencesSchema',
'MultiSelectEnumSchemaSchema',
'NotificationSchema',
'NumberSchemaSchema',
'PaginatedRequestSchema',
'PaginatedRequestParamsSchema',
'PaginatedResultSchema',
'PingRequestSchema',
'PrimitiveSchemaDefinitionSchema',
'ProgressSchema',
'ProgressNotificationSchema',
'ProgressNotificationParamsSchema',
'ProgressTokenSchema',
'PromptSchema',
'PromptArgumentSchema',
'PromptListChangedNotificationSchema',
'PromptMessageSchema',
'PromptReferenceSchema',
'ReadResourceRequestSchema',
'ReadResourceRequestParamsSchema',
'ReadResourceResultSchema',
'RelatedTaskMetadataSchema',
'RequestSchema',
'RequestIdSchema',
'RequestMetaSchema',
'ResourceSchema',
'ResourceContentsSchema',
'ResourceLinkSchema',
'ResourceListChangedNotificationSchema',
'ResourceRequestParamsSchema',
'ResourceTemplateSchema',
'ResourceTemplateReferenceSchema',
'ResourceUpdatedNotificationSchema',
'ResourceUpdatedNotificationParamsSchema',
'ResultSchema',
'RoleSchema',
'RootSchema',
'RootsListChangedNotificationSchema',
'SamplingContentSchema',
'SamplingMessageSchema',
'SamplingMessageContentBlockSchema',
'ServerCapabilitiesSchema',
'ServerNotificationSchema',
'ServerRequestSchema',
'ServerResultSchema',
'SetLevelRequestSchema',
'SetLevelRequestParamsSchema',
'SingleSelectEnumSchemaSchema',
'StringSchemaSchema',
'SubscribeRequestSchema',
'SubscribeRequestParamsSchema',
'TaskSchema',
'TaskAugmentedRequestParamsSchema',
'TaskCreationParamsSchema',
'TaskMetadataSchema',
'TaskStatusSchema',
'TaskStatusNotificationSchema',
'TaskStatusNotificationParamsSchema',
'TextContentSchema',
'TextResourceContentsSchema',
'TitledMultiSelectEnumSchemaSchema',
'TitledSingleSelectEnumSchemaSchema',
'ToolSchema',
'ToolAnnotationsSchema',
'ToolChoiceSchema',
'ToolExecutionSchema',
'ToolListChangedNotificationSchema',
'ToolResultContentSchema',
'ToolUseContentSchema',
'UnsubscribeRequestSchema',
'UnsubscribeRequestParamsSchema',
'UntitledMultiSelectEnumSchemaSchema',
'UntitledSingleSelectEnumSchemaSchema'
] as const satisfies readonly (keyof typeof schemas)[];

const authSchemas = {
OAuthClientInformationFullSchema,
OAuthClientInformationSchema,
OAuthClientMetadataSchema,
OAuthClientRegistrationErrorSchema,
OAuthErrorResponseSchema,
OAuthMetadataSchema,
OAuthProtectedResourceMetadataSchema,
OAuthTokenRevocationRequestSchema,
OAuthTokensSchema,
OpenIdProviderDiscoveryMetadataSchema,
OpenIdProviderMetadataSchema
} as const;

type ProtocolSchemaKey = (typeof SPEC_SCHEMA_KEYS)[number];
type AuthSchemaKey = keyof typeof authSchemas;
type SchemaKey = ProtocolSchemaKey | AuthSchemaKey;

type SchemaFor<K extends SchemaKey> = K extends ProtocolSchemaKey
? (typeof schemas)[K]
: K extends AuthSchemaKey
? (typeof authSchemas)[K]
: never;

type StripSchemaSuffix<K> = K extends `${infer N}Schema` ? N : never;

/**
* Union of every named type in the SDK's protocol and OAuth schemas (e.g. `'CallToolResult'`,
* `'ContentBlock'`, `'Tool'`, `'OAuthTokens'`). Derived from the internal Zod schemas, so it stays
* in sync with the spec.
*/
export type SpecTypeName = StripSchemaSuffix<SchemaKey>;

/**
* Maps each {@linkcode SpecTypeName} to its TypeScript type.
*
* `SpecTypes['CallToolResult']` is equivalent to importing the `CallToolResult` type directly.
*/
export type SpecTypes = {
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaFor<K> extends z.ZodType ? z.output<SchemaFor<K>> : never;
};

/**
* Input shape for each {@linkcode SpecTypeName}. For most types this equals {@linkcode SpecTypes},
* but a few schemas apply defaults/preprocessing, so the accepted input may be looser than the
* resulting output type.
*/
type SpecTypeInputs = {
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaFor<K> extends z.ZodType ? z.input<SchemaFor<K>> : never;
};

// Populated for every SpecTypeName by the loops below; the cast lets `allSchemas[name]` be
// non-undefined under `noUncheckedIndexedAccess` when `name` is a SpecTypeName.
const allSchemas = {} as Record<SpecTypeName, z.ZodType>;
for (const key of SPEC_SCHEMA_KEYS) {
// eslint-disable-next-line import/namespace -- key is constrained to keyof typeof schemas via the satisfies clause above
allSchemas[key.slice(0, -'Schema'.length) as SpecTypeName] = schemas[key];
}
for (const [key, schema] of Object.entries(authSchemas)) {
allSchemas[key.slice(0, -'Schema'.length) as SpecTypeName] = schema;
}

/**
* Returns the runtime validator for the named MCP spec type.
*
* Use this when you need to validate a spec-defined shape at a boundary the SDK does not own, for
* example an extension's custom-method payload that embeds a `CallToolResult`, or a value read from
* storage that should be a `Tool`.
*
* The returned validator implements the Standard Schema interface, so it composes with any
* Standard-Schema-aware library. For a simple boolean check, use {@linkcode isSpecType} instead.
*
* @example
* ```ts source="./specTypeSchema.examples.ts#specTypeSchema_basicUsage"
* const result = await specTypeSchema('CallToolResult')['~standard'].validate(untrusted);
* if (result.issues === undefined) {
* // result.value is CallToolResult
* }
* ```
*/
export function specTypeSchema<K extends SpecTypeName>(name: K): StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]>;
export function specTypeSchema(name: SpecTypeName): StandardSchemaV1 {
return allSchemas[name];
}

/**
* Type predicate for the named MCP spec type.
*
* Returns `true` if the value satisfies the schema's input type (`z.input<>`, before defaults and
* transforms are applied), and narrows to that input type. For schemas with `.default()` or
* `.preprocess()`, this may accept values that do not structurally match the named output type;
* for example `isSpecType('CallToolResult', {})` is `true` because `content` has a default. Use
* `specTypeSchema(name)['~standard'].validate(value)` when you need the validated output value.
*
* @example
* ```ts source="./specTypeSchema.examples.ts#isSpecType_basicUsage"
* if (isSpecType('ContentBlock', value)) {
* // value is ContentBlock
* }
*
* const blocks = mixed.filter(v => isSpecType('ContentBlock', v));
* ```
*/
export function isSpecType<K extends SpecTypeName>(name: K, value: unknown): value is SpecTypeInputs[K];
export function isSpecType(name: SpecTypeName, value: unknown): boolean {
return allSchemas[name].safeParse(value).success;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';

import { JSONRPC_VERSION } from './constants.js';
import { isCallToolResult, isJSONRPCErrorResponse, isJSONRPCResponse, isJSONRPCResultResponse } from './guards.js';
import { JSONRPC_VERSION } from '../../src/types/constants.js';
import { isCallToolResult, isJSONRPCErrorResponse, isJSONRPCResponse, isJSONRPCResultResponse } from '../../src/types/guards.js';

describe('isJSONRPCResponse', () => {
it('returns true for a valid result response', () => {
Expand Down
Loading
Loading