/**
* Convert Zod schema to JSON Schema format for MCP SDK
*/
import { z, ZodType } from 'zod';
type JSONSchema = {
type?: string | string[];
properties?: Record<string, JSONSchema>;
required?: string[];
enum?: string[];
items?: JSONSchema;
description?: string;
default?: unknown;
anyOf?: JSONSchema[];
oneOf?: JSONSchema[];
[key: string]: unknown;
};
/**
* Extract description from a Zod schema (handles Zod v4 and v3)
*/
function getDescription(schema: ZodType<any>): string | undefined {
// Zod v4 structure
if ((schema as any)._zod?.def?.description) {
return (schema as any)._zod.def.description;
}
// Zod v3 structure
if ((schema as any)._def?.description) {
return (schema as any)._def.description;
}
// Direct description property
if ((schema as any).description) {
return (schema as any).description;
}
return undefined;
}
/**
* Get the inner type from optional/default wrappers
*/
function unwrapSchema(schema: ZodType<any>): ZodType<any> {
const zodDef = (schema as any)._zod?.def || (schema as any)._def || {};
const typeName = zodDef.type || zodDef.typeName;
// Unwrap optional, default, and effects
if (['optional', 'default', 'effects', 'transform'].includes(typeName)) {
const inner = zodDef.type || zodDef.innerType || zodDef.schema;
if (inner) {
return unwrapSchema(inner);
}
}
return schema;
}
/**
* Check if a schema is optional
*/
function isOptional(schema: ZodType<any>): boolean {
const zodDef = (schema as any)._zod?.def || (schema as any)._def || {};
const typeName = zodDef.type || zodDef.typeName;
return typeName === 'optional';
}
/**
* Convert a Zod schema to JSON Schema format
*/
export function zodToJsonSchema(schema: ZodType<any>): Record<string, unknown> {
const result = convertZodSchema(schema);
// Add top-level description if present
const description = getDescription(schema);
if (description) {
result.description = description;
}
return result as Record<string, unknown>;
}
function convertZodSchema(schema: ZodType<any>): JSONSchema {
const zodDef = (schema as any)._zod?.def || (schema as any)._def || {};
const typeName = zodDef.type || zodDef.typeName;
// ZodString
if (typeName === 'string') {
const result: JSONSchema = { type: 'string' };
const desc = getDescription(schema);
if (desc) result.description = desc;
return result;
}
// ZodNumber
if (typeName === 'number' || typeName === 'int') {
const result: JSONSchema = { type: 'number' };
const desc = getDescription(schema);
if (desc) result.description = desc;
return result;
}
// ZodBoolean
if (typeName === 'boolean') {
const result: JSONSchema = { type: 'boolean' };
const desc = getDescription(schema);
if (desc) result.description = desc;
return result;
}
// ZodArray
if (typeName === 'array') {
const elementType = zodDef.type || zodDef.element;
const result: JSONSchema = {
type: 'array',
items: elementType ? convertZodSchema(elementType) : {},
};
const desc = getDescription(schema);
if (desc) result.description = desc;
return result;
}
// ZodEnum
if (typeName === 'enum') {
const values = zodDef.values || [];
const result: JSONSchema = {
type: 'string',
enum: Array.isArray(values) ? values : Object.values(values),
};
const desc = getDescription(schema);
if (desc) result.description = desc;
return result;
}
// ZodOptional
if (typeName === 'optional') {
const innerType = zodDef.type || zodDef.innerType;
if (innerType) {
const result = convertZodSchema(innerType);
return result;
}
return {};
}
// ZodDefault
if (typeName === 'default') {
const innerType = zodDef.type || zodDef.innerType;
const defaultValue = zodDef.defaultValue;
const result = innerType ? convertZodSchema(innerType) : {};
if (defaultValue !== undefined) {
result.default = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
}
const desc = getDescription(schema);
if (desc) result.description = desc;
return result;
}
// ZodUnion
if (typeName === 'union') {
const options = zodDef.options || zodDef.members || [];
const result: JSONSchema = {
anyOf: options.map((opt: ZodType<any>) => convertZodSchema(opt)),
};
const desc = getDescription(schema);
if (desc) result.description = desc;
return result;
}
// ZodObject - This is the critical one for tool parameters
if (typeName === 'object') {
const properties: Record<string, JSONSchema> = {};
const required: string[] = [];
// Handle shape - Zod v4 stores shape differently
const shape = zodDef.shape || zodDef._shape || {};
for (const [key, value] of Object.entries(shape)) {
const fieldType = value as ZodType<any>;
// Convert the field schema
properties[key] = convertZodSchema(fieldType);
// Check if field is required (not optional)
if (!isOptional(fieldType)) {
required.push(key);
}
}
const result: JSONSchema = {
type: 'object',
properties,
};
// Only add required if there are required fields
if (required.length > 0) {
result.required = required;
}
// Add description if present
const desc = getDescription(schema);
if (desc) result.description = desc;
return result;
}
// ZodEffects (for refined schemas)
if (typeName === 'effects' || typeName === 'transform') {
const innerType = zodDef.schema || zodDef.type || zodDef.innerType;
if (innerType) {
return convertZodSchema(innerType);
}
return {};
}
// Unknown/any/never types
if (typeName === 'unknown' || typeName === 'any' || typeName === 'never') {
return {};
}
// Fallback - try to get description at least
const result: JSONSchema = {};
const desc = getDescription(schema);
if (desc) result.description = desc;
return result;
}