Description
Zod transform functions are lost during JSON Schema conversion, breaking union types
Description
When using Zod schemas with the MCP SDK, transform functions are stripped during the conversion to JSON Schema. This causes union types like z.union([z.array(z.string()), z.string()]).transform(parseJsonStringArray)
to lose the string option, making it impossible for MCP clients to send JSON-serialized arrays.
Current Behavior
When a Zod schema is defined as:
const schema = z.union([
z.array(z.string()),
z.string()
]).transform(parseJsonStringArray);
The MCP SDK converts this to JSON Schema using zodToJsonSchema
, which strips the transform and results in:
{
"type": "array",
"items": { "type": "string" }
}
The string option is lost, so when clients (like Claude) send '["file1.ts", "file2.ts"]'
, they get a validation error: "Expected array, received string".
Expected Behavior
MCP clients should be able to see both options in the schema and send either:
- Native arrays:
["file1.ts", "file2.ts"]
- JSON strings:
'["file1.ts", "file2.ts"]'
Root Cause
In src/server/mcp.ts
lines 120-122:
zodSchema: zodToJsonSchema(tool.inputSchema, {
strictUnions: true,
}),
The zod-to-json-schema
library explicitly cannot preserve transforms because JSON Schema has no way to represent transformation functions.
Proposed Solutions
Solution 1: Add preprocessing hooks
Allow tools to define preprocessors that run before validation:
mcpServer.tool('myTool', 'Description', schema, {
preprocess: (params) => {
// Transform JSON strings to arrays
for (const [key, value] of Object.entries(params)) {
if (typeof value === 'string' && value.startsWith('[')) {
try {
params[key] = JSON.parse(value);
} catch {}
}
}
return params;
}
}, handler);
Solution 2: Support custom validation
Allow tools to provide their own validation function:
mcpServer.tool('myTool', 'Description', schema, {
validate: (params) => {
// Use Zod directly, preserving transforms
return schema.parse(params);
}
}, handler);
Solution 3: Preserve original Zod schema
Include both the original Zod schema and JSON Schema in the protocol:
{
"inputSchema": {
"type": "zod",
"zodSchema": /* serialized Zod schema */,
"jsonSchema": /* current JSON Schema for compatibility */
}
}
Impact
This issue affects any MCP server that needs to accept parameters that can be either arrays or JSON-serialized arrays, which is common when:
- Clients have different serialization capabilities
- Parameters are passed through various systems
- Maintaining backward compatibility
Reproduction
- Create an MCP server with a Zod schema using transform on a union type
- Try to invoke the tool with a JSON string array
- Observe validation error
Example Use Case
In the SonarQube MCP server, we need to accept file filters as either:
component_keys: ["src/file1.ts", "src/file2.ts"]
component_keys: '["src/file1.ts", "src/file2.ts"]'
Currently, we have to work around this by:
- Removing transforms from schemas (losing type safety)
- Manually parsing JSON strings in handlers (error-prone and repetitive)
Related Issues
- zod-to-json-schema limitation: https://github.com/StefanTerdell/zod-to-json-schema#limitations