feat(core): add isSpecType / specTypeSchemas Records for runtime validation of spec types (#1887)

This commit is contained in:
Felix Weinberger 2026-04-30 13:16:23 +01:00 • committed by GitHub
commit 96db044fe9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 590 additions and 76 deletions

View file

@ -0,0 +1,6 @@
---
'@modelcontextprotocol/client': minor
'@modelcontextprotocol/server': minor
---
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.

View file

@ -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

View file

@ -36,13 +36,13 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table.
### Client imports
| v1 import path | v2 package |
| ---------------------------------------------------- | ------------------------------ |
| `@modelcontextprotocol/sdk/client/index.js` | `@modelcontextprotocol/client` |
| `@modelcontextprotocol/sdk/client/auth.js` | `@modelcontextprotocol/client` |
| `@modelcontextprotocol/sdk/client/streamableHttp.js` | `@modelcontextprotocol/client` |
| `@modelcontextprotocol/sdk/client/sse.js` | `@modelcontextprotocol/client` |
| `@modelcontextprotocol/sdk/client/stdio.js` | `@modelcontextprotocol/client/stdio` |
| v1 import path | v2 package |
| ---------------------------------------------------- | ------------------------------------------------------------------------------ |
| `@modelcontextprotocol/sdk/client/index.js` | `@modelcontextprotocol/client` |
| `@modelcontextprotocol/sdk/client/auth.js` | `@modelcontextprotocol/client` |
| `@modelcontextprotocol/sdk/client/streamableHttp.js` | `@modelcontextprotocol/client` |
| `@modelcontextprotocol/sdk/client/sse.js` | `@modelcontextprotocol/client` |
| `@modelcontextprotocol/sdk/client/stdio.js` | `@modelcontextprotocol/client/stdio` |
| `@modelcontextprotocol/sdk/client/websocket.js` | REMOVED (use Streamable HTTP or stdio; implement `Transport` for custom needs) |
### Server imports
@ -59,8 +59,8 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table.
### Types / shared imports
| v1 import path | v2 package |
| ------------------------------------------------- | ---------------------------- |
| v1 import path | v2 package |
| ------------------------------------------------- | ---------------------------------------------------------------- |
| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
@ -81,24 +81,25 @@ Notes:
## 5. Removed / Renamed Type Aliases and Symbols
| v1 (removed) | v2 (replacement) |
| ---------------------------------------- | -------------------------------------------------------- |
| `JSONRPCError` | `JSONRPCErrorResponse` |
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
| v1 (removed) | v2 (replacement) |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `JSONRPCError` | `JSONRPCErrorResponse` |
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
| `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` (**not** v2's new `isJSONRPCResponse`, which correctly matches both result and error) |
| `ResourceReference` | `ResourceTemplateReference` |
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) |
| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) |
| `McpError` | `ProtocolError` |
| `ErrorCode` | `ProtocolErrorCode` |
| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` |
| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` |
| `StreamableHTTPError` | REMOVED (use `SdkError` with `SdkErrorCode.ClientHttp*`) |
| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) |
| `ResourceReference` | `ResourceTemplateReference` |
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) |
| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) |
| `McpError` | `ProtocolError` |
| `ErrorCode` | `ProtocolErrorCode` |
| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` |
| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` |
| `StreamableHTTPError` | REMOVED (use `SdkError` with `SdkErrorCode.ClientHttp*`) |
| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) |
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 type guard functions like `isCallToolResult` instead of `CallToolResultSchema.safeParse()`.
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
`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.
### Error class changes
@ -212,7 +213,8 @@ Zod schemas, all callback return types. Note: `callTool()` and `request()` signa
The variadic `.tool()`, `.prompt()`, `.resource()` methods are removed. Use the `register*` methods with a config object.
**IMPORTANT**: v2 requires schema objects implementing [Standard Schema](https://standardschema.dev/) — raw shapes like `{ name: z.string() }` are no longer supported. Wrap with `z.object()` (Zod v4), or use ArkType's `type({...})`, or Valibot. For raw JSON Schema, wrap with `fromJsonSchema(schema)` from `@modelcontextprotocol/server` (validator defaults automatically; pass an explicit validator for custom configurations). Applies to `inputSchema`, `outputSchema`, and `argsSchema`.
**IMPORTANT**: v2 requires schema objects implementing [Standard Schema](https://standardschema.dev/) — raw shapes like `{ name: z.string() }` are no longer supported. Wrap with `z.object()` (Zod v4), or use ArkType's `type({...})`, or Valibot. For raw JSON Schema, wrap with
`fromJsonSchema(schema)` from `@modelcontextprotocol/server` (validator defaults automatically; pass an explicit validator for custom configurations). Applies to `inputSchema`, `outputSchema`, and `argsSchema`.
### Tools
@ -282,20 +284,20 @@ Note: the third argument (`metadata`) is required — pass `{}` if no metadata.
### Schema Migration Quick Reference
| v1 (raw shape) | v2 (Standard Schema object) |
|----------------|-----------------|
| `{ name: z.string() }` | `z.object({ name: z.string() })` |
| v1 (raw shape) | v2 (Standard Schema object) |
| ---------------------------------- | -------------------------------------------- |
| `{ name: z.string() }` | `z.object({ name: z.string() })` |
| `{ count: z.number().optional() }` | `z.object({ count: z.number().optional() })` |
| `{}` (empty) | `z.object({})` |
| `undefined` (no schema) | `undefined` or omit the field |
### Removed core exports
| Removed from `@modelcontextprotocol/core` | Replacement |
|---|---|
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
| Removed from `@modelcontextprotocol/core` | Replacement |
| ------------------------------------------------------------------------------------ | ----------------------------------------- |
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
| `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | none (internal Zod introspection helpers) |
## 7. Headers API
@ -460,29 +462,33 @@ For **custom (non-spec)** methods, keep the result-schema argument — see §9.
Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResultSchema`, `ElicitResultSchema`, `CreateMessageResultSchema`, etc., when they were only used in `request()`/`send()`/`callTool()` calls.
If `CallToolResultSchema` was used for **runtime validation** (not just as a `request()` argument), replace with the `isCallToolResult` type guard:
If a `*Schema` constant was used for **runtime validation** (not just as a `request()` argument), replace with `isSpecType` / `specTypeSchemas`:
| v1 pattern | v2 replacement |
| --------------------------------------------------- | -------------------------- |
| `CallToolResultSchema.safeParse(value).success` | `isCallToolResult(value)` |
| `CallToolResultSchema.parse(value)` | Use `isCallToolResult(value)` then cast, or use `CallToolResult` type |
| v1 pattern | v2 replacement |
| -------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `CallToolResultSchema.safeParse(value).success` | `isSpecType.CallToolResult(value)` |
| `<TypeName>Schema.safeParse(value).success` | `isSpecType.<TypeName>(value)` |
| `<TypeName>Schema.parse(value)` | `await specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result`, not the value) |
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1<In, Out>`) |
`isCallToolResult(value)` still works, but `isSpecType` covers every spec type by name.
## 12. Experimental: `TaskCreationParams.ttl` no longer accepts `null`
`TaskCreationParams.ttl` changed from `z.union([z.number(), z.null()]).optional()` to `z.number().optional()`. Per the MCP spec, `null` TTL (unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Omit `ttl` to let the server decide.
| v1 | v2 |
|---|---|
| `task: { ttl: null }` | `task: {}` (omit ttl) |
| v1 | v2 |
| ---------------------- | ---------------------------------- |
| `task: { ttl: null }` | `task: {}` (omit ttl) |
| `task: { ttl: 60000 }` | `task: { ttl: 60000 }` (unchanged) |
Type changes in handler context:
| Type | v1 | v2 |
|---|---|---|
| `TaskContext.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
| Type | v1 | v2 |
| ------------------------------------------- | ----------------------------- | --------------------- |
| `TaskContext.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
| `CreateTaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
| `TaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
| `TaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
> These task APIs are `@experimental` and may change without notice.
@ -513,6 +519,7 @@ new McpServer({ name: 'server', version: '1.0.0' }, {});
```
Access validators explicitly:
- Runtime-aware default: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';`
- AJV (Node.js): `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';`
- CF Worker: `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';`

View file

@ -59,7 +59,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
```
Note: `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core`, so you can import types and error classes from whichever package you already depend on. Do not import from `@modelcontextprotocol/core` directly — it is an internal package.
Note: `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core`, so you can import types and error classes from whichever package you already depend on. Do not import from `@modelcontextprotocol/core` directly
— it is an internal package.
### Dropped Node.js 18 and CommonJS
@ -296,11 +297,11 @@ This applies to:
**Removed Zod-specific helpers** from `@modelcontextprotocol/core` (use Standard Schema equivalents):
| Removed | Replacement |
|---|---|
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
| Removed | Replacement |
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
| `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | No replacement — these are now internal Zod introspection helpers |
### Host header validation moved
@ -488,18 +489,29 @@ The return type is now inferred from the method name via `ResultTypeMap`. For ex
For **custom (non-spec)** methods, keep the result-schema argument — see [Sending custom-method requests](#sending-custom-method-requests). Only drop the schema when calling a spec method.
If you were using `CallToolResultSchema` for **runtime validation** (not just in `request()`/`callTool()` calls), use the new `isCallToolResult` type guard instead:
If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), use `isSpecType` or `specTypeSchemas`:
```typescript
// v1: runtime validation with Zod schema
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
if (CallToolResultSchema.safeParse(value).success) { /* ... */ }
if (CallToolResultSchema.safeParse(value).success) {
/* ... */
}
// v2: use the type guard
import { isCallToolResult } from '@modelcontextprotocol/client';
if (isCallToolResult(value)) { /* ... */ }
// v2: keyed type predicate
import { isSpecType } from '@modelcontextprotocol/client';
if (isSpecType.CallToolResult(value)) {
/* ... */
}
const blocks = mixed.filter(isSpecType.ContentBlock);
// v2: or get the StandardSchemaV1 validator object directly
import { specTypeSchemas } from '@modelcontextprotocol/client';
const result = await specTypeSchemas.CallToolResult['~standard'].validate(value);
```
`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.
### Client list methods return empty results for missing capabilities
`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, and `listTools()` now return empty results when the server didn't advertise the corresponding capability, instead of sending the request. This respects the MCP spec's capability negotiation.
@ -533,20 +545,21 @@ import { InMemoryTransport } from '@modelcontextprotocol/client';
The following deprecated type aliases have been removed from `@modelcontextprotocol/core`:
| Removed | Replacement |
| ---------------------------------------- | ------------------------------------------------ |
| `JSONRPCError` | `JSONRPCErrorResponse` |
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
| `isJSONRPCResponse` | `isJSONRPCResultResponse` (see note below) |
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
| `ResourceReference` | `ResourceTemplateReference` |
| `IsomorphicHeaders` | Use Web Standard `Headers` |
| Removed | Replacement |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `JSONRPCError` | `JSONRPCErrorResponse` |
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
| `isJSONRPCResponse` | `isJSONRPCResultResponse` (see note below) |
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
| `ResourceReference` | `ResourceTemplateReference` |
| `IsomorphicHeaders` | Use Web Standard `Headers` |
| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) |
All other types and schemas exported from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`.
> **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for *result* responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it checks for *any* response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses.
> **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for _result_ responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it
> checks for _any_ response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses.
**Before (v1):**
@ -574,7 +587,7 @@ The `RequestHandlerExtra` type has been replaced with a structured context type
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
| `extra.authInfo` | `ctx.http?.authInfo` |
| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only on `ServerContext`) |
| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only on `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
| `extra.sessionId` | `ctx.sessionId` |
@ -842,7 +855,8 @@ try {
### Experimental: `TaskCreationParams.ttl` no longer accepts `null`
The `ttl` field in `TaskCreationParams` (used when requesting the server to create a task) no longer accepts `null`. Per the MCP spec, `null` TTL (meaning unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Clients should omit `ttl` to let the server decide the lifetime.
The `ttl` field in `TaskCreationParams` (used when requesting the server to create a task) no longer accepts `null`. Per the MCP spec, `null` TTL (meaning unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Clients should omit `ttl` to let
the server decide the lifetime.
This also narrows the type of `requestedTtl` in `TaskContext`, `CreateTaskServerContext`, and `TaskServerContext` from `number | null | undefined` to `number | undefined`.

View file

@ -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, specTypeSchemas } 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';

View file

@ -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';

View file

@ -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.
*/

View file

@ -0,0 +1,40 @@
/**
* 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, specTypeSchemas } from './specTypeSchema.js';
declare const untrusted: unknown;
declare const value: unknown;
declare const mixed: unknown[];
async function specTypeSchemas_basicUsage() {
//#region specTypeSchemas_basicUsage
const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
if (result.issues === undefined) {
// result.value is CallToolResult
}
//#endregion specTypeSchemas_basicUsage
void result;
}
function isSpecType_basicUsage() {
/* eslint-disable unicorn/no-array-callback-reference -- showcasing the guard-as-callback pattern */
//#region isSpecType_basicUsage
if (isSpecType.ContentBlock(value)) {
// value is ContentBlock
}
const blocks = mixed.filter(isSpecType.ContentBlock);
//#endregion isSpecType_basicUsage
/* eslint-enable unicorn/no-array-callback-reference */
void blocks;
}
void specTypeSchemas_basicUsage;
void isSpecType_basicUsage;

View file

@ -0,0 +1,296 @@
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;
};
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]> };
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };
const _specTypeSchemas: Record<string, z.ZodTypeAny> = {};
const _isSpecType: Record<string, (value: unknown) => boolean> = {};
function register(key: string, schema: z.ZodTypeAny): void {
const name = key.slice(0, -'Schema'.length);
_specTypeSchemas[name] = schema;
_isSpecType[name] = (v: unknown) => schema.safeParse(v).success;
}
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
register(key, schemas[key]);
}
for (const [key, schema] of Object.entries(authSchemas)) {
register(key, schema);
}
/**
* Runtime validators for every MCP spec type, keyed by type name.
*
* 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`.
*
* Each entry 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#specTypeSchemas_basicUsage"
* const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
* if (result.issues === undefined) {
* // result.value is CallToolResult
* }
* ```
*/
export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as unknown as SchemaRecord;
/**
* Type predicates for every MCP spec type, keyed by type name.
*
* 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
* `specTypeSchemas.X['~standard'].validate(value)` when you need the validated output value.
*
* Each guard is a standalone function, so it can be passed directly as a callback.
*
* @example
* ```ts source="./specTypeSchema.examples.ts#isSpecType_basicUsage"
* if (isSpecType.ContentBlock(value)) {
* // value is ContentBlock
* }
*
* const blocks = mixed.filter(isSpecType.ContentBlock);
* ```
*/
export const isSpecType: GuardRecord = Object.freeze(_isSpecType) as unknown as GuardRecord;

View file

@ -0,0 +1,123 @@
import { describe, expect, it } from 'vitest';
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', () => {
expect(
isJSONRPCResponse({
jsonrpc: JSONRPC_VERSION,
id: 1,
result: {}
})
).toBe(true);
});
it('returns true for a valid error response', () => {
expect(
isJSONRPCResponse({
jsonrpc: JSONRPC_VERSION,
id: 1,
error: { code: -32_600, message: 'Invalid Request' }
})
).toBe(true);
});
it('returns false for a request', () => {
expect(
isJSONRPCResponse({
jsonrpc: JSONRPC_VERSION,
id: 1,
method: 'test'
})
).toBe(false);
});
it('returns false for a notification', () => {
expect(
isJSONRPCResponse({
jsonrpc: JSONRPC_VERSION,
method: 'test'
})
).toBe(false);
});
it('returns false for arbitrary objects', () => {
expect(isJSONRPCResponse({ foo: 'bar' })).toBe(false);
});
it('narrows the type correctly', () => {
const value: unknown = {
jsonrpc: JSONRPC_VERSION,
id: 1,
result: { content: [] }
};
if (isJSONRPCResponse(value)) {
// Type should be narrowed to JSONRPCResponse
expect(value.jsonrpc).toBe(JSONRPC_VERSION);
expect(value.id).toBe(1);
}
});
it('agrees with isJSONRPCResultResponse || isJSONRPCErrorResponse', () => {
const values = [
{ jsonrpc: JSONRPC_VERSION, id: 1, result: {} },
{ jsonrpc: JSONRPC_VERSION, id: 2, error: { code: -1, message: 'err' } },
{ jsonrpc: JSONRPC_VERSION, id: 3, method: 'test' },
{ jsonrpc: JSONRPC_VERSION, method: 'notify' },
{ foo: 'bar' },
null,
42
];
for (const v of values) {
expect(isJSONRPCResponse(v)).toBe(isJSONRPCResultResponse(v) || isJSONRPCErrorResponse(v));
}
});
});
describe('isCallToolResult', () => {
it('returns false for an empty object (content is required)', () => {
expect(isCallToolResult({})).toBe(false);
});
it('returns true for a result with content', () => {
expect(
isCallToolResult({
content: [{ type: 'text', text: 'hello' }]
})
).toBe(true);
});
it('returns true for a result with isError', () => {
expect(
isCallToolResult({
content: [{ type: 'text', text: 'fail' }],
isError: true
})
).toBe(true);
});
it('returns true for a result with structuredContent', () => {
expect(
isCallToolResult({
content: [],
structuredContent: { key: 'value' }
})
).toBe(true);
});
it('returns false for non-objects', () => {
expect(isCallToolResult(null)).toBe(false);
expect(isCallToolResult(42)).toBe(false);
expect(isCallToolResult('string')).toBe(false);
});
it('returns false for invalid content items', () => {
expect(
isCallToolResult({
content: [{ type: 'invalid' }]
})
).toBe(false);
});
});

View file

@ -0,0 +1,148 @@
import { describe, expect, expectTypeOf, it } from 'vitest';
import type { OAuthMetadata, OAuthTokens } from '../../src/shared/auth.js';
import type { SpecTypeName, SpecTypes } from '../../src/types/specTypeSchema.js';
import { isSpecType, specTypeSchemas } from '../../src/types/specTypeSchema.js';
import type {
CallToolResult,
ContentBlock,
Implementation,
JSONObject,
JSONRPCRequest,
JSONValue,
ResourceTemplateType,
Tool
} from '../../src/types/types.js';
describe('specTypeSchemas', () => {
it('returns a StandardSchemaV1 validator that accepts valid values', () => {
const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x', version: '1.0.0' });
expect((result as { issues?: unknown }).issues).toBeUndefined();
});
it('returns a validator that rejects invalid values with issues', () => {
const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x' });
expect((result as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0);
});
it('rejects unknown names at compile time and is undefined at runtime', () => {
// @ts-expect-error - 'NotASpecType' is not a SpecTypeName
expect(specTypeSchemas['NotASpecType']).toBeUndefined();
});
it('covers JSON-RPC envelope types', () => {
const ok = specTypeSchemas.JSONRPCRequest['~standard'].validate({ jsonrpc: '2.0', id: 1, method: 'ping' });
expect((ok as { issues?: unknown }).issues).toBeUndefined();
});
it('covers OAuth types from shared/auth.ts', () => {
const ok = specTypeSchemas.OAuthTokens['~standard'].validate({ access_token: 'x', token_type: 'Bearer' });
expect((ok as { issues?: unknown }).issues).toBeUndefined();
const bad = specTypeSchemas.OAuthTokens['~standard'].validate({ token_type: 'Bearer' });
expect((bad as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0);
});
});
describe('isSpecType', () => {
it('CallToolResult — accepts valid, rejects invalid/null/primitive', () => {
expect(isSpecType.CallToolResult({ content: [{ type: 'text', text: 'hi' }] })).toBe(true);
expect(isSpecType.CallToolResult({ content: 'not-an-array' })).toBe(false);
expect(isSpecType.CallToolResult(null)).toBe(false);
expect(isSpecType.CallToolResult('string')).toBe(false);
});
it('ContentBlock — accepts text block, rejects wrong shape', () => {
expect(isSpecType.ContentBlock({ type: 'text', text: 'hi' })).toBe(true);
expect(isSpecType.ContentBlock({ type: 'text' })).toBe(false);
expect(isSpecType.ContentBlock({})).toBe(false);
});
it('Tool — accepts valid, rejects missing inputSchema', () => {
expect(isSpecType.Tool({ name: 'echo', inputSchema: { type: 'object' } })).toBe(true);
expect(isSpecType.Tool({ name: 'echo' })).toBe(false);
});
it('ResourceTemplate — accepts valid, rejects missing uriTemplate', () => {
expect(isSpecType.ResourceTemplate({ name: 'r', uriTemplate: 'file:///{path}' })).toBe(true);
expect(isSpecType.ResourceTemplate({ name: 'r' })).toBe(false);
});
it('rejects unknown names at compile time and is undefined at runtime', () => {
// @ts-expect-error - 'NotASpecType' is not a SpecTypeName
expect(isSpecType['NotASpecType']).toBeUndefined();
});
it('excludes internal helper schemas (no matching public type)', () => {
// @ts-expect-error - ListChangedOptionsBase is internal-only
expect(isSpecType['ListChangedOptionsBase']).toBeUndefined();
// @ts-expect-error - BaseRequestParams is internal-only
expect(specTypeSchemas['BaseRequestParams']).toBeUndefined();
// @ts-expect-error - NotificationsParams is internal-only
expect(isSpecType['NotificationsParams']).toBeUndefined();
});
it('narrows the value type to the schema input type', () => {
const v: unknown = { name: 'x', version: '1.0.0' };
if (isSpecType.Implementation(v)) {
// ImplementationSchema has no defaults/transforms, so its input type equals Implementation.
expectTypeOf(v).toEqualTypeOf<Implementation>();
}
});
it('narrows to the input type, not the output type, for schemas with defaults', () => {
const v: unknown = {};
expect(isSpecType.CallToolResult(v)).toBe(true);
if (isSpecType.CallToolResult(v)) {
// CallToolResultSchema has `content: z.array(...).default([])`, so the input type
// permits `content` to be absent. The guard narrows to that input shape.
expectTypeOf(v.content).toEqualTypeOf<ContentBlock[] | undefined>();
expectTypeOf(v).not.toEqualTypeOf<CallToolResult>();
}
});
it('JSONValue / JSONObject — narrows to the JSON type, not unknown', () => {
// These schemas use an explicit z.ZodType<T, T> annotation for recursion; without the
// second param Zod's Input defaults to `unknown` and the predicate would not narrow.
const v: unknown = { a: 1 };
if (isSpecType.JSONValue(v)) {
expectTypeOf(v).toEqualTypeOf<JSONValue>();
}
if (isSpecType.JSONObject(v)) {
expectTypeOf(v).toEqualTypeOf<JSONObject>();
}
});
it('guards work as filter callbacks and narrow the element type', () => {
const mixed: unknown[] = [{ type: 'text', text: 'hi' }, 42, { type: 'text' }];
const blocks = mixed.filter(isSpecType.ContentBlock);
expect(blocks).toHaveLength(1);
expectTypeOf(blocks).toEqualTypeOf<ContentBlock[]>();
});
});
describe('SpecTypeName / SpecTypes (type-level)', () => {
it('SpecTypeName includes representative names', () => {
expectTypeOf<'CallToolResult'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'ContentBlock'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'Tool'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'Implementation'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'JSONRPCRequest'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'OAuthTokens'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'OAuthMetadata'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'ResourceTemplate'>().toMatchTypeOf<SpecTypeName>();
});
it('SpecTypes[K] matches the named export type', () => {
expectTypeOf<SpecTypes['CallToolResult']>().toEqualTypeOf<CallToolResult>();
expectTypeOf<SpecTypes['ContentBlock']>().toEqualTypeOf<ContentBlock>();
expectTypeOf<SpecTypes['Tool']>().toEqualTypeOf<Tool>();
expectTypeOf<SpecTypes['Implementation']>().toEqualTypeOf<Implementation>();
expectTypeOf<SpecTypes['JSONRPCRequest']>().toEqualTypeOf<JSONRPCRequest>();
expectTypeOf<SpecTypes['OAuthTokens']>().toEqualTypeOf<OAuthTokens>();
expectTypeOf<SpecTypes['OAuthMetadata']>().toEqualTypeOf<OAuthMetadata>();
// The public type is exported as ResourceTemplateType (the bare name collides with the
// server package's ResourceTemplate class), so this is the one entry where the key and
// the public type name differ.
expectTypeOf<SpecTypes['ResourceTemplate']>().toEqualTypeOf<ResourceTemplateType>();
});
});