feat: support Standard Schema for tool/prompt schemas (#1689)

Co-authored-by: Matt Carey <mcarey@cloudflare.com>
This commit is contained in:
Felix Weinberger 2026-03-23 14:44:38 +00:00 • committed by GitHub
commit 0784be1a67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1327 additions and 212 deletions

View file

@ -4,12 +4,12 @@
*/
import type {
AnySchema,
CallToolResult,
CreateTaskResult,
CreateTaskServerContext,
GetTaskResult,
Result,
StandardSchemaWithJSON,
TaskServerContext
} from '@modelcontextprotocol/core';
@ -23,18 +23,17 @@ import type { BaseToolCallback } from '../../server/mcp.js';
* Handler for creating a task.
* @experimental
*/
export type CreateTaskRequestHandler<ResultT extends Result, Args extends AnySchema | undefined = undefined> = BaseToolCallback<
ResultT,
CreateTaskServerContext,
Args
>;
export type CreateTaskRequestHandler<
SendResultT extends Result,
Args extends StandardSchemaWithJSON | undefined = undefined
> = BaseToolCallback<SendResultT, CreateTaskServerContext, Args>;
/**
* Handler for task operations (`get`, `getResult`).
* @experimental
*/
export type TaskRequestHandler<ResultT extends Result, Args extends AnySchema | undefined = undefined> = BaseToolCallback<
ResultT,
export type TaskRequestHandler<SendResultT extends Result, Args extends StandardSchemaWithJSON | undefined = undefined> = BaseToolCallback<
SendResultT,
TaskServerContext,
Args
>;
@ -48,7 +47,7 @@ export type TaskRequestHandler<ResultT extends Result, Args extends AnySchema |
* @see {@linkcode @modelcontextprotocol/server!experimental/tasks/mcpServer.ExperimentalMcpServerTasks#registerToolTask | registerToolTask} for registration.
* @experimental
*/
export interface ToolTaskHandler<Args extends AnySchema | undefined = undefined> {
export interface ToolTaskHandler<Args extends StandardSchemaWithJSON | undefined = undefined> {
/**
* Called on the initial `tools/call` request.
*

View file

@ -5,7 +5,7 @@
* @experimental
*/
import type { AnySchema, TaskToolExecution, ToolAnnotations, ToolExecution } from '@modelcontextprotocol/core';
import type { StandardSchemaWithJSON, TaskToolExecution, ToolAnnotations, ToolExecution } from '@modelcontextprotocol/core';
import type { AnyToolHandler, McpServer, RegisteredTool } from '../../server/mcp.js';
import type { ToolTaskHandler } from './interfaces.js';
@ -19,12 +19,12 @@ interface McpServerInternal {
name: string,
title: string | undefined,
description: string | undefined,
inputSchema: AnySchema | undefined,
outputSchema: AnySchema | undefined,
inputSchema: StandardSchemaWithJSON | undefined,
outputSchema: StandardSchemaWithJSON | undefined,
annotations: ToolAnnotations | undefined,
execution: ToolExecution | undefined,
_meta: Record<string, unknown> | undefined,
handler: AnyToolHandler<AnySchema | undefined>
handler: AnyToolHandler<StandardSchemaWithJSON | undefined>
): RegisteredTool;
}
@ -76,7 +76,7 @@ export class ExperimentalMcpServerTasks {
*
* @experimental
*/
registerToolTask<OutputArgs extends AnySchema | undefined>(
registerToolTask<OutputArgs extends StandardSchemaWithJSON | undefined>(
name: string,
config: {
title?: string;
@ -89,7 +89,7 @@ export class ExperimentalMcpServerTasks {
handler: ToolTaskHandler<undefined>
): RegisteredTool;
registerToolTask<InputArgs extends AnySchema, OutputArgs extends AnySchema | undefined>(
registerToolTask<InputArgs extends StandardSchemaWithJSON, OutputArgs extends StandardSchemaWithJSON | undefined>(
name: string,
config: {
title?: string;
@ -103,7 +103,7 @@ export class ExperimentalMcpServerTasks {
handler: ToolTaskHandler<InputArgs>
): RegisteredTool;
registerToolTask<InputArgs extends AnySchema | undefined, OutputArgs extends AnySchema | undefined>(
registerToolTask<InputArgs extends StandardSchemaWithJSON | undefined, OutputArgs extends StandardSchemaWithJSON | undefined>(
name: string,
config: {
title?: string;
@ -133,7 +133,7 @@ export class ExperimentalMcpServerTasks {
config.annotations,
execution,
config._meta,
handler as AnyToolHandler<AnySchema | undefined>
handler as AnyToolHandler<StandardSchemaWithJSON | undefined>
);
}
}

View file

@ -6,21 +6,21 @@
*/
import type {
AnySchema,
CancelTaskResult,
CreateMessageRequestParams,
CreateMessageResult,
ElicitRequestFormParams,
ElicitRequestURLParams,
ElicitResult,
GetTaskPayloadResult,
GetTaskResult,
ListTasksResult,
RequestMethod,
RequestOptions,
ResponseMessage,
ResultTypeMap,
SchemaOutput
ResultTypeMap
} from '@modelcontextprotocol/core';
import { GetTaskPayloadResultSchema } from '@modelcontextprotocol/core';
import type { Server } from '../../server/server.js';
@ -258,22 +258,22 @@ export class ExperimentalServerTasks {
* Retrieves the result of a completed task.
*
* @param taskId - The task identifier
* @param resultSchema - Zod schema for validating the result
* @param options - Optional request options
* @returns The task result
* @returns The task result. The payload structure matches the result type of the
* original request (e.g., a `tools/call` task returns a `CallToolResult`).
*
* @experimental
*/
async getTaskResult<T extends AnySchema>(taskId: string, resultSchema?: T, options?: RequestOptions): Promise<SchemaOutput<T>> {
async getTaskResult(taskId: string, options?: RequestOptions): Promise<GetTaskPayloadResult> {
return (
this._server as unknown as {
getTaskResult: <U extends AnySchema>(
getTaskResult: (
params: { taskId: string },
resultSchema?: U,
resultSchema: typeof GetTaskPayloadResultSchema,
options?: RequestOptions
) => Promise<SchemaOutput<U>>;
) => Promise<GetTaskPayloadResult>;
}
).getTaskResult({ taskId }, resultSchema, options);
).getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
}
/**

View file

@ -1,25 +1,24 @@
import type { AnySchema } from '@modelcontextprotocol/core';
import type * as z from 'zod/v4';
import type { StandardSchemaWithJSON } from '@modelcontextprotocol/core';
export const COMPLETABLE_SYMBOL: unique symbol = Symbol.for('mcp.completable');
export type CompleteCallback<T extends AnySchema = AnySchema> = (
value: z.input<T>,
export type CompleteCallback<T extends StandardSchemaWithJSON = StandardSchemaWithJSON> = (
value: StandardSchemaWithJSON.InferInput<T>,
context?: {
arguments?: Record<string, string>;
}
) => z.input<T>[] | Promise<z.input<T>[]>;
) => StandardSchemaWithJSON.InferInput<T>[] | Promise<StandardSchemaWithJSON.InferInput<T>[]>;
export type CompletableMeta<T extends AnySchema = AnySchema> = {
export type CompletableMeta<T extends StandardSchemaWithJSON = StandardSchemaWithJSON> = {
complete: CompleteCallback<T>;
};
export type CompletableSchema<T extends AnySchema> = T & {
export type CompletableSchema<T extends StandardSchemaWithJSON> = T & {
[COMPLETABLE_SYMBOL]: CompletableMeta<T>;
};
/**
* Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP.
* Wraps a schema to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP.
*
* @example
* ```ts source="./completable.examples.ts#completable_basicUsage"
@ -49,7 +48,7 @@ export type CompletableSchema<T extends AnySchema> = T & {
*
* @see {@linkcode server/mcp.McpServer.registerPrompt | McpServer.registerPrompt} for using completable schemas in prompt argument definitions
*/
export function completable<T extends AnySchema>(schema: T, complete: CompleteCallback<T>): CompletableSchema<T> {
export function completable<T extends StandardSchemaWithJSON>(schema: T, complete: CompleteCallback<T>): CompletableSchema<T> {
Object.defineProperty(schema as object, COMPLETABLE_SYMBOL, {
value: { complete } as CompletableMeta<T>,
enumerable: false,
@ -62,14 +61,14 @@ export function completable<T extends AnySchema>(schema: T, complete: CompleteCa
/**
* Checks if a schema is completable (has completion metadata).
*/
export function isCompletable(schema: unknown): schema is CompletableSchema<AnySchema> {
export function isCompletable(schema: unknown): schema is CompletableSchema<StandardSchemaWithJSON> {
return !!schema && typeof schema === 'object' && COMPLETABLE_SYMBOL in (schema as object);
}
/**
* Gets the completer callback from a completable schema, if it exists.
*/
export function getCompleter<T extends AnySchema>(schema: T): CompleteCallback<T> | undefined {
export function getCompleter<T extends StandardSchemaWithJSON>(schema: T): CompleteCallback<T> | undefined {
const meta = (schema as unknown as { [COMPLETABLE_SYMBOL]?: CompletableMeta<T> })[COMPLETABLE_SYMBOL];
return meta?.complete as CompleteCallback<T> | undefined;
}

View file

@ -1,5 +1,4 @@
import type {
AnySchema,
BaseMetadata,
CallToolRequest,
CallToolResult,
@ -15,14 +14,13 @@ import type {
ListToolsResult,
LoggingMessageNotification,
Prompt,
PromptArgument,
PromptReference,
ReadResourceResult,
Resource,
ResourceTemplateReference,
Result,
SchemaOutput,
ServerContext,
StandardSchemaWithJSON,
Tool,
ToolAnnotations,
ToolExecution,
@ -32,16 +30,13 @@ import type {
import {
assertCompleteRequestPrompt,
assertCompleteRequestResourceTemplate,
getSchemaDescription,
getSchemaShape,
isOptionalSchema,
parseSchemaAsync,
promptArgumentsFromStandardSchema,
ProtocolError,
ProtocolErrorCode,
schemaToJson,
unwrapOptionalSchema,
standardSchemaToJsonSchema,
UriTemplate,
validateAndWarnToolName
validateAndWarnToolName,
validateStandardSchema
} from '@modelcontextprotocol/core';
import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js';
@ -147,7 +142,7 @@ export class McpServer {
title: tool.title,
description: tool.description,
inputSchema: tool.inputSchema
? (schemaToJson(tool.inputSchema, { io: 'input' }) as Tool['inputSchema'])
? (standardSchemaToJsonSchema(tool.inputSchema, 'input') as Tool['inputSchema'])
: EMPTY_OBJECT_JSON_SCHEMA,
annotations: tool.annotations,
execution: tool.execution,
@ -155,9 +150,7 @@ export class McpServer {
};
if (tool.outputSchema) {
toolDefinition.outputSchema = schemaToJson(tool.outputSchema, {
io: 'output'
}) as Tool['outputSchema'];
toolDefinition.outputSchema = standardSchemaToJsonSchema(tool.outputSchema, 'output') as Tool['outputSchema'];
}
return toolDefinition;
@ -177,7 +170,7 @@ export class McpServer {
try {
const isTaskRequest = !!request.params.task;
const taskSupport = tool.execution?.taskSupport;
const isTaskHandler = 'createTask' in (tool.handler as AnyToolHandler<AnySchema>);
const isTaskHandler = 'createTask' in (tool.handler as AnyToolHandler<StandardSchemaWithJSON>);
// Validate task hint configuration
if ((taskSupport === 'required' || taskSupport === 'optional') && !isTaskHandler) {
@ -245,23 +238,22 @@ export class McpServer {
* Validates tool input arguments against the tool's input schema.
*/
private async validateToolInput<
Tool extends RegisteredTool,
Args extends Tool['inputSchema'] extends infer InputSchema
? InputSchema extends AnySchema
? SchemaOutput<InputSchema>
ToolType extends RegisteredTool,
Args extends ToolType['inputSchema'] extends infer InputSchema
? InputSchema extends StandardSchemaWithJSON
? StandardSchemaWithJSON.InferOutput<InputSchema>
: undefined
: undefined
>(tool: Tool, args: Args, toolName: string): Promise<Args> {
>(tool: ToolType, args: Args, toolName: string): Promise<Args> {
if (!tool.inputSchema) {
return undefined as Args;
}
const parseResult = await parseSchemaAsync(tool.inputSchema, args ?? {});
const parseResult = await validateStandardSchema(tool.inputSchema, args ?? {});
if (!parseResult.success) {
const errorMessage = parseResult.error.issues.map((i: { message: string }) => i.message).join(', ');
throw new ProtocolError(
ProtocolErrorCode.InvalidParams,
`Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`
`Input validation error: Invalid arguments for tool ${toolName}: ${parseResult.error}`
);
}
@ -293,12 +285,11 @@ export class McpServer {
}
// if the tool has an output schema, validate structured content
const parseResult = await parseSchemaAsync(tool.outputSchema, result.structuredContent);
const parseResult = await validateStandardSchema(tool.outputSchema, result.structuredContent);
if (!parseResult.success) {
const errorMessage = parseResult.error.issues.map((i: { message: string }) => i.message).join(', ');
throw new ProtocolError(
ProtocolErrorCode.InvalidParams,
`Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`
`Output validation error: Invalid structured content for tool ${toolName}: ${parseResult.error}`
);
}
}
@ -394,7 +385,7 @@ export class McpServer {
}
const promptShape = getSchemaShape(prompt.argsSchema);
const field = promptShape?.[request.params.argument.name];
const field = unwrapOptionalSchema(promptShape?.[request.params.argument.name]);
if (!isCompletable(field)) {
return EMPTY_COMPLETION_RESULT;
}
@ -403,6 +394,7 @@ export class McpServer {
if (!completer) {
return EMPTY_COMPLETION_RESULT;
}
const suggestions = await completer(request.params.argument.value, request.params.context);
return createCompletionResult(suggestions);
}
@ -538,7 +530,7 @@ export class McpServer {
name,
title: prompt.title,
description: prompt.description,
arguments: prompt.argsSchema ? promptArgumentsFromSchema(prompt.argsSchema) : undefined
arguments: prompt.argsSchema ? promptArgumentsFromStandardSchema(prompt.argsSchema) : undefined
};
})
})
@ -706,8 +698,8 @@ export class McpServer {
name: string,
title: string | undefined,
description: string | undefined,
argsSchema: AnySchema | undefined,
callback: PromptCallback<AnySchema | undefined>
argsSchema: StandardSchemaWithJSON | undefined,
callback: PromptCallback<StandardSchemaWithJSON | undefined>
): RegisteredPrompt {
// Track current schema and callback for handler regeneration
let currentArgsSchema = argsSchema;
@ -738,7 +730,7 @@ export class McpServer {
needsHandlerRegen = true;
}
if (updates.callback !== undefined) {
currentCallback = updates.callback as PromptCallback<AnySchema | undefined>;
currentCallback = updates.callback as PromptCallback<StandardSchemaWithJSON | undefined>;
needsHandlerRegen = true;
}
if (needsHandlerRegen) {
@ -772,12 +764,12 @@ export class McpServer {
name: string,
title: string | undefined,
description: string | undefined,
inputSchema: AnySchema | undefined,
outputSchema: AnySchema | undefined,
inputSchema: StandardSchemaWithJSON | undefined,
outputSchema: StandardSchemaWithJSON | undefined,
annotations: ToolAnnotations | undefined,
execution: ToolExecution | undefined,
_meta: Record<string, unknown> | undefined,
handler: AnyToolHandler<AnySchema | undefined>
handler: AnyToolHandler<StandardSchemaWithJSON | undefined>
): RegisteredTool {
// Validate tool name according to SEP specification
validateAndWarnToolName(name);
@ -818,7 +810,7 @@ export class McpServer {
}
if (updates.callback !== undefined) {
registeredTool.handler = updates.callback;
currentHandler = updates.callback as AnyToolHandler<AnySchema | undefined>;
currentHandler = updates.callback as AnyToolHandler<StandardSchemaWithJSON | undefined>;
needsExecutorRegen = true;
}
if (needsExecutorRegen) {
@ -866,7 +858,7 @@ export class McpServer {
* );
* ```
*/
registerTool<OutputArgs extends AnySchema, InputArgs extends AnySchema | undefined = undefined>(
registerTool<OutputArgs extends StandardSchemaWithJSON, InputArgs extends StandardSchemaWithJSON | undefined = undefined>(
name: string,
config: {
title?: string;
@ -893,7 +885,7 @@ export class McpServer {
annotations,
{ taskSupport: 'forbidden' },
_meta,
cb as ToolCallback<AnySchema | undefined>
cb as ToolCallback<StandardSchemaWithJSON | undefined>
);
}
@ -923,7 +915,7 @@ export class McpServer {
* );
* ```
*/
registerPrompt<Args extends AnySchema>(
registerPrompt<Args extends StandardSchemaWithJSON>(
name: string,
config: {
title?: string;
@ -943,7 +935,7 @@ export class McpServer {
title,
description,
argsSchema,
cb as PromptCallback<AnySchema | undefined>
cb as PromptCallback<StandardSchemaWithJSON | undefined>
);
this.setPromptRequestHandlers();
@ -1064,19 +1056,27 @@ export class ResourceTemplate {
}
}
export type BaseToolCallback<ResultT extends Result, Ctx extends ServerContext, Args extends AnySchema | undefined> = Args extends AnySchema
? (args: SchemaOutput<Args>, ctx: Ctx) => ResultT | Promise<ResultT>
: (ctx: Ctx) => ResultT | Promise<ResultT>;
export type BaseToolCallback<
SendResultT extends Result,
Ctx extends ServerContext,
Args extends StandardSchemaWithJSON | undefined
> = Args extends StandardSchemaWithJSON
? (args: StandardSchemaWithJSON.InferOutput<Args>, ctx: Ctx) => SendResultT | Promise<SendResultT>
: (ctx: Ctx) => SendResultT | Promise<SendResultT>;
/**
* Callback for a tool handler registered with {@linkcode McpServer.registerTool}.
*/
export type ToolCallback<Args extends AnySchema | undefined = undefined> = BaseToolCallback<CallToolResult, ServerContext, Args>;
export type ToolCallback<Args extends StandardSchemaWithJSON | undefined = undefined> = BaseToolCallback<
CallToolResult,
ServerContext,
Args
>;
/**
* Supertype that can handle both regular tools (simple callback) and task-based tools (task handler object).
*/
export type AnyToolHandler<Args extends AnySchema | undefined = undefined> = ToolCallback<Args> | ToolTaskHandler<Args>;
export type AnyToolHandler<Args extends StandardSchemaWithJSON | undefined = undefined> = ToolCallback<Args> | ToolTaskHandler<Args>;
/**
* Internal executor type that encapsulates handler invocation with proper types.
@ -1086,12 +1086,12 @@ type ToolExecutor = (args: unknown, ctx: ServerContext) => Promise<CallToolResul
export type RegisteredTool = {
title?: string;
description?: string;
inputSchema?: AnySchema;
outputSchema?: AnySchema;
inputSchema?: StandardSchemaWithJSON;
outputSchema?: StandardSchemaWithJSON;
annotations?: ToolAnnotations;
execution?: ToolExecution;
_meta?: Record<string, unknown>;
handler: AnyToolHandler<AnySchema | undefined>;
handler: AnyToolHandler<StandardSchemaWithJSON | undefined>;
/** @hidden */
executor: ToolExecutor;
enabled: boolean;
@ -1101,11 +1101,11 @@ export type RegisteredTool = {
name?: string | null;
title?: string;
description?: string;
paramsSchema?: AnySchema;
outputSchema?: AnySchema;
paramsSchema?: StandardSchemaWithJSON;
outputSchema?: StandardSchemaWithJSON;
annotations?: ToolAnnotations;
_meta?: Record<string, unknown>;
callback?: ToolCallback<AnySchema>;
callback?: ToolCallback<StandardSchemaWithJSON>;
enabled?: boolean;
}): void;
remove(): void;
@ -1116,7 +1116,10 @@ export type RegisteredTool = {
* When `inputSchema` is defined, the handler is called with `(args, ctx)`.
* When `inputSchema` is undefined, the handler is called with just `(ctx)`.
*/
function createToolExecutor(inputSchema: AnySchema | undefined, handler: AnyToolHandler<AnySchema | undefined>): ToolExecutor {
function createToolExecutor(
inputSchema: StandardSchemaWithJSON | undefined,
handler: AnyToolHandler<StandardSchemaWithJSON | undefined>
): ToolExecutor {
const isTaskHandler = 'createTask' in handler;
if (isTaskHandler) {
@ -1211,8 +1214,8 @@ export type RegisteredResourceTemplate = {
remove(): void;
};
export type PromptCallback<Args extends AnySchema | undefined = undefined> = Args extends AnySchema
? (args: SchemaOutput<Args>, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>
export type PromptCallback<Args extends StandardSchemaWithJSON | undefined = undefined> = Args extends StandardSchemaWithJSON
? (args: StandardSchemaWithJSON.InferOutput<Args>, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>
: (ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
/**
@ -1230,13 +1233,13 @@ type TaskHandlerInternal = {
export type RegisteredPrompt = {
title?: string;
description?: string;
argsSchema?: AnySchema;
argsSchema?: StandardSchemaWithJSON;
/** @hidden */
handler: PromptHandler;
enabled: boolean;
enable(): void;
disable(): void;
update<Args extends AnySchema>(updates: {
update<Args extends StandardSchemaWithJSON>(updates: {
name?: string | null;
title?: string;
description?: string;
@ -1253,19 +1256,18 @@ export type RegisteredPrompt = {
*/
function createPromptHandler(
name: string,
argsSchema: AnySchema | undefined,
callback: PromptCallback<AnySchema | undefined>
argsSchema: StandardSchemaWithJSON | undefined,
callback: PromptCallback<StandardSchemaWithJSON | undefined>
): PromptHandler {
if (argsSchema) {
const typedCallback = callback as (args: SchemaOutput<AnySchema>, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
const typedCallback = callback as (args: unknown, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
return async (args, ctx) => {
const parseResult = await parseSchemaAsync(argsSchema, args);
const parseResult = await validateStandardSchema(argsSchema, args);
if (!parseResult.success) {
const errorMessage = parseResult.error.issues.map((i: { message: string }) => i.message).join(', ');
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for prompt ${name}: ${errorMessage}`);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for prompt ${name}: ${parseResult.error}`);
}
return typedCallback(parseResult.data as SchemaOutput<AnySchema>, ctx);
return typedCallback(parseResult.data, ctx);
};
} else {
const typedCallback = callback as (ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
@ -1276,18 +1278,6 @@ function createPromptHandler(
}
}
function promptArgumentsFromSchema(schema: AnySchema): PromptArgument[] {
const shape = getSchemaShape(schema);
if (!shape) return [];
return Object.entries(shape).map(([name, field]): PromptArgument => {
return {
name,
description: getSchemaDescription(field),
required: !isOptionalSchema(field)
};
});
}
function createCompletionResult(suggestions: readonly unknown[]): CompleteResult {
const values = suggestions.map(String).slice(0, 100);
return {
@ -1305,3 +1295,27 @@ const EMPTY_COMPLETION_RESULT: CompleteResult = {
hasMore: false
}
};
/** @internal Gets the shape of a Zod object schema */
function getSchemaShape(schema: unknown): Record<string, unknown> | undefined {
const candidate = schema as { shape?: unknown };
if (candidate.shape && typeof candidate.shape === 'object') {
return candidate.shape as Record<string, unknown>;
}
return undefined;
}
/** @internal Checks if a Zod schema is optional */
function isOptionalSchema(schema: unknown): boolean {
const candidate = schema as { type?: string } | null | undefined;
return candidate?.type === 'optional';
}
/** @internal Unwraps an optional Zod schema */
function unwrapOptionalSchema(schema: unknown): unknown {
if (!isOptionalSchema(schema)) {
return schema;
}
const candidate = schema as { def?: { innerType?: unknown } };
return candidate.def?.innerType ?? schema;
}