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
11 changed files with 1327 additions and 212 deletions

View file

@ -6,20 +6,19 @@
*/
import type {
AnyObjectSchema,
CallToolRequest,
CallToolResult,
CancelTaskResult,
CreateTaskResult,
GetTaskPayloadResult,
GetTaskResult,
ListTasksResult,
RequestMethod,
RequestOptions,
ResponseMessage,
ResultTypeMap,
SchemaOutput
ResultTypeMap
} from '@modelcontextprotocol/core';
import { ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
import { GetTaskPayloadResultSchema, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
import type { Client } from '../../client/client.js';
@ -185,23 +184,22 @@ export class ExperimentalClientTasks {
* 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 AnyObjectSchema>(taskId: string, resultSchema?: T, options?: RequestOptions): Promise<SchemaOutput<T>> {
// Delegate to the client's underlying Protocol method
async getTaskResult(taskId: string, options?: RequestOptions): Promise<GetTaskPayloadResult> {
return (
this._client as unknown as {
getTaskResult: <U extends AnyObjectSchema>(
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

@ -12,11 +12,13 @@ export * from './shared/uriTemplate.js';
export * from './types/types.js';
export * from './util/inMemory.js';
export * from './util/schema.js';
export * from './util/standardSchema.js';
// experimental exports
export * from './experimental/index.js';
export * from './validators/ajvProvider.js';
export * from './validators/cfWorkerProvider.js';
export * from './validators/fromJsonSchema.js';
/**
* JSON Schema validation
*

View file

@ -1,34 +1,25 @@
/**
* Internal Zod schema utilities for protocol handling.
* These are used internally by the SDK for protocol message validation.
*/
import * as z from 'zod/v4';
/**
* Base type for any Zod schema.
* This is the canonical type to use when accepting user-provided schemas.
*/
export type AnySchema = z.core.$ZodType;
/**
* A Zod schema for objects specifically (not unions).
* Use this when you need to constrain to ZodObject schemas.
* A Zod schema for objects specifically.
*/
export type AnyObjectSchema = z.core.$ZodObject;
/**
* Extracts the input type from a Zod schema.
*/
export type SchemaInput<T extends AnySchema> = z.input<T>;
/**
* Extracts the output type from a Zod schema.
*/
export type SchemaOutput<T extends AnySchema> = z.output<T>;
/**
* Converts a Zod schema to JSON Schema.
*/
export function schemaToJson(schema: AnySchema, options?: { io?: 'input' | 'output' }): Record<string, unknown> {
return z.toJSONSchema(schema, options) as Record<string, unknown>;
}
/**
* Parses data against a Zod schema (synchronous).
* Returns a discriminated union with success/error.
@ -39,56 +30,3 @@ export function parseSchema<T extends AnySchema>(
): { success: true; data: z.output<T> } | { success: false; error: z.core.$ZodError } {
return z.safeParse(schema, data);
}
/**
* Parses data against a Zod schema (asynchronous).
* Returns a discriminated union with success/error.
*/
export function parseSchemaAsync<T extends AnySchema>(
schema: T,
data: unknown
): Promise<{ success: true; data: z.output<T> } | { success: false; error: z.core.$ZodError }> {
return z.safeParseAsync(schema, data);
}
/**
* Gets the shape of an object schema.
* Returns undefined if the schema is not an object schema.
*/
export function getSchemaShape(schema: AnySchema): Record<string, AnySchema> | undefined {
const candidate = schema as { shape?: unknown };
if (candidate.shape && typeof candidate.shape === 'object') {
return candidate.shape as Record<string, AnySchema>;
}
return undefined;
}
/**
* Gets the description from a schema if it has one.
*/
export function getSchemaDescription(schema: AnySchema): string | undefined {
const candidate = schema as { description?: string };
return candidate.description;
}
/**
* Checks if a schema is optional (accepts undefined).
* Uses the public .type property which works in both zod/v4 and zod/v4/mini.
*/
export function isOptionalSchema(schema: AnySchema): boolean {
const candidate = schema as { type?: string };
return candidate.type === 'optional';
}
/**
* Unwraps an optional schema to get the inner schema.
* If the schema is not optional, returns it unchanged.
* Uses the public .def.innerType property which works in both zod/v4 and zod/v4/mini.
*/
export function unwrapOptionalSchema(schema: AnySchema): AnySchema {
if (!isOptionalSchema(schema)) {
return schema;
}
const candidate = schema as { def?: { innerType?: AnySchema } };
return candidate.def?.innerType ?? schema;
}

View file

@ -0,0 +1,180 @@
/**
* Standard Schema utilities for user-provided schemas.
* Supports Zod v4, Valibot, ArkType, and other Standard Schema implementations.
* @see https://standardschema.dev
*/
/* eslint-disable @typescript-eslint/no-namespace */
// Standard Schema interfaces — vendored from https://standardschema.dev (spec v1, Jan 2025)
export interface StandardTypedV1<Input = unknown, Output = Input> {
readonly '~standard': StandardTypedV1.Props<Input, Output>;
}
export namespace StandardTypedV1 {
export interface Props<Input = unknown, Output = Input> {
readonly version: 1;
readonly vendor: string;
readonly types?: Types<Input, Output> | undefined;
}
export interface Types<Input = unknown, Output = Input> {
readonly input: Input;
readonly output: Output;
}
export type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema['~standard']['types']>['input'];
export type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema['~standard']['types']>['output'];
}
export interface StandardSchemaV1<Input = unknown, Output = Input> {
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
}
export namespace StandardSchemaV1 {
export interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
readonly validate: (value: unknown, options?: Options | undefined) => Result<Output> | Promise<Result<Output>>;
}
export interface Options {
readonly libraryOptions?: Record<string, unknown> | undefined;
}
export type Result<Output> = SuccessResult<Output> | FailureResult;
export interface SuccessResult<Output> {
readonly value: Output;
readonly issues?: undefined;
}
export interface FailureResult {
readonly issues: ReadonlyArray<Issue>;
}
export interface Issue {
readonly message: string;
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
}
export interface PathSegment {
readonly key: PropertyKey;
}
export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}
export interface StandardJSONSchemaV1<Input = unknown, Output = Input> {
readonly '~standard': StandardJSONSchemaV1.Props<Input, Output>;
}
export namespace StandardJSONSchemaV1 {
export interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
readonly jsonSchema: Converter;
}
export interface Converter {
readonly input: (options: Options) => Record<string, unknown>;
readonly output: (options: Options) => Record<string, unknown>;
}
export type Target = 'draft-2020-12' | 'draft-07' | 'openapi-3.0' | (object & string);
export interface Options {
readonly target: Target;
readonly libraryOptions?: Record<string, unknown> | undefined;
}
export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}
/**
* Combined interface for schemas with both validation and JSON Schema conversion —
* the intersection of {@linkcode StandardSchemaV1} and {@linkcode StandardJSONSchemaV1}.
*
* This is the type accepted by `registerTool` / `registerPrompt`. The SDK needs
* `~standard.jsonSchema` to advertise the tool's argument shape in `tools/list`, and
* `~standard.validate` to check incoming arguments when a `tools/call` arrives.
*
* Zod v4, ArkType, and Valibot (via `@valibot/to-json-schema`'s `toStandardJsonSchema`)
* all implement both interfaces.
*
* @see https://standardschema.dev/ for the Standard Schema specification
*/
export interface StandardSchemaWithJSON<Input = unknown, Output = Input> {
readonly '~standard': StandardSchemaV1.Props<Input, Output> & StandardJSONSchemaV1.Props<Input, Output>;
}
export namespace StandardSchemaWithJSON {
export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}
// Type guards
export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 {
if (schema == null) return false;
const schemaType = typeof schema;
if (schemaType !== 'object' && schemaType !== 'function') return false;
if (!('~standard' in (schema as object))) return false;
const std = (schema as StandardJSONSchemaV1)['~standard'];
return typeof std?.jsonSchema?.input === 'function' && typeof std?.jsonSchema?.output === 'function';
}
export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
if (schema == null) return false;
const schemaType = typeof schema;
if (schemaType !== 'object' && schemaType !== 'function') return false;
if (!('~standard' in (schema as object))) return false;
const std = (schema as StandardSchemaV1)['~standard'];
return typeof std?.validate === 'function';
}
export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSchemaWithJSON {
return isStandardJSONSchema(schema) && isStandardSchema(schema);
}
// JSON Schema conversion
export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'input' | 'output' = 'input'): Record<string, unknown> {
return schema['~standard'].jsonSchema[io]({ target: 'draft-2020-12' });
}
// Validation
export type StandardSchemaValidationResult<T> = { success: true; data: T } | { success: false; error: string };
function formatIssue(issue: StandardSchemaV1.Issue): string {
if (!issue.path?.length) return issue.message;
const path = issue.path.map(p => String(typeof p === 'object' ? p.key : p)).join('.');
return `${path}: ${issue.message}`;
}
export async function validateStandardSchema<T extends StandardSchemaWithJSON>(
schema: T,
data: unknown
): Promise<StandardSchemaValidationResult<StandardSchemaWithJSON.InferOutput<T>>> {
const result = await schema['~standard'].validate(data);
if (result.issues && result.issues.length > 0) {
return { success: false, error: result.issues.map(i => formatIssue(i)).join(', ') };
}
return { success: true, data: (result as StandardSchemaV1.SuccessResult<unknown>).value as StandardSchemaWithJSON.InferOutput<T> };
}
// Prompt argument extraction
export function promptArgumentsFromStandardSchema(
schema: StandardJSONSchemaV1
): Array<{ name: string; description?: string; required: boolean }> {
const jsonSchema = standardSchemaToJsonSchema(schema, 'input');
const properties = (jsonSchema.properties as Record<string, { description?: string }>) || {};
const required = (jsonSchema.required as string[]) || [];
return Object.entries(properties).map(([name, prop]) => ({
name,
description: prop?.description,
required: required.includes(name)
}));
}

View file

@ -0,0 +1,24 @@
/**
* Type-checked examples for `fromJsonSchema.ts`.
*
* These examples are synced into JSDoc comments via the sync-snippets script.
*
* @module
*/
import { AjvJsonSchemaValidator } from './ajvProvider.js';
import { fromJsonSchema } from './fromJsonSchema.js';
/**
* Example: wrap a raw JSON Schema object for use with registerTool.
*/
function fromJsonSchema_basicUsage() {
//#region fromJsonSchema_basicUsage
const inputSchema = fromJsonSchema<{ name: string }>(
{ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
new AjvJsonSchemaValidator()
);
// Use with server.registerTool('greet', { inputSchema }, handler)
//#endregion fromJsonSchema_basicUsage
return inputSchema;
}

View file

@ -0,0 +1,38 @@
import type { StandardSchemaV1, StandardSchemaWithJSON } from '../util/standardSchema.js';
import type { JsonSchemaType, jsonSchemaValidator } from './types.js';
/**
* Wrap a raw JSON Schema object as a {@linkcode StandardSchemaWithJSON} so it can be
* passed to `registerTool` / `registerPrompt`. Use this when you already have JSON
* Schema (e.g. from TypeBox, or hand-written) and want to register it without going
* through a Standard Schema library.
*
* The callback arguments will be typed `unknown` (raw JSON Schema has no TypeScript
* types attached). Cast at the call site, or use the generic `fromJsonSchema<MyType>(...)`.
*
* @example
* ```ts source="./fromJsonSchema.examples.ts#fromJsonSchema_basicUsage"
* const inputSchema = fromJsonSchema<{ name: string }>(
* { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
* new AjvJsonSchemaValidator()
* );
* // Use with server.registerTool('greet', { inputSchema }, handler)
* ```
*/
export function fromJsonSchema<T = unknown>(schema: JsonSchemaType, validator: jsonSchemaValidator): StandardSchemaWithJSON<T, T> {
const check = validator.getValidator<T>(schema);
return {
'~standard': {
version: 1,
vendor: 'mcp',
jsonSchema: {
input: () => schema as Record<string, unknown>,
output: () => schema as Record<string, unknown>
},
validate: (data: unknown): StandardSchemaV1.Result<T> => {
const result = check(data);
return result.valid ? { value: result.data } : { issues: [{ message: result.errorMessage }] };
}
}
};
}

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;
}