Skip to content

Commit 62a86f5

Browse files
Royal-lobsterclaude
andcommitted
fix: Update generate-tools.mjs for Zod v4 compatibility
- Add native Zod v4 schema extraction (zod-to-json-schema only supports v3) - Extract properties from _zod.def.shape structure - Handle default values and optional flags correctly Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e5b2771 commit 62a86f5

File tree

3 files changed

+334
-65
lines changed

3 files changed

+334
-65
lines changed

.github/actions/generate-mcp-tools/generate-tools.mjs

Lines changed: 144 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import { fileURLToPath } from "node:url";
4-
import { zodToJsonSchema } from "zod-to-json-schema";
54

65
const __dirname = path.dirname(fileURLToPath(import.meta.url));
76
const ROOT = path.resolve(__dirname, "../../..");
@@ -11,6 +10,143 @@ const TOOLS_DIR = path.join(ROOT, "src", "tools");
1110
const START = "<!-- AUTO-GENERATED TOOLS START -->";
1211
const END = "<!-- AUTO-GENERATED TOOLS END -->";
1312

13+
/**
14+
* Check if a schema is a Zod v4 schema
15+
*/
16+
function isZodV4(schema) {
17+
return schema && typeof schema === "object" && schema._zod?.def?.type;
18+
}
19+
20+
/**
21+
* Check if a schema is a Zod v3 schema
22+
*/
23+
function isZodV3(schema) {
24+
return (
25+
schema &&
26+
typeof schema === "object" &&
27+
typeof schema.safeParse === "function" &&
28+
!isZodV4(schema)
29+
);
30+
}
31+
32+
/**
33+
* Extract the base type from a Zod v4 schema property
34+
*/
35+
function extractZodV4BaseType(prop) {
36+
if (!prop) return { type: "unknown" };
37+
38+
const def = prop._zod?.def || prop.def;
39+
if (!def) {
40+
// Check if it's a direct type
41+
if (prop.type) return { type: prop.type };
42+
return { type: "unknown" };
43+
}
44+
45+
const wrapperType = def.type;
46+
47+
// Unwrap default/optional wrappers
48+
if (wrapperType === "default" || wrapperType === "optional") {
49+
const inner = def.innerType;
50+
const baseInfo = extractZodV4BaseType(inner);
51+
52+
if (wrapperType === "default") {
53+
baseInfo.default = def.defaultValue;
54+
}
55+
if (wrapperType === "optional") {
56+
baseInfo.optional = true;
57+
}
58+
return baseInfo;
59+
}
60+
61+
// Handle base types
62+
switch (wrapperType) {
63+
case "string":
64+
return { type: "string" };
65+
case "number":
66+
return { type: "number" };
67+
case "boolean":
68+
return { type: "boolean" };
69+
case "enum":
70+
return {
71+
type: "string",
72+
enum: def.entries ? Object.keys(def.entries) : [],
73+
};
74+
case "array":
75+
return { type: "array" };
76+
case "object":
77+
return { type: "object" };
78+
case "union":
79+
return { type: "union" };
80+
default:
81+
return { type: wrapperType || "unknown" };
82+
}
83+
}
84+
85+
/**
86+
* Convert a Zod v4 schema to JSON Schema format
87+
*/
88+
function zodV4ToJsonSchema(schema) {
89+
const def = schema._zod?.def;
90+
if (!def || def.type !== "object") {
91+
return { properties: {}, required: [] };
92+
}
93+
94+
const shape = def.shape || {};
95+
const properties = {};
96+
const required = [];
97+
98+
for (const [key, prop] of Object.entries(shape)) {
99+
const typeInfo = extractZodV4BaseType(prop);
100+
const description = prop.description || "";
101+
102+
properties[key] = {
103+
type: typeInfo.type,
104+
description,
105+
};
106+
107+
if (typeInfo.enum) {
108+
properties[key].enum = typeInfo.enum;
109+
}
110+
111+
if (typeInfo.default !== undefined) {
112+
properties[key].default = typeInfo.default;
113+
}
114+
115+
// If not optional and no default, it's required
116+
if (!typeInfo.optional && typeInfo.default === undefined) {
117+
required.push(key);
118+
}
119+
}
120+
121+
return { properties, required };
122+
}
123+
124+
/**
125+
* Convert any Zod schema to JSON Schema format
126+
*/
127+
async function zodToJsonSchema(schema) {
128+
// Handle Zod v4
129+
if (isZodV4(schema)) {
130+
return zodV4ToJsonSchema(schema);
131+
}
132+
133+
// Handle Zod v3 - dynamically import zod-to-json-schema
134+
if (isZodV3(schema)) {
135+
try {
136+
const { zodToJsonSchema: zodV3ToJsonSchema } = await import(
137+
"zod-to-json-schema"
138+
);
139+
return zodV3ToJsonSchema(schema);
140+
} catch (err) {
141+
console.warn("Warning: Could not use zod-to-json-schema:", err.message);
142+
return { properties: {}, required: [] };
143+
}
144+
}
145+
146+
// Already JSON Schema or unknown format
147+
return schema;
148+
}
149+
14150
/**
15151
* Check if value is an MCP tool object
16152
*/
@@ -94,14 +230,12 @@ async function loadTools() {
94230
return tools.sort((a, b) => a.name.localeCompare(b.name));
95231
}
96232

97-
function renderSchema(schema) {
233+
async function renderSchema(schema) {
98234
if (!schema) {
99235
return "_No parameters_";
100236
}
101237

102-
// If this is a Zod schema, convert it to JSON Schema
103-
const jsonSchema =
104-
typeof schema.safeParse === "function" ? zodToJsonSchema(schema) : schema;
238+
const jsonSchema = await zodToJsonSchema(schema);
105239

106240
const properties = jsonSchema.properties ?? {};
107241
const required = new Set(jsonSchema.required ?? []);
@@ -150,27 +284,25 @@ function renderSchema(schema) {
150284
return table.trim();
151285
}
152286

153-
function renderMarkdown(tools) {
287+
async function renderMarkdown(tools) {
154288
let md = "";
155289

156290
for (const tool of tools) {
157291
const schema = tool.parameters || tool.schema;
158292

159293
md += `### \`${tool.name}\`\n`;
160294
md += `${tool.description}\n\n`;
161-
md += `${renderSchema(schema)}\n\n`;
295+
md += `${await renderSchema(schema)}\n\n`;
162296
}
163297

164298
return md.trim();
165299
}
166300

167-
function updateReadme({ readme, tools }) {
301+
function updateReadme({ readme, toolsMd }) {
168302
if (!readme.includes(START) || !readme.includes(END)) {
169303
throw new Error("README missing AUTO-GENERATED TOOLS markers");
170304
}
171305

172-
const toolsMd = renderMarkdown(tools);
173-
174306
return readme.replace(
175307
new RegExp(`${START}[\\s\\S]*?${END}`, "m"),
176308
`${START}\n\n${toolsMd}\n\n${END}`,
@@ -186,7 +318,8 @@ async function main() {
186318
console.warn("Warning: No tools found!");
187319
}
188320

189-
const updated = updateReadme({ readme, tools });
321+
const toolsMd = await renderMarkdown(tools);
322+
const updated = updateReadme({ readme, toolsMd });
190323

191324
fs.writeFileSync(README_PATH, updated);
192325
console.log(`Synced ${tools.length} MCP tools to README.md`);

0 commit comments

Comments
 (0)