|
| 1 | +/** |
| 2 | + * MCP Schema Generation Script |
| 3 | + * |
| 4 | + * Generates Zod schemas from the vendored MCP spec types (src/vendor/mcp-types.ts) |
| 5 | + * using ts-to-zod. The output (src/generated/mcp-schemas.ts) provides schemas like |
| 6 | + * ContentBlockSchema, CallToolResultSchema, ToolSchema, etc. that are consumed by |
| 7 | + * the main schema generator (generate-schemas.ts) and the rest of the SDK. |
| 8 | + * |
| 9 | + * Post-processing: |
| 10 | + * - Rewrites `import { z } from "zod"` to `import { z } from "zod/v4"` |
| 11 | + * - Replaces `z.record().and(z.object({...}))` with `z.object({...}).passthrough()` |
| 12 | + * - Adds a header comment noting the source |
| 13 | + * |
| 14 | + * @see scripts/generate-schemas.ts |
| 15 | + */ |
| 16 | + |
| 17 | +import { readFileSync, writeFileSync } from "node:fs"; |
| 18 | +import { dirname, join } from "node:path"; |
| 19 | +import { fileURLToPath } from "node:url"; |
| 20 | +import { generate } from "ts-to-zod"; |
| 21 | + |
| 22 | +const __filename = fileURLToPath(import.meta.url); |
| 23 | +const __dirname = dirname(__filename); |
| 24 | +const PROJECT_ROOT = join(__dirname, ".."); |
| 25 | + |
| 26 | +const MCP_TYPES_FILE = join(PROJECT_ROOT, "src", "vendor", "mcp-types.ts"); |
| 27 | +const GENERATED_DIR = join(PROJECT_ROOT, "src", "generated"); |
| 28 | +const OUTPUT_FILE = join(GENERATED_DIR, "mcp-schemas.ts"); |
| 29 | + |
| 30 | +function main() { |
| 31 | + console.log("Generating MCP Zod schemas from vendor/mcp-types.ts...\n"); |
| 32 | + |
| 33 | + const sourceText = readFileSync(MCP_TYPES_FILE, "utf-8"); |
| 34 | + |
| 35 | + const result = generate({ |
| 36 | + sourceText, |
| 37 | + keepComments: true, |
| 38 | + skipParseJSDoc: false, |
| 39 | + getSchemaName: (typeName: string) => `${typeName}Schema`, |
| 40 | + }); |
| 41 | + |
| 42 | + if (result.errors.length > 0) { |
| 43 | + console.error("Generation errors:"); |
| 44 | + for (const error of result.errors) { |
| 45 | + console.error(` - ${error}`); |
| 46 | + } |
| 47 | + process.exit(1); |
| 48 | + } |
| 49 | + |
| 50 | + if (result.hasCircularDependencies) { |
| 51 | + console.warn("Warning: Circular dependencies detected in MCP types"); |
| 52 | + } |
| 53 | + |
| 54 | + let content = result.getZodSchemasFile("../vendor/mcp-types.js"); |
| 55 | + content = postProcess(content); |
| 56 | + |
| 57 | + writeFileSync(OUTPUT_FILE, content, "utf-8"); |
| 58 | + console.log(`Written: ${OUTPUT_FILE}`); |
| 59 | + console.log("\nMCP schema generation complete!"); |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Post-process generated schemas for project compatibility. |
| 64 | + */ |
| 65 | +function postProcess(content: string): string { |
| 66 | + // 1. Rewrite zod import to zod/v4 |
| 67 | + content = content.replace( |
| 68 | + 'import { z } from "zod";', |
| 69 | + 'import { z } from "zod/v4";', |
| 70 | + ); |
| 71 | + |
| 72 | + // 2. Replace z.record().and(z.object({...})) with z.object({...}).passthrough() |
| 73 | + content = replaceRecordAndWithPassthrough(content); |
| 74 | + |
| 75 | + // 3. Add header comment |
| 76 | + content = content.replace( |
| 77 | + "// Generated by ts-to-zod", |
| 78 | + `// Generated by ts-to-zod from src/vendor/mcp-types.ts |
| 79 | +// Source: MCP spec schema/2025-11-25/schema.ts |
| 80 | +// Run: npm run generate:mcp-schemas`, |
| 81 | + ); |
| 82 | + |
| 83 | + return content; |
| 84 | +} |
| 85 | + |
| 86 | +/** |
| 87 | + * Replace z.record(z.string(), z.unknown()).and(z.object({...})) with z.object({...}).passthrough() |
| 88 | + * Uses brace-counting to handle nested objects correctly. |
| 89 | + * passthrough() works in both Zod v3 and v4, allowing extra properties. |
| 90 | + */ |
| 91 | +function replaceRecordAndWithPassthrough(content: string): string { |
| 92 | + const pattern = "z.record(z.string(), z.unknown()).and(z.object({"; |
| 93 | + let result = content; |
| 94 | + let startIndex = 0; |
| 95 | + |
| 96 | + while (true) { |
| 97 | + const matchStart = result.indexOf(pattern, startIndex); |
| 98 | + if (matchStart === -1) break; |
| 99 | + |
| 100 | + // Find the matching closing brace for z.object({ |
| 101 | + const objectStart = matchStart + pattern.length; |
| 102 | + let braceCount = 1; |
| 103 | + let i = objectStart; |
| 104 | + |
| 105 | + while (i < result.length && braceCount > 0) { |
| 106 | + if (result[i] === "{") braceCount++; |
| 107 | + else if (result[i] === "}") braceCount--; |
| 108 | + i++; |
| 109 | + } |
| 110 | + |
| 111 | + // i now points after the closing } of z.object({...}) |
| 112 | + // Check if followed by )) |
| 113 | + if (result.slice(i, i + 2) === "))") { |
| 114 | + const objectContent = result.slice(objectStart, i - 1); |
| 115 | + const replacement = `z.object({${objectContent}}).passthrough()`; |
| 116 | + result = result.slice(0, matchStart) + replacement + result.slice(i + 2); |
| 117 | + startIndex = matchStart + replacement.length; |
| 118 | + } else { |
| 119 | + startIndex = i; |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + return result; |
| 124 | +} |
| 125 | + |
| 126 | +main(); |
0 commit comments