Skip to content

Conversation

@ochafik
Copy link
Collaborator

@ochafik ochafik commented Dec 9, 2025

TL;DR

Edit types in one place → Zod schemas + JSON Schema auto-generated.

Before: Hand-maintain both TypeScript interfaces AND Zod schemas (error-prone, tedious).
After: Edit src/spec.types.ts only. Run npm run generate:schemas. Done.


How It Works

1. Single Source of Truth

src/spec.types.ts          ← YOU EDIT THIS (TypeScript interfaces)
        ↓
   npm run generate:schemas
        ↓
src/generated/
├── schema.ts              ← Zod schemas (auto-generated)
├── schema.test.ts         ← Type verification (auto-generated)  
└── schema.json            ← JSON Schema (auto-generated)

2. Generation Pipeline

We use ts-to-zod to convert TypeScript interfaces to Zod schemas, then post-process the output:

ts-to-zod Output Post-Processing Why
import { z } from "zod" "zod/v4" We use Zod v4 subpath
z.any() for SDK types → Import actual schemas ContentBlock, Tool, etc. have real schemas in MCP SDK
z.record().and(z.object()) z.looseObject() Index signatures need simpler representation

3. Adding Descriptions

Use @description JSDoc tags to generate .describe() calls:

// In spec.types.ts:
interface McpUiOpenLinkRequest {
  params: {
    /** @description URL to open in the host's browser */
    url: string;
  };
}

Generates:

// In generated/schema.ts:
export const McpUiOpenLinkRequestSchema = z.object({
  params: z.object({
    url: z.string().describe("URL to open in the host's browser")
  })
});

These descriptions flow into the JSON Schema too, enabling documentation generation and IDE support.


What's Exported

// Types (from spec.types.ts)
import type { McpUiOpenLinkRequest, McpUiHostContext, ... } from "@modelcontextprotocol/ext-apps";

// Zod Schemas (from generated/schema.ts)  
import { McpUiOpenLinkRequestSchema, McpUiHostContextSchema, ... } from "@modelcontextprotocol/ext-apps";

// JSON Schema
import schema from "@modelcontextprotocol/ext-apps/schema.json";

All 47 exports from main branch are preserved with identical types.


Testing

Check Status
npm run build ✅ Passes
npm test ✅ 15 tests pass
Type compatibility ✅ Compile-time verified via schema.test.ts
Backwards compatibility ✅ All 47 exports from main preserved
CI freshness check git diff --exit-code src/generated/

For Reviewers

Key files to review:

  • src/spec.types.ts - The new source of truth for all protocol types
  • scripts/generate-schemas.ts - The generation + post-processing logic
  • src/types.ts - Re-exports (just wiring, no logic)

The src/generated/ files are auto-generated and don't need detailed review.

🤖 Generated with Claude Code

ochafik and others added 14 commits December 1, 2025 20:49
Add automated Zod schema generation using ts-to-zod:
- New npm script `generate:schemas` to regenerate schemas from spec.types.ts
- Post-processing script for Zod v4 compatibility and MCP SDK type imports
- src/spec.types.ts: Pure TypeScript interface definitions (source of truth)
- src/schemas.generated.ts: Auto-generated Zod schemas
- src/schemas.ts: Re-exports with PascalCase naming for backwards compatibility

The generated schemas provide a simpler alternative to the hand-written schemas
in types.ts, useful for lighter validation or reference implementations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…rated.ts

- types.ts imports and re-exports types from spec.types.ts (source of truth)
- types.ts imports and re-exports schemas from schemas.generated.ts
- Added compile-time VerifySchemaMatches checks to flag mismatches between
  interfaces and generated schemas
- Removed redundant schemas.ts (types.ts now handles all re-exports)
- Applied prettier formatting to schemas.generated.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Replace CLI-based schema generation with library API:

- scripts/generate-schemas.ts: New script using ts-to-zod library API
  - Detailed documentation of why each post-processing step is needed
  - Generates integration tests via getIntegrationTestFile()
  - Better error reporting and control

- src/schemas.generated.test.ts: Auto-generated integration tests
  - Compile-time verification that schemas match TypeScript types
  - Uses expectType<> pattern for bidirectional type checking

- Removed scripts/postprocess-schemas.ts (merged into generate-schemas.ts)

- Fixed McpUiResourceTeardownResult to include index signature for MCP SDK
  Protocol compatibility (detected by the new integration tests)

Post-processing rationale (documented in generate-schemas.ts):
1. zod → zod/v4: Explicit v4 subpath for version clarity
2. z.any() → MCP SDK schemas: External types need real validation
3. z.record().and() → z.looseObject(): Index signatures need Zod v4 idiom

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- build script now runs generate:schemas before bun build
- CI workflow verifies generated schemas are up-to-date
  (fails if spec.types.ts changed but schemas weren't regenerated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Resolved conflicts:
- .github/workflows/ci.yml: Combined schema verification with new test step
- package.json: Merged schema generation script with new example scripts
- src/types.ts: Kept refactored structure with spec.types.ts/schemas.generated.ts
- package-lock.json: Regenerated

Renamed McpUiSizeChangeNotification -> McpUiSizeChangedNotification to match main.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add getSchemaName option to generate PascalCase schema names directly
  (McpUiOpenLinkRequestSchema instead of mcpUiOpenLinkRequestSchema)
- Remove redundant compile-time verification from types.ts
  (already handled by generated test file's bidirectional expectType checks)
- Simplify types.ts from 235 to 57 lines (just re-exports)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add missing csp field to McpUiSandboxResourceReadyNotification (regression from merge)
- Simplify generate-schemas.ts - remove complex .describe() conversion
- Document that ts-to-zod supports @description JSDoc tags for .describe()
- Improve z.looseObject() replacement with brace-counting for nested objects

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added @description tags to all interfaces and fields in spec.types.ts.
ts-to-zod now generates .describe() calls on all schema fields, making
descriptions available at runtime for introspection and JSON Schema generation.

Example:
  /** @description URL to open in the host's browser */
  url: string;

Generates:
  url: z.string().describe("URL to open in the host's browser")

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added JSON Schema generation to the schema generation pipeline:
- Uses Zod v4's built-in toJSONSchema() to convert schemas
- Generates src/schema.json with all message types in $defs
- Includes descriptions from @description JSDoc tags
- Added export "./schema.json" to package.json
- Updated CI to verify schema.json is up-to-date

Use cases:
- Language-agnostic validation (Python, Go, Rust)
- Documentation generation (Redoc, Swagger UI)
- Code generation (quicktype)
- IDE autocomplete for JSON/YAML

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Renamed to match other generated files naming convention.
Export path unchanged: ./schema.json -> ./dist/src/schema.generated.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Consistent singular naming for all generated files:
- schema.generated.ts
- schema.generated.test.ts
- schema.generated.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Consistent singular naming for all generated files:
- schema.generated.ts
- schema.generated.test.ts
- schema.generated.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Moved:
- src/schema.generated.ts → src/generated/schema.ts
- src/schema.generated.test.ts → src/generated/schema.test.ts
- src/schema.generated.json → src/generated/schema.json

Benefits:
- Cleaner organization (generated files isolated)
- Simpler names without .generated suffix
- Easier to gitignore if desired later

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 9, 2025

Open in StackBlitz

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/ext-apps@116

commit: 0285d51

Added missing exports that existed in main:
- McpUiTheme, McpUiThemeSchema
- McpUiDisplayMode, McpUiDisplayModeSchema
- McpUiResourceCsp, McpUiResourceCspSchema
- McpUiResourceMeta, McpUiResourceMetaSchema

Also fixed theme/displayMode to match original values:
- theme: 'light' | 'dark' (removed 'system')
- displayMode: 'inline' | 'fullscreen' | 'pip' (removed 'carousel')

Verified: all 47 exports from main are preserved with compatible types.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@ochafik ochafik force-pushed the ochafik/ts2zod-schemas branch from 334b656 to 93dd4a6 Compare December 9, 2025 21:14
@ochafik ochafik changed the title feat: auto-generate Zod schemas from TypeScript types using ts-to-zod build: auto-generate Zod schemas from TypeScript types using ts-to-zod Dec 9, 2025
@ochafik ochafik marked this pull request as ready for review December 9, 2025 21:22
@ochafik ochafik requested review from antonpk1, idosal, jonathanhefner and liady and removed request for antonpk1 December 9, 2025 21:22
ochafik and others added 3 commits December 9, 2025 22:26
The pre-commit hook runs build (which generates schemas) then prettier
(which formats them). The formatted generated files weren't being staged,
causing CI to fail with 'Generated schemas are out of date'.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
CI was comparing unformatted generated output against formatted committed
files, causing false 'out of date' failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
idosal
idosal previously approved these changes Dec 9, 2025
Copy link
Collaborator

@idosal idosal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome @ochafik !

Resolved conflict in src/types.ts - kept the clean re-export structure
from this branch which separates concerns between spec.types.ts (interfaces)
and generated/schema.ts (Zod schemas).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@ochafik ochafik merged commit f06340b into main Dec 10, 2025
8 checks passed
@ochafik ochafik deleted the ochafik/ts2zod-schemas branch December 13, 2025 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants