openuispec 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +6 -5
  2. package/cli/index.ts +18 -12
  3. package/cli/init.ts +79 -13
  4. package/docs/cli.md +134 -27
  5. package/docs/file-formats.md +51 -1
  6. package/drift/index.ts +7 -2
  7. package/examples/social-app/openuispec/README.md +2 -1
  8. package/examples/social-app/openuispec/mock/chat_detail.yaml +25 -0
  9. package/examples/social-app/openuispec/mock/discover.yaml +17 -0
  10. package/examples/social-app/openuispec/mock/edit_profile.yaml +9 -0
  11. package/examples/social-app/openuispec/mock/home_feed.yaml +32 -0
  12. package/examples/social-app/openuispec/mock/messages_inbox.yaml +15 -0
  13. package/examples/social-app/openuispec/mock/notifications.yaml +30 -0
  14. package/examples/social-app/openuispec/mock/post_detail.yaml +26 -0
  15. package/examples/social-app/openuispec/mock/profile_self.yaml +28 -0
  16. package/examples/social-app/openuispec/mock/profile_user.yaml +32 -0
  17. package/examples/social-app/openuispec/mock/search_results.yaml +17 -0
  18. package/examples/social-app/openuispec/mock/settings.yaml +7 -0
  19. package/examples/social-app/openuispec/openuispec.yaml +3 -2
  20. package/examples/taskflow/README.md +4 -2
  21. package/examples/taskflow/openuispec/README.md +2 -1
  22. package/examples/taskflow/openuispec/components/media_player.yaml +92 -0
  23. package/examples/taskflow/openuispec/contracts/README.md +2 -2
  24. package/examples/taskflow/openuispec/locales/en.json +1 -0
  25. package/examples/taskflow/openuispec/mock/home.yaml +64 -0
  26. package/examples/taskflow/openuispec/mock/profile_edit.yaml +6 -0
  27. package/examples/taskflow/openuispec/mock/project_detail.yaml +33 -0
  28. package/examples/taskflow/openuispec/mock/settings.yaml +13 -0
  29. package/examples/taskflow/openuispec/mock/task_detail.yaml +18 -0
  30. package/examples/taskflow/openuispec/openuispec.yaml +3 -4
  31. package/examples/taskflow/openuispec/platform/ios.yaml +0 -4
  32. package/examples/taskflow/openuispec/screens/task_detail.yaml +5 -8
  33. package/examples/taskflow/openuispec/tokens/icons.yaml +16 -0
  34. package/examples/todo-orbit/README.md +3 -2
  35. package/examples/todo-orbit/openuispec/README.md +2 -1
  36. package/examples/todo-orbit/openuispec/components/task_trend_chart.yaml +85 -0
  37. package/examples/todo-orbit/openuispec/locales/en.json +3 -0
  38. package/examples/todo-orbit/openuispec/locales/ru.json +3 -0
  39. package/examples/todo-orbit/openuispec/mock/analytics.yaml +26 -0
  40. package/examples/todo-orbit/openuispec/mock/home.yaml +33 -0
  41. package/examples/todo-orbit/openuispec/mock/settings.yaml +7 -0
  42. package/examples/todo-orbit/openuispec/mock/task_detail.yaml +14 -0
  43. package/examples/todo-orbit/openuispec/openuispec.yaml +3 -3
  44. package/examples/todo-orbit/openuispec/platform/android.yaml +0 -3
  45. package/examples/todo-orbit/openuispec/platform/ios.yaml +0 -3
  46. package/examples/todo-orbit/openuispec/platform/web.yaml +0 -3
  47. package/examples/todo-orbit/openuispec/screens/analytics.yaml +1 -4
  48. package/mcp-server/index.ts +87 -6
  49. package/mcp-server/preview-render.ts +1922 -0
  50. package/mcp-server/preview.ts +292 -0
  51. package/mcp-server/screenshot-shared.ts +41 -4
  52. package/mcp-server/screenshot.ts +283 -97
  53. package/package.json +1 -1
  54. package/prepare/index.ts +1 -1
  55. package/schema/component.schema.json +278 -0
  56. package/schema/openuispec.schema.json +5 -1
  57. package/schema/screen.schema.json +12 -1
  58. package/schema/semantic-lint.ts +29 -5
  59. package/schema/validate.ts +21 -0
  60. package/scripts/regenerate-previews.ts +136 -0
  61. package/spec/{openuispec-v0.1.md → openuispec-v0.2.md} +266 -8
  62. package/examples/taskflow/openuispec/contracts/x_media_player.yaml +0 -185
  63. package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +0 -139
@@ -10,13 +10,14 @@ Todo Orbit is a multi-platform sample project for OpenUISpec. It combines a sour
10
10
  - Create and edit task flows
11
11
  - Recurring-rule creation with conditional fields and validation
12
12
  - Bilingual localization with English and Russian
13
- - Custom contracts for schedule preview and task trend charts
13
+ - Components for reusable composed UI (task trend chart)
14
+ - Custom contracts for schedule preview
14
15
 
15
16
  ## Project layout
16
17
 
17
18
  | Path | Purpose |
18
19
  |------|---------|
19
- | [`openuispec/`](./openuispec/) | Source OpenUISpec project: manifest, tokens, screens, flows, contracts, locales |
20
+ | [`openuispec/`](./openuispec/) | Source OpenUISpec project: manifest, tokens, screens, flows, contracts, components, locales |
20
21
  | [`generated/web/Todo Orbit/`](./generated/web/Todo%20Orbit/) | Generated React + Vite web app |
21
22
  | [`generated/ios/Todo Orbit/`](./generated/ios/Todo%20Orbit/) | Generated SwiftUI iOS target |
22
23
  | [`generated/android/Todo Orbit/`](./generated/android/Todo%20Orbit/) | Generated Jetpack Compose Android target |
@@ -12,6 +12,7 @@ This directory contains the **OpenUISpec** semantic UI specification for **todo-
12
12
  | `screens/` | Screen definitions — one YAML file per screen |
13
13
  | `flows/` | Navigation flows — multi-step user journeys |
14
14
  | `contracts/` | Component contracts — standard extensions and custom (`x_` prefixed) |
15
+ | `components/` | Reusable compositions — contract slot compositions with states and variants |
15
16
  | `platform/` | Platform overrides — per-target (iOS, Android, Web) behaviors |
16
17
  | `locales/` | Localization — i18n strings (JSON, ICU MessageFormat) |
17
18
 
@@ -28,7 +29,7 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
28
29
 
29
30
  **Reference files inside the package (read in this order):**
30
31
  1. `README.md` — schema tables, file format reference, root wrapper keys
31
- 2. `spec/openuispec-v0.1.md` — full specification (contracts, layout, expressions, etc.)
32
+ 2. `spec/openuispec-v0.2.md` — full specification (contracts, layout, expressions, etc.)
32
33
  3. `examples/taskflow/openuispec/` — complete working example with all file types
33
34
  4. `schema/` — JSON Schemas for validation
34
35
 
@@ -0,0 +1,85 @@
1
+ # ============================================================
2
+ # Component: task_trend_chart
3
+ # ============================================================
4
+ # Visualizes task creation and completion trends as a composed
5
+ # component with chart area, legend, and period selector slots.
6
+ # ============================================================
7
+
8
+ task_trend_chart:
9
+ semantic: "Visualizes task creation and completion trends over time for productivity analysis"
10
+
11
+ props:
12
+ metric:
13
+ type: enum
14
+ values: [completed, created, completion_rate]
15
+ required: true
16
+ period:
17
+ type: enum
18
+ values: [week, month, quarter]
19
+ required: true
20
+ show_legend: { type: bool, default: true }
21
+
22
+ slots:
23
+ chart_area:
24
+ contract: data_display
25
+ variant: card
26
+ props: { title: "$t:analytics.trend_chart" }
27
+ period_selector:
28
+ contract: input_field
29
+ input_type: select
30
+ props: { label: "$t:analytics.period", options: [week, month, quarter] }
31
+ hideable: true
32
+ legend:
33
+ contract: data_display
34
+ variant: inline
35
+ props: { title: "$t:analytics.legend" }
36
+ hideable: true
37
+
38
+ layout:
39
+ type: stack
40
+ spacing: "spacing.sm"
41
+ sections:
42
+ - slot: period_selector
43
+ - slot: chart_area
44
+ - slot: legend
45
+
46
+ states:
47
+ idle: { semantic: "Chart has not loaded data yet" }
48
+ loading:
49
+ semantic: "Trend data is loading"
50
+ hide_slots: [legend]
51
+ ready:
52
+ semantic: "Trend data is available and rendered"
53
+ empty:
54
+ semantic: "No trend points available"
55
+ hide_slots: [legend]
56
+ error:
57
+ semantic: "Trend data failed to load"
58
+ hide_slots: [legend]
59
+
60
+ variants:
61
+ compact:
62
+ semantic: "Compact chart for dashboard cards"
63
+ hide_slots: [period_selector]
64
+ tokens:
65
+ background: "color.surface.secondary"
66
+ radius: "spacing.sm"
67
+
68
+ tokens:
69
+ background: "color.surface.primary"
70
+ radius: "spacing.sm"
71
+ padding: "spacing.md"
72
+
73
+ a11y:
74
+ role: "img"
75
+ label: "Analytics trend chart"
76
+
77
+ platform_mapping:
78
+ ios: { component: "Chart", framework: "Swift Charts" }
79
+ android: { component: "Canvas chart composable" }
80
+ web: { component: "SVG chart component" }
81
+
82
+ generation:
83
+ must_handle:
84
+ - "Render chart area with completed and created series"
85
+ - "Handle loading, empty, and error states"
@@ -26,6 +26,9 @@
26
26
  "analytics.completion_rate": "Completion rate",
27
27
  "analytics.overdue_section": "Overdue review",
28
28
  "analytics.overdue_subtitle": "Tasks that need attention first.",
29
+ "analytics.trend_chart": "Trend chart",
30
+ "analytics.period": "Period",
31
+ "analytics.legend": "Legend",
29
32
  "analytics.trend_subtitle": "Completion trend",
30
33
  "analytics.legend_completed": "Completed",
31
34
  "analytics.legend_created": "Created",
@@ -26,6 +26,9 @@
26
26
  "analytics.completion_rate": "Процент выполнения",
27
27
  "analytics.overdue_section": "Просроченные задачи",
28
28
  "analytics.overdue_subtitle": "Задачи, которым нужно уделить внимание в первую очередь.",
29
+ "analytics.trend_chart": "График тренда",
30
+ "analytics.period": "Период",
31
+ "analytics.legend": "Легенда",
29
32
  "analytics.trend_subtitle": "Динамика выполнения",
30
33
  "analytics.legend_completed": "Выполнено",
31
34
  "analytics.legend_created": "Создано",
@@ -0,0 +1,26 @@
1
+ # Mock data for the analytics screen
2
+ data:
3
+ overview:
4
+ completed_today: 3
5
+ open_tasks: 8
6
+ overdue_tasks: 2
7
+ completion_rate: 67
8
+
9
+ trend:
10
+ - { label: "Mon", completed: 4, created: 2 }
11
+ - { label: "Tue", completed: 3, created: 5 }
12
+ - { label: "Wed", completed: 6, created: 3 }
13
+ - { label: "Thu", completed: 2, created: 4 }
14
+ - { label: "Fri", completed: 5, created: 1 }
15
+ - { label: "Sat", completed: 1, created: 0 }
16
+ - { label: "Sun", completed: 0, created: 2 }
17
+
18
+ overdue:
19
+ - id: "t6"
20
+ title: "Submit tax forms"
21
+ priority: high
22
+ due_date: "2026-03-15"
23
+ - id: "t7"
24
+ title: "Renew gym membership"
25
+ priority: low
26
+ due_date: "2026-03-14"
@@ -0,0 +1,33 @@
1
+ # Mock data for the home screen
2
+ data:
3
+ tasks:
4
+ - id: "t1"
5
+ title: "Buy groceries"
6
+ due_date: "2026-03-18"
7
+ status: open
8
+ priority: high
9
+ - id: "t2"
10
+ title: "Finish project proposal"
11
+ due_date: "2026-03-19"
12
+ status: open
13
+ priority: medium
14
+ - id: "t3"
15
+ title: "Call dentist"
16
+ due_date: "2026-03-20"
17
+ status: done
18
+ priority: low
19
+ - id: "t4"
20
+ title: "Review pull request"
21
+ due_date: "2026-03-18"
22
+ status: open
23
+ priority: high
24
+ - id: "t5"
25
+ title: "Clean apartment"
26
+ due_date: "2026-03-21"
27
+ status: open
28
+ priority: medium
29
+
30
+ task_counts:
31
+ all: 12
32
+ open: 8
33
+ done: 4
@@ -0,0 +1,7 @@
1
+ # Mock data for the settings screen
2
+ data:
3
+ preferences:
4
+ locale: "en"
5
+ theme: "light"
6
+ reminders_enabled: true
7
+ daily_summary_enabled: false
@@ -0,0 +1,14 @@
1
+ # Mock data for the task_detail screen
2
+ data:
3
+ task:
4
+ id: "t1"
5
+ title: "Buy groceries"
6
+ status: open
7
+ priority: high
8
+ due_date: "2026-03-18"
9
+ notes: "Get milk, eggs, bread, and vegetables. Check the weekly deals at the store."
10
+ created_at: "2026-03-10"
11
+ updated_at: "2026-03-17"
12
+
13
+ params:
14
+ task_id: "t1"
@@ -1,5 +1,5 @@
1
- # openuispec-sample — OpenUISpec v0.1
2
- spec_version: "0.1"
1
+ # openuispec-sample — OpenUISpec v0.2
2
+ spec_version: "0.2"
3
3
 
4
4
  project:
5
5
  name: "Todo Orbit"
@@ -9,13 +9,13 @@ project:
9
9
  includes:
10
10
  tokens: "./tokens/"
11
11
  contracts: "./contracts/"
12
+ components: "./components/"
12
13
  screens: "./screens/"
13
14
  flows: "./flows/"
14
15
  platform: "./platform/"
15
16
  locales: "./locales/"
16
17
 
17
18
  custom_contracts:
18
- - "./contracts/x_task_trend_chart.yaml"
19
19
  - "./contracts/x_schedule_preview.yaml"
20
20
 
21
21
  i18n:
@@ -4,9 +4,6 @@ android:
4
4
  min_sdk: 26
5
5
 
6
6
  overrides:
7
- x_task_trend_chart:
8
- compact: { uses_canvas_rendering: true }
9
- detail: { uses_canvas_rendering: true, supports_pointer_highlight: true }
10
7
  x_schedule_preview:
11
8
  compact: { uses_lazy_column: true }
12
9
  detail: { uses_lazy_column: true, supports_animated_content: true }
@@ -3,9 +3,6 @@ ios:
3
3
  min_version: "17.0"
4
4
 
5
5
  overrides:
6
- x_task_trend_chart:
7
- compact: { uses_native_chart_framework: true }
8
- detail: { uses_native_chart_framework: true, supports_rule_mark: true }
9
6
  x_schedule_preview:
10
7
  compact: { uses_timeline_view: true }
11
8
  detail: { uses_timeline_view: true, supports_content_transition: true }
@@ -3,9 +3,6 @@ web:
3
3
  language: typescript
4
4
 
5
5
  overrides:
6
- x_task_trend_chart:
7
- compact: { implementation: "svg", responsive: true }
8
- detail: { implementation: "svg", responsive: true, supports_tooltip: true }
9
6
  x_schedule_preview:
10
7
  compact: { implementation: "semantic_list", responsive: true }
11
8
  detail: { implementation: "semantic_list", responsive: true, supports_staggered_updates: true }
@@ -102,14 +102,11 @@ analytics:
102
102
 
103
103
  - id: trend_chart
104
104
  margin_top: "spacing.lg"
105
- contract: x_task_trend_chart
106
- variant: detail
105
+ component: task_trend_chart
107
106
  props:
108
- series: "trend"
109
107
  metric: completed
110
108
  period: "state.period"
111
109
  show_legend: true
112
- empty_message: "$t:analytics.empty_trend"
113
110
 
114
111
  - id: overdue_header
115
112
  margin_top: "spacing.xl"
@@ -25,6 +25,7 @@ import YAML from "yaml";
25
25
  import { takeScreenshot, takeScreenshotBatch } from "./screenshot.js";
26
26
  import { takeAndroidScreenshot, takeAndroidScreenshotBatch } from "./screenshot-android.js";
27
27
  import { takeIOSScreenshot, takeIOSScreenshotBatch } from "./screenshot-ios.js";
28
+ import { renderPreview } from "./preview.js";
28
29
 
29
30
  // ── resolve project cwd ──────────────────────────────────────────────
30
31
 
@@ -121,8 +122,9 @@ WORKFLOW — each tool response includes a next_tool hint, follow it:
121
122
  5. Remind the user to baseline when satisfied: openuispec drift --snapshot --target <t>
122
123
  Do not baseline on your own initiative — the user decides when output is accepted.
123
124
 
124
- FOCUSED GETTERS (prefer for incremental edits): get_screen, get_contract, get_tokens, get_locale
125
+ FOCUSED GETTERS (prefer for incremental edits): get_screen, get_contract, get_component, get_tokens, get_locale
125
126
  SPEC AUTHORING: spec_types → spec_schema(type, summary?) → write YAML
127
+ PREVIEW: openuispec_preview(screen) → render spec as HTML with mock data, returns screenshot (no app needed)
126
128
  SCREENSHOTS: screenshot (web), screenshot_android, screenshot_ios — single + batch variants
127
129
 
128
130
  Skip only for purely non-UI requests.`,
@@ -387,10 +389,10 @@ server.registerTool(
387
389
  server.registerTool(
388
390
  "openuispec_validate",
389
391
  {
390
- description: "Validate spec files against JSON schemas. Returns validation errors grouped by type (manifest, tokens, screens, flows, platform, locales, contracts, semantic).",
392
+ description: "Validate spec files against JSON schemas. Returns validation errors grouped by type (manifest, tokens, screens, flows, platform, locales, contracts, components, semantic).",
391
393
  inputSchema: {
392
394
  groups: z
393
- .array(z.enum(["manifest", "tokens", "screens", "flows", "platform", "locales", "contracts", "semantic"]))
395
+ .array(z.enum(["manifest", "tokens", "screens", "flows", "platform", "locales", "contracts", "components", "semantic"]))
394
396
  .optional()
395
397
  .describe("Specific groups to validate. If omitted, validates all groups."),
396
398
  },
@@ -491,6 +493,7 @@ const SCHEMA_CATALOG: Record<string, { file: string; title: string; description:
491
493
  platform: { file: "platform.schema.json", title: "Platform", description: "Platform-specific generation config: architecture, naming, CSS framework, component mapping" },
492
494
  contract: { file: "contract.schema.json", title: "Contract", description: "Built-in UI contract definitions: variants, props, must_handle states, generation hints" },
493
495
  "custom-contract":{ file: "custom-contract.schema.json", title: "Custom Contract", description: "User-defined UI contract definitions (x_ prefixed)" },
496
+ component: { file: "component.schema.json", title: "Component", description: "Reusable composition of contracts with named slots, states, variants, and layout" },
494
497
  locale: { file: "locale.schema.json", title: "Locale", description: "Locale translation files: flat key-value string maps" },
495
498
  "tokens/color": { file: "tokens/color.schema.json", title: "Color Tokens", description: "Color tokens: brand, surface, text, semantic, border groups with HSL ranges and contrast" },
496
499
  "tokens/typography": { file: "tokens/typography.schema.json", title: "Typography Tokens", description: "Typography tokens: font families, sizes, weights, line heights, letter spacing" },
@@ -634,6 +637,53 @@ server.registerTool(
634
637
  }
635
638
  );
636
639
 
640
+ // ── tool: openuispec_get_component ──────────────────────────────────
641
+
642
+ server.registerTool(
643
+ "openuispec_get_component",
644
+ {
645
+ description: "Get a single component spec. Components are reusable compositions of contracts with named slots.",
646
+ inputSchema: {
647
+ name: z.string().describe("Component name, e.g. 'media_player'"),
648
+ variant: z.string().optional().describe("Optional variant name. If given, returns only that variant's definition."),
649
+ },
650
+ },
651
+ async ({ name, variant }) => {
652
+ try {
653
+ const projectDir = findProjectDir(projectCwd);
654
+ const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
655
+ const componentsDir = resolveSpecDir(projectDir, manifest, "components");
656
+
657
+ if (!existsSync(componentsDir)) {
658
+ return toolError(`Components directory not found: ${componentsDir}`);
659
+ }
660
+
661
+ for (const file of readdirSync(componentsDir).filter(f => f.endsWith(".yaml")).sort()) {
662
+ const filePath = join(componentsDir, file);
663
+ const raw = readFileSync(filePath, "utf-8");
664
+ const content = YAML.parse(raw);
665
+ const componentName = Object.keys(content)[0];
666
+ if (componentName !== name) continue;
667
+
668
+ if (variant) {
669
+ const component = content[componentName];
670
+ const variantDef = component?.variants?.[variant];
671
+ if (!variantDef) {
672
+ return toolError(`Variant "${variant}" not found in component "${name}". Available variants: ${Object.keys(component?.variants ?? {}).join(", ")}`);
673
+ }
674
+ return toolResult({ name, variant, definition: variantDef });
675
+ }
676
+
677
+ return toolResult({ name, path: relative(projectDir, filePath), content: raw });
678
+ }
679
+
680
+ return toolError(`Component "${name}" not found in ${componentsDir}`);
681
+ } catch (err) {
682
+ return toolError(err);
683
+ }
684
+ }
685
+ );
686
+
637
687
  // ── tool: openuispec_get_tokens ─────────────────────────────────────
638
688
 
639
689
  server.registerTool(
@@ -763,9 +813,10 @@ server.registerTool(
763
813
  full_page: z.boolean().optional().default(false).describe("Capture the full scrollable page instead of just the viewport"),
764
814
  selector: z.string().optional().describe("CSS selector to screenshot a specific element instead of the full page"),
765
815
  output_dir: z.string().optional().describe("Directory to save the screenshot PNG (relative to web app root). E.g. 'screenshots'. If omitted, only returns base64 in response."),
816
+ init_script: z.string().optional().describe("JavaScript to run before the page renders. Passed to the app via ?__ous_init=<base64> query param. The app's bootstrapper decodes and executes it — use for auth injection, role switching, or session setup."),
766
817
  },
767
818
  },
768
- async ({ route, viewport, scale, theme, wait_for, full_page, selector, output_dir }) => {
819
+ async ({ route, viewport, scale, theme, wait_for, full_page, selector, output_dir, init_script }) => {
769
820
  try {
770
821
  return await takeScreenshot(projectCwd, {
771
822
  route,
@@ -776,6 +827,7 @@ server.registerTool(
776
827
  full_page,
777
828
  selector,
778
829
  output_dir,
830
+ init_script,
779
831
  });
780
832
  } catch (err) {
781
833
  return toolError(err);
@@ -844,6 +896,7 @@ const webBatchCaptureSchema = z.object({
844
896
  selector: z.string().optional().describe("CSS selector to screenshot a specific element"),
845
897
  full_page: z.boolean().optional().describe("Capture full scrollable page"),
846
898
  wait_for: z.number().optional().describe("Per-capture wait time in ms"),
899
+ init_script: z.string().optional().describe("Per-capture init script (overrides shared init_script for this capture)"),
847
900
  });
848
901
 
849
902
  server.registerTool(
@@ -856,11 +909,12 @@ server.registerTool(
856
909
  scale: z.number().optional().default(2).describe("Device pixel ratio for all captures (default 2)"),
857
910
  theme: z.enum(["light", "dark"]).optional().describe("Force color scheme for all captures"),
858
911
  output_dir: z.string().optional().describe("Directory to save all PNGs (relative to web app root)"),
912
+ init_script: z.string().optional().describe("Shared init script for all captures. Passed via ?__ous_init=<base64>. Per-capture init_script overrides this."),
859
913
  },
860
914
  },
861
- async ({ captures, viewport, scale, theme, output_dir }) => {
915
+ async ({ captures, viewport, scale, theme, output_dir, init_script }) => {
862
916
  try {
863
- return await takeScreenshotBatch(projectCwd, { captures, viewport, scale, theme, output_dir });
917
+ return await takeScreenshotBatch(projectCwd, { captures, viewport, scale, theme, output_dir, init_script });
864
918
  } catch (err) {
865
919
  return toolError(err);
866
920
  }
@@ -928,6 +982,33 @@ server.registerTool(
928
982
  }
929
983
  );
930
984
 
985
+ // ── tool: openuispec_preview ────────────────────────────────────────────
986
+
987
+ server.registerTool(
988
+ "openuispec_preview",
989
+ {
990
+ description: "Render a screen spec as an HTML preview with mock data and return a screenshot. Uses token values, locale strings, and contract-to-HTML mapping to produce a visual approximation without generating a full app. Mock data should be placed in openuispec/mock/<screen>.yaml.",
991
+ inputSchema: {
992
+ screen: z.string().describe("Screen name (e.g. 'home', 'settings', 'task_detail')"),
993
+ size_class: z.enum(["compact", "regular", "expanded"]).optional().default("compact").describe("Adaptive size class — compact (phone), regular (tablet), expanded (desktop)"),
994
+ theme: z.enum(["light", "dark"]).optional().default("light").describe("Color theme"),
995
+ locale: z.string().optional().default("en").describe("Locale code for i18n strings"),
996
+ viewport: z.object({
997
+ width: z.number(),
998
+ height: z.number(),
999
+ }).optional().describe("Custom viewport size (overrides size_class default)"),
1000
+ include_html: z.boolean().optional().default(false).describe("Also return the rendered HTML string in the response"),
1001
+ },
1002
+ },
1003
+ async ({ screen, size_class, theme, locale, viewport, include_html }) => {
1004
+ try {
1005
+ return await renderPreview(projectCwd, { screen, size_class, theme, locale, viewport, include_html });
1006
+ } catch (err) {
1007
+ return toolError(err);
1008
+ }
1009
+ }
1010
+ );
1011
+
931
1012
  // ── start server ─────────────────────────────────────────────────────
932
1013
 
933
1014
  export async function startMcpServer() {