| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { readFileSync, writeFileSync } from "node:fs"; |
| import { dirname, join } from "node:path"; |
| import { fileURLToPath } from "node:url"; |
| import { generate } from "ts-to-zod"; |
| import { toJSONSchema, type $ZodType } from "zod/v4/core"; |
|
|
| const __filename = fileURLToPath(import.meta.url); |
| const __dirname = dirname(__filename); |
| const PROJECT_ROOT = join(__dirname, ".."); |
|
|
| const SPEC_TYPES_FILE = join(PROJECT_ROOT, "src", "spec.types.ts"); |
| const GENERATED_DIR = join(PROJECT_ROOT, "src", "generated"); |
| const SCHEMA_OUTPUT_FILE = join(GENERATED_DIR, "schema.ts"); |
| const SCHEMA_TEST_OUTPUT_FILE = join(GENERATED_DIR, "schema.test.ts"); |
| const JSON_SCHEMA_OUTPUT_FILE = join(GENERATED_DIR, "schema.json"); |
|
|
| |
| |
| |
| |
| const EXTERNAL_TYPE_SCHEMAS = [ |
| "ContentBlockSchema", |
| "CallToolResultSchema", |
| "ImplementationSchema", |
| "RequestIdSchema", |
| "ToolSchema", |
| ]; |
|
|
| async function main() { |
| console.log("🔧 Generating Zod schemas from spec.types.ts...\n"); |
|
|
| const sourceText = readFileSync(SPEC_TYPES_FILE, "utf-8"); |
|
|
| const result = generate({ |
| sourceText, |
| keepComments: true, |
| skipParseJSDoc: false, |
| |
| getSchemaName: (typeName: string) => `${typeName}Schema`, |
| }); |
|
|
| if (result.errors.length > 0) { |
| console.error("❌ Generation errors:"); |
| for (const error of result.errors) { |
| console.error(` - ${error}`); |
| } |
| process.exit(1); |
| } |
|
|
| if (result.hasCircularDependencies) { |
| console.warn("⚠️ Warning: Circular dependencies detected in types"); |
| } |
|
|
| let schemasContent = result.getZodSchemasFile("../spec.types.js"); |
| schemasContent = postProcess(schemasContent); |
|
|
| writeFileSync(SCHEMA_OUTPUT_FILE, schemasContent, "utf-8"); |
| console.log(`✅ Written: ${SCHEMA_OUTPUT_FILE}`); |
|
|
| const testsContent = result.getIntegrationTestFile( |
| "../spec.types.js", |
| "./schema.js", |
| ); |
| if (testsContent) { |
| const processedTests = postProcessTests(testsContent); |
| writeFileSync(SCHEMA_TEST_OUTPUT_FILE, processedTests, "utf-8"); |
| console.log(`✅ Written: ${SCHEMA_TEST_OUTPUT_FILE}`); |
| } |
|
|
| |
| await generateJsonSchema(); |
|
|
| console.log("\n🎉 Schema generation complete!"); |
| } |
|
|
| |
| |
| |
| |
| async function generateJsonSchema() { |
| |
| |
| const schemas = await import("../src/generated/schema.js"); |
|
|
| const jsonSchema: { |
| $schema: string; |
| $id: string; |
| title: string; |
| description: string; |
| $defs: Record<string, unknown>; |
| } = { |
| $schema: "https://json-schema.org/draft/2020-12/schema", |
| $id: "https://modelcontextprotocol.io/ext-apps/schema.json", |
| title: "MCP Apps Protocol", |
| description: "JSON Schema for MCP Apps UI protocol messages", |
| $defs: {}, |
| }; |
|
|
| |
| for (const [name, schema] of Object.entries(schemas)) { |
| if ( |
| name.endsWith("Schema") && |
| typeof schema === "object" && |
| schema !== null |
| ) { |
| const typeName = name.replace(/Schema$/, ""); |
| try { |
| |
| |
| jsonSchema.$defs[typeName] = toJSONSchema(schema as $ZodType, { |
| unrepresentable: "any", |
| }); |
| } catch (error) { |
| console.warn(`⚠️ Could not convert ${name} to JSON Schema: ${error}`); |
| } |
| } |
| } |
|
|
| writeFileSync( |
| JSON_SCHEMA_OUTPUT_FILE, |
| JSON.stringify(jsonSchema, null, 2) + "\n", |
| "utf-8", |
| ); |
| console.log(`✅ Written: ${JSON_SCHEMA_OUTPUT_FILE}`); |
| } |
|
|
| |
| |
| |
| function postProcess(content: string): string { |
| |
| const mcpImports = EXTERNAL_TYPE_SCHEMAS.join(",\n "); |
| content = content.replace( |
| 'import { z } from "zod";', |
| `import { z } from "zod"; |
| import { |
| ${mcpImports}, |
| } from "@modelcontextprotocol/sdk/types.js";`, |
| ); |
|
|
| |
| for (const schema of EXTERNAL_TYPE_SCHEMAS) { |
| content = content.replace( |
| new RegExp(`(?:export )?const ${schema} = z\\.any\\(\\);\\n?`, "g"), |
| "", |
| ); |
| } |
|
|
| |
| |
| |
| content = replaceRecordAndWithPassthrough(content); |
|
|
| |
| content = content.replace( |
| "// Generated by ts-to-zod", |
| `// Generated by ts-to-zod |
| // Post-processed for Zod v3/v4 compatibility and MCP SDK integration |
| // Run: npm run generate:schemas`, |
| ); |
|
|
| return content; |
| } |
|
|
| |
| |
| |
| |
| |
| function replaceRecordAndWithPassthrough(content: string): string { |
| const pattern = "z.record(z.string(), z.unknown()).and(z.object({"; |
| let result = content; |
| let startIndex = 0; |
|
|
| while (true) { |
| const matchStart = result.indexOf(pattern, startIndex); |
| if (matchStart === -1) break; |
|
|
| |
| const objectStart = matchStart + pattern.length; |
| let braceCount = 1; |
| let i = objectStart; |
|
|
| while (i < result.length && braceCount > 0) { |
| if (result[i] === "{") braceCount++; |
| else if (result[i] === "}") braceCount--; |
| i++; |
| } |
|
|
| |
| |
| if (result.slice(i, i + 2) === "))") { |
| const objectContent = result.slice(objectStart, i - 1); |
| const replacement = `z.object({${objectContent}}).passthrough()`; |
| result = result.slice(0, matchStart) + replacement + result.slice(i + 2); |
| startIndex = matchStart + replacement.length; |
| } else { |
| startIndex = i; |
| } |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| function postProcessTests(content: string): string { |
| |
| content = content.replace( |
| "// Generated by ts-to-zod", |
| `// Generated by ts-to-zod |
| // Integration tests verifying schemas match TypeScript types |
| // Run: npm run generate:schemas`, |
| ); |
|
|
| return content; |
| } |
|
|
| main().catch((error) => { |
| console.error("❌ Schema generation failed:", error); |
| process.exit(1); |
| }); |
|
|