openuispec 0.1.33 → 0.1.35

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.
package/README.md CHANGED
@@ -52,7 +52,7 @@ cd your-project
52
52
  openuispec init
53
53
  ```
54
54
 
55
- This scaffolds a spec directory, starter tokens, and adds rules to `CLAUDE.md` / `AGENTS.md` so AI assistants track spec changes automatically.
55
+ This scaffolds a spec directory, starter tokens, adds rules to `CLAUDE.md` / `AGENTS.md`, and configures the MCP server in `.claude.json` so AI assistants track spec changes automatically.
56
56
  Use `openuispec init --no-configure-targets` if you want to scaffold first and choose target stacks later.
57
57
 
58
58
  Then hand your spec to any AI code generator:
@@ -116,10 +116,16 @@ openuispec/
116
116
  ├── cli/ # CLI tool (openuispec init, drift, prepare, validate)
117
117
  │ ├── index.ts # Entry point
118
118
  │ └── init.ts # Project scaffolding + AI rules
119
+ ├── mcp-server/ # MCP server (openuispec-mcp)
120
+ │ └── index.ts # Stdio transport, 5 tools
121
+ ├── check/ # Composite validation command
122
+ │ └── index.ts # Schema + semantic + readiness
119
123
  ├── drift/ # Drift detection (spec change tracking)
120
124
  │ └── index.ts # Hash-based drift checker
121
125
  ├── prepare/ # Target work bundle generation
122
126
  │ └── index.ts # Baseline-aware target preparation
127
+ ├── status/ # Cross-target status summary
128
+ │ └── index.ts # Baseline/drift overview
123
129
  ├── LICENSE
124
130
  └── README.md
125
131
  ```
@@ -239,6 +245,41 @@ to see which targets are already up to date and which ones still need to catch u
239
245
 
240
246
  `drift --snapshot` is bookkeeping. It does not prove that the target code matches the spec, and it will not create a missing target output directory for you.
241
247
 
248
+ ## MCP server
249
+
250
+ OpenUISpec includes an MCP (Model Context Protocol) server that exposes CLI commands as tools for AI assistants. This is the recommended way to integrate with Claude Code and other MCP-compatible clients — tools are called more reliably than CLAUDE.md instructions.
251
+
252
+ ### Setup
253
+
254
+ `openuispec init` automatically configures the MCP server in `.claude.json`. For existing projects, add it manually:
255
+
256
+ ```json
257
+ {
258
+ "mcpServers": {
259
+ "openuispec": {
260
+ "command": "npx",
261
+ "args": ["openuispec-mcp"]
262
+ }
263
+ }
264
+ }
265
+ ```
266
+
267
+ Or run directly: `npx openuispec-mcp`
268
+
269
+ Set `OPENUISPEC_PROJECT_DIR` to override the working directory.
270
+
271
+ ### Tools
272
+
273
+ | Tool | Description |
274
+ |------|-------------|
275
+ | `openuispec_prepare` | Build AI-ready work bundle for a target. **Call before any UI code generation.** |
276
+ | `openuispec_check` | Schema validation + semantic lint + prepare readiness. Call after spec edits. |
277
+ | `openuispec_status` | Cross-target summary: baselines, drift, next steps. |
278
+ | `openuispec_validate` | Schema-only validation, optionally filtered by group. |
279
+ | `openuispec_drift` | Detect spec drift since last snapshot, with optional semantic explanation. |
280
+
281
+ All tools return structured JSON. The server includes protocol-level instructions that tell AI assistants to call `openuispec_prepare` before any UI work — this works independently of CLAUDE.md rules.
282
+
242
283
  ## Spec at a glance
243
284
 
244
285
  | Section | What it defines |
@@ -289,6 +330,7 @@ to see which targets are already up to date and which ones still need to catch u
289
330
  - [x] Form system (validation rules, field dependencies)
290
331
  - [x] Drift detection (spec change tracking per platform)
291
332
  - [x] CLI tool (`openuispec init` for project scaffolding + AI rules)
333
+ - [x] MCP server for AI tool integration (`openuispec-mcp`)
292
334
  - [x] Multi-platform showcase app (`examples/todo-orbit/`)
293
335
  - [ ] More example apps (e-commerce, social, dashboard)
294
336
 
package/check/index.ts ADDED
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Composite check command for OpenUISpec projects.
4
+ *
5
+ * Combines schema validation, semantic linting, and prepare readiness
6
+ * into a single call for AI agents.
7
+ *
8
+ * Usage:
9
+ * openuispec check --target web # human-readable summary
10
+ * openuispec check --target ios --json # machine-readable output
11
+ */
12
+
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import YAML from "yaml";
16
+ import {
17
+ findProjectDir,
18
+ readManifest,
19
+ readProjectName,
20
+ resolveOutputDir,
21
+ } from "../drift/index.js";
22
+ import {
23
+ buildAjv,
24
+ readIncludes,
25
+ GROUPS,
26
+ type JsonGroupResult,
27
+ } from "../schema/validate.js";
28
+ import { collectSemanticLint } from "../schema/semantic-lint.js";
29
+
30
+ // ── types ─────────────────────────────────────────────────────────────
31
+
32
+ interface CheckValidation {
33
+ total_errors: number;
34
+ groups: JsonGroupResult[];
35
+ }
36
+
37
+ interface CheckSemantic {
38
+ total_errors: number;
39
+ errors: Array<{ path: string; message: string }>;
40
+ }
41
+
42
+ interface CheckPrepare {
43
+ mode: "bootstrap" | "update";
44
+ ready: boolean;
45
+ missing: string[];
46
+ warnings: string[];
47
+ }
48
+
49
+ export interface CheckResult {
50
+ target: string;
51
+ validation: CheckValidation;
52
+ semantic: CheckSemantic;
53
+ prepare: CheckPrepare;
54
+ }
55
+
56
+ // ── prepare readiness helpers ─────────────────────────────────────────
57
+
58
+ const PRESENTATION_ONLY_KEYS = new Set(["naming", "bundler", "css"]);
59
+
60
+ function platformStackKeys(target: string): string[] {
61
+ switch (target) {
62
+ case "android":
63
+ return ["architecture", "state", "preferences", "database", "di", "naming"];
64
+ case "web":
65
+ return ["runtime", "routing", "state", "storage_backend", "bundler", "css", "naming"];
66
+ case "ios":
67
+ return ["architecture", "persistence", "di", "naming"];
68
+ default:
69
+ return [];
70
+ }
71
+ }
72
+
73
+ function requiredPlatformDecisionKeys(target: string): string[] {
74
+ return platformStackKeys(target).filter((key) => !PRESENTATION_ONLY_KEYS.has(key));
75
+ }
76
+
77
+ function readPlatformDefinition(
78
+ projectDir: string,
79
+ manifest: Record<string, any>,
80
+ target: string,
81
+ ): Record<string, any> {
82
+ const platformDir = resolve(projectDir, manifest.includes?.platform ?? "./platform/");
83
+ const platformPath = join(platformDir, `${target}.yaml`);
84
+ if (!existsSync(platformPath)) return {};
85
+ try {
86
+ const doc = YAML.parse(readFileSync(platformPath, "utf-8"));
87
+ return doc?.[target] ?? {};
88
+ } catch {
89
+ return {};
90
+ }
91
+ }
92
+
93
+ function missingPlatformDecisions(
94
+ target: string,
95
+ platformDef: Record<string, any>,
96
+ ): string[] {
97
+ const generation = platformDef.generation ?? {};
98
+ return requiredPlatformDecisionKeys(target).filter((key) => {
99
+ const value = generation[key];
100
+ return typeof value !== "string" || value.trim().length === 0;
101
+ });
102
+ }
103
+
104
+ function hasApiEndpoints(manifest: Record<string, any>): boolean {
105
+ const endpoints = manifest.api?.endpoints;
106
+ return typeof endpoints === "object" && endpoints !== null && Object.keys(endpoints).length > 0;
107
+ }
108
+
109
+ function resolveBackendRoot(
110
+ projectDir: string,
111
+ manifest: Record<string, any>,
112
+ ): string | null {
113
+ const backendRoot = manifest.generation?.code_roots?.backend;
114
+ if (typeof backendRoot !== "string" || backendRoot.trim().length === 0) {
115
+ return null;
116
+ }
117
+ return resolve(projectDir, backendRoot);
118
+ }
119
+
120
+ function determinePrepare(
121
+ projectDir: string,
122
+ projectName: string,
123
+ target: string,
124
+ ): CheckPrepare {
125
+ const manifest = readManifest(projectDir);
126
+ const outputDir = resolveOutputDir(projectDir, projectName, target);
127
+ const statePath = join(outputDir, ".openuispec-state.json");
128
+ const snapshotExists = existsSync(statePath);
129
+ const mode: "bootstrap" | "update" = snapshotExists ? "update" : "bootstrap";
130
+
131
+ const platformDef = readPlatformDefinition(projectDir, manifest, target);
132
+ const missing = missingPlatformDecisions(target, platformDef);
133
+ const warnings: string[] = [];
134
+
135
+ const backendContextRequired = hasApiEndpoints(manifest);
136
+ const backendRoot = resolveBackendRoot(projectDir, manifest);
137
+ const backendContextReady =
138
+ !backendContextRequired || (backendRoot !== null && existsSync(backendRoot));
139
+
140
+ if (!backendContextReady) {
141
+ warnings.push(
142
+ "api endpoints require generation.code_roots.backend to point at an existing backend folder",
143
+ );
144
+ }
145
+
146
+ const stackConfirmation = platformDef.generation?.stack_confirmation;
147
+ const pendingUserConfirmation =
148
+ typeof stackConfirmation === "string" && stackConfirmation !== "confirmed";
149
+
150
+ if (pendingUserConfirmation) {
151
+ warnings.push(
152
+ `Target stack for "${target}" requires explicit user confirmation before implementation.`,
153
+ );
154
+ }
155
+
156
+ const ready =
157
+ missing.length === 0 && backendContextReady && !pendingUserConfirmation;
158
+
159
+ return { mode, ready, missing, warnings };
160
+ }
161
+
162
+ // ── core (importable, no process.exit) ───────────────────────────────
163
+
164
+ export function buildCheckResult(target: string, cwd: string = process.cwd()): CheckResult {
165
+ const projectDir = findProjectDir(cwd);
166
+ const projectName = readProjectName(projectDir);
167
+ const includes = readIncludes(projectDir);
168
+ const ajv = buildAjv();
169
+
170
+ // 1. Schema validation (all groups except semantic)
171
+ const schemaGroupKeys = Object.keys(GROUPS).filter((k) => k !== "semantic");
172
+ const validationGroups: JsonGroupResult[] = [];
173
+ let validationTotalErrors = 0;
174
+
175
+ for (const key of schemaGroupKeys) {
176
+ const result = GROUPS[key].collectJson(ajv, projectDir, includes, key);
177
+ validationGroups.push(result);
178
+ validationTotalErrors += result.errors.length;
179
+ }
180
+
181
+ const validation: CheckValidation = {
182
+ total_errors: validationTotalErrors,
183
+ groups: validationGroups,
184
+ };
185
+
186
+ // 2. Semantic validation
187
+ const semanticErrors = collectSemanticLint(projectDir, includes);
188
+ const semantic: CheckSemantic = {
189
+ total_errors: semanticErrors.length,
190
+ errors: semanticErrors.map((e) => ({ path: e.path, message: e.message })),
191
+ };
192
+
193
+ // 3. Prepare readiness
194
+ const prepare = determinePrepare(projectDir, projectName, target);
195
+
196
+ return { target, validation, semantic, prepare };
197
+ }
198
+
199
+ // ── main ──────────────────────────────────────────────────────────────
200
+
201
+ export function runCheck(argv: string[]): void {
202
+ const isJson = argv.includes("--json");
203
+ const targetIdx = argv.indexOf("--target");
204
+ const target =
205
+ targetIdx !== -1 && argv[targetIdx + 1] ? argv[targetIdx + 1] : null;
206
+
207
+ if (!target) {
208
+ console.error("Error: --target is required for check");
209
+ console.error("Usage: openuispec check --target <target> [--json]");
210
+ process.exit(1);
211
+ }
212
+
213
+ const result = buildCheckResult(target);
214
+
215
+ if (isJson) {
216
+ console.log(JSON.stringify(result, null, 2));
217
+ } else {
218
+ printReport(result);
219
+ }
220
+
221
+ // Exit codes: 0 = clean + ready, 2 = validation errors, 1 = config error
222
+ const totalErrors = result.validation.total_errors + result.semantic.total_errors;
223
+ if (totalErrors > 0) {
224
+ process.exit(2);
225
+ }
226
+ if (!result.prepare.ready) {
227
+ process.exit(2);
228
+ }
229
+ process.exit(0);
230
+ }
231
+
232
+ function printReport(result: CheckResult): void {
233
+ console.log("OpenUISpec Check");
234
+ console.log("================");
235
+ console.log(`Target: ${result.target}`);
236
+
237
+ const totalValidation = result.validation.total_errors;
238
+ const totalSemantic = result.semantic.total_errors;
239
+
240
+ console.log(`\nSchema validation: ${totalValidation === 0 ? "PASS" : `FAIL (${totalValidation} error(s))`}`);
241
+ if (totalValidation > 0) {
242
+ for (const group of result.validation.groups) {
243
+ if (group.errors.length > 0) {
244
+ console.log(` ${group.group}: ${group.errors.length} error(s)`);
245
+ for (const err of group.errors.slice(0, 3)) {
246
+ console.log(` [${err.file}] ${err.path}: ${err.message}`);
247
+ }
248
+ if (group.errors.length > 3) {
249
+ console.log(` ... and ${group.errors.length - 3} more`);
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ console.log(`Semantic lint: ${totalSemantic === 0 ? "PASS" : `FAIL (${totalSemantic} error(s))`}`);
256
+ if (totalSemantic > 0) {
257
+ for (const err of result.semantic.errors.slice(0, 5)) {
258
+ console.log(` [${err.path}] ${err.message}`);
259
+ }
260
+ if (result.semantic.errors.length > 5) {
261
+ console.log(` ... and ${result.semantic.errors.length - 5} more`);
262
+ }
263
+ }
264
+
265
+ console.log(`\nPrepare readiness: ${result.prepare.ready ? "READY" : "NOT READY"}`);
266
+ console.log(` Mode: ${result.prepare.mode}`);
267
+ if (result.prepare.missing.length > 0) {
268
+ console.log(` Missing platform decisions: ${result.prepare.missing.join(", ")}`);
269
+ }
270
+ for (const w of result.prepare.warnings) {
271
+ console.log(` Warning: ${w}`);
272
+ }
273
+
274
+ const totalErrors = result.validation.total_errors + result.semantic.total_errors;
275
+ console.log(
276
+ `\nResult: ${totalErrors === 0 && result.prepare.ready ? "ALL CLEAR" : "ACTION NEEDED"}`,
277
+ );
278
+ }
279
+
280
+ // Direct execution
281
+ const isDirectRun =
282
+ process.argv[1]?.endsWith("check/index.ts") ||
283
+ process.argv[1]?.endsWith("check/index.js");
284
+
285
+ if (isDirectRun) {
286
+ runCheck(process.argv.slice(2));
287
+ }
package/cli/init.ts CHANGED
@@ -261,12 +261,23 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
261
261
  3. \`examples/taskflow/openuispec/\` — complete working example with all file types
262
262
  4. \`schema/\` — JSON Schemas for validation
263
263
 
264
+ ## MCP Tools (recommended for AI assistants)
265
+
266
+ When the openuispec MCP server is configured (see \`.claude.json\`), AI assistants should use these tools instead of CLI commands:
267
+
268
+ | Tool | When to use |
269
+ |------|-------------|
270
+ | \`openuispec_prepare\` | **Before any UI code generation.** Returns spec context, platform config, and constraints. |
271
+ | \`openuispec_check\` | After editing spec files. Validates schema + semantics + readiness. |
272
+ | \`openuispec_status\` | To understand cross-target state (baselines, drift, next steps). |
273
+ | \`openuispec_validate\` | Schema-only validation, optionally by group. |
274
+ | \`openuispec_drift\` | Detect spec changes since last snapshot. |
275
+
264
276
  ## CLI commands
265
277
 
266
278
  \`\`\`bash
267
279
  openuispec validate # Validate spec files against schemas
268
280
  openuispec validate semantic # Run semantic cross-reference linting
269
- openuispec validate screens # Validate only screens
270
281
  openuispec configure-target ${targets[0]} [--defaults] # Configure target stack; --defaults stays unconfirmed
271
282
  openuispec status # Show cross-target baseline/drift status
272
283
  openuispec drift --target ${targets[0]} --explain # Explain semantic spec drift
@@ -298,111 +309,90 @@ ${RULES_START_MARKER}
298
309
  # Spec files are the single source of truth for all UI across platforms.
299
310
  # Targets: ${targetList}
300
311
 
301
- ## IMPORTANTRead the specification before working with spec files
312
+ ## MANDATORYUI work requires OpenUISpec tools
302
313
 
303
- The spec format, file schemas, and generation rules are defined in the installed \`openuispec\` package.
304
- You MUST read the reference files listed below before creating, editing, or generating from any spec file.
305
- Do NOT guess the file format — skipping this step will produce invalid YAML that fails validation.
314
+ When the user's request involves UI screens, navigation, layout, tokens, flows, localization,
315
+ or any visual/structural change you MUST use the OpenUISpec tools before writing any code.
306
316
 
307
- **Find the package in this order:**
308
- 1. \`node_modules/openuispec/\` (project dependency)
309
- 2. Run \`npm root -g\` → \`<prefix>/openuispec/\` (global install)
310
- 3. Online: \`https://openuispec.rsteam.uz/llms-full.txt\` (if not installed)
317
+ ### MCP Tools (use these when available)
311
318
 
312
- **Reference files inside the package (read in this order):**
313
- 1. \`README.md\` — schema tables, file format reference, root wrapper keys
314
- 2. \`spec/openuispec-v0.1.md\` — full specification (contracts, layout, expressions, adaptive, etc.)
315
- 3. \`examples/taskflow/openuispec/\` — complete working example with all file types
316
- 4. \`schema/\` — JSON Schemas for every file type
319
+ Call these MCP tools directly. They return structured JSON with everything you need.
317
320
 
318
- These files are updated with each package version. Always read from the installed package,
319
- not from cached or memorized content, to ensure you use the latest spec.
321
+ 1. **Before ANY UI code generation or modification:**
322
+ Call \`openuispec_prepare\` with the target platform. This returns the spec context,
323
+ platform config, generation constraints, and semantic changes. Do not skip this step.
320
324
 
321
- ## What is OpenUISpec
322
- OpenUISpec is a YAML-based spec format that describes an app's UI semantically — tokens, screens, flows, and platform overrides. AI reads the spec and generates native code (SwiftUI, Compose, React). AI reads native code and updates the spec. The spec is the sync layer between platforms.
325
+ 2. **After editing spec files:**
326
+ Call \`openuispec_check\` to validate schema + semantic lint + prepare readiness.
323
327
 
324
- ## Spec location
325
- - Spec root: \`${specDir}/\`
326
- - Manifest: \`${specDir}/openuispec.yaml\` — always read this first.
327
- - Tokens: \`${specDir}/tokens/\`
328
- - Screens: \`${specDir}/screens/\`
329
- - Flows: \`${specDir}/flows/\`
330
- - Contracts: \`${specDir}/contracts/\`
331
- - Platform: \`${specDir}/platform/\`
332
- - Locales: \`${specDir}/locales/\`
328
+ 3. **To understand project state:**
329
+ Call \`openuispec_status\` for cross-target summary.
333
330
 
334
- **Note:** These are the default paths. Actual paths are in \`includes:\` in \`openuispec.yaml\` and may use relative paths. Always read \`openuispec.yaml\` to find the real directories.
331
+ 4. **To detect what changed:**
332
+ Call \`openuispec_drift\` with \`explain: true\` to see property-level spec changes.
335
333
 
336
- ## If spec directories are empty (first-time setup)
337
- This means the project has existing UI code but hasn't been specced yet. Your job:
338
-
339
- 1. **Read the spec first** — find and read \`spec/openuispec-v0.1.md\` from the installed package.
340
- 2. **Find existing screens** — scan the codebase for UI screen files.
341
- 3. **Create stubs** — for each screen, create \`${specDir}/screens/<name>.yaml\` with:
342
- \`\`\`yaml
343
- screen_name:
344
- semantic: "Brief description of what this screen does"
345
- status: stub
346
- layout:
347
- type: scroll_vertical
348
- \`\`\`
349
- 4. **Extract tokens** — scan for colors, fonts, spacing and create files in \`${specDir}/tokens/\`.
350
- 5. **Update the manifest** — fill in \`data_model\`, \`api.endpoints\`, and \`generation.code_roots.backend\` in \`${specDir}/openuispec.yaml\`.
351
-
352
- ## OpenUISpec Source Of Truth
353
-
354
- OpenUISpec spec files are the primary source of truth for UI behavior across platforms.
355
-
356
- ### Start from spec when:
357
- - the request changes screen structure
358
- - the request changes navigation
359
- - the request changes fields, actions, validation, or data binding
360
- - the request changes tokens, variants, contracts, flows, or localization
361
- - the request affects more than one platform
362
- - the request is phrased in product/UI terms rather than platform-code terms
363
-
364
- Spec-first workflow:
365
- 1. Read \`${specDir}/openuispec.yaml\` and the relevant spec files first.
366
- 2. Update the spec first.
367
- 3. Update the affected generated/native UI code to match the spec.
368
- 4. Run \`openuispec validate\`.
369
- 5. Run \`openuispec validate semantic\`.
370
- 6. Run \`openuispec drift --target <target> --explain\` to inspect semantic changes since that target's baseline.
371
- 7. Run \`openuispec prepare --target <target>\` to build the target work bundle for that target. In \`bootstrap\` mode it provides first-generation constraints; in \`update\` mode it provides drift-based update scope.
372
- If the target stack was filled from defaults, stop and ask the user to confirm or change it before implementation.
373
- 8. Verify the affected UI targets build/run if possible.
374
- 9. Only then run \`openuispec drift --snapshot --target <target>\` for affected targets, after that target output directory exists.
375
- 10. Run \`openuispec drift --target <target> --explain\` again to confirm no spec changes remain for that target.
376
- 11. Use \`openuispec status\` to see which other targets are still behind the updated spec.
377
-
378
- ### Start from platform code when:
379
- - the change is platform-specific polish
380
- - the change is a local bug fix that does not alter shared semantic behavior
381
- - the request explicitly asks for an iOS-only, Android-only, or web-only adjustment
382
-
383
- Platform-first workflow:
384
- 1. Update native/platform code.
385
- 2. If the change affects shared semantics, sync the spec afterward.
386
- 3. If the change is intentionally platform-specific, document it in \`platform/*.yaml\` when appropriate.
387
-
388
- ### Never do this:
389
- - Do not snapshot drift immediately after changing spec unless the UI code has also been updated.
390
- - Do not treat \`openuispec drift\` as proof that generated UI matches the spec.
391
- - Do not skip \`--explain\` / \`prepare\` when another platform needs to catch up with shared spec changes.
392
- - Do not modify generated UI without checking whether the spec must change first.
393
- - Do not use \`configure-target --defaults\` as silent approval for implementation. Ask the user to confirm the stack first.
334
+ 5. **For schema validation only:**
335
+ Call \`openuispec_validate\` optionally with specific groups.
394
336
 
395
- ## CLI commands
337
+ ### CLI fallback (when MCP is not available)
338
+
339
+ If MCP tools are not available, use these CLI commands with \`--json\` flag:
340
+ - \`openuispec prepare --target <t> --json\` — build AI-ready work bundle
341
+ - \`openuispec check --target <t> --json\` — composite validation
342
+ - \`openuispec status --json\` — cross-target status
343
+ - \`openuispec drift --target <t> --explain --json\` — semantic drift
344
+ - \`openuispec validate [group...] --json\` — schema validation
345
+
346
+ ### Other CLI commands
396
347
  - \`openuispec init\` — scaffold a new spec project
397
- - \`openuispec validate [group...]\`validate spec files against schemas
398
- - \`openuispec validate semantic\`run semantic cross-reference linting
399
- - \`openuispec drift --target <t>\` — check for spec drift
400
- - \`openuispec drift --target <t> --explain\` — explain semantic spec drift since the target baseline
401
- - \`openuispec drift --snapshot --target <t>\` — snapshot current state after the target output exists
402
- - \`openuispec prepare --target <t>\` — build the target work bundle and check whether stack confirmation is still pending
403
- - \`openuispec status\` — show cross-target baseline/drift status
348
+ - \`openuispec drift --snapshot --target <t>\` snapshot current state (only after UI code is updated)
349
+ - \`openuispec configure-target <t>\`configure target platform stack
404
350
  - \`openuispec update-rules\` — update AI rules to match installed package version
405
- - \`openuispec drift --all\` — include stubs in drift check
351
+
352
+ ## Spec format reference
353
+
354
+ The spec format, schemas, and generation rules are in the installed \`openuispec\` package.
355
+ You MUST read the reference files before creating or editing spec files — do NOT guess the format.
356
+
357
+ **Find the package:** \`node_modules/openuispec/\` or run \`npm root -g\` → \`<prefix>/openuispec/\`.
358
+ **Online fallback:** \`https://openuispec.rsteam.uz/llms-full.txt\`
359
+
360
+ **Reference files (read in order):**
361
+ 1. \`README.md\` — schema tables, file format, root wrapper keys
362
+ 2. \`spec/openuispec-v0.1.md\` — full specification
363
+ 3. \`examples/taskflow/openuispec/\` — complete working example
364
+ 4. \`schema/\` — JSON Schemas for every file type
365
+
366
+ ## Spec location
367
+ - Spec root: \`${specDir}/\` — read \`${specDir}/openuispec.yaml\` first for actual paths.
368
+ - Default dirs: tokens/, screens/, flows/, contracts/, platform/, locales/
369
+
370
+ ## When to start from spec vs platform code
371
+
372
+ **Spec-first** (use \`openuispec_prepare\` or \`openuispec prepare\`):
373
+ - Screen structure, navigation, fields, actions, validation, data binding changes
374
+ - Token, variant, contract, flow, or localization changes
375
+ - Changes affecting multiple platforms
376
+ - Requests in product/UI terms
377
+
378
+ **Platform-first** (skip spec tools):
379
+ - Platform-specific polish (iOS-only, Android-only, web-only)
380
+ - Local bug fixes that don't alter shared semantic behavior
381
+
382
+ ## If spec directories are empty (first-time setup)
383
+
384
+ Read \`spec/openuispec-v0.1.md\` from the package first, then:
385
+ 1. Scan codebase for UI screens → create \`${specDir}/screens/<name>.yaml\` as \`status: stub\`
386
+ 2. Extract tokens (colors, fonts, spacing) → \`${specDir}/tokens/\`
387
+ 3. Create contract extensions → \`${specDir}/contracts/\`
388
+ 4. Create locale files → \`${specDir}/locales/<locale>.json\`
389
+ 5. Fill in \`data_model\`, \`api.endpoints\` in \`${specDir}/openuispec.yaml\`
390
+
391
+ ## Rules
392
+ - Do not snapshot drift unless the UI code has also been updated.
393
+ - Do not modify generated UI without checking whether the spec must change first.
394
+ - Do not use \`configure-target --defaults\` as silent approval — ask the user to confirm.
395
+ - Always read spec format from the installed package, not from cached/memorized content.
406
396
  ${RULES_END_MARKER}
407
397
  `;
408
398
  }
@@ -479,6 +469,30 @@ export function updateRules(): void {
479
469
  } else {
480
470
  console.log(`\nAI rules updated to v${version}`);
481
471
  }
472
+
473
+ // Ensure MCP server is configured
474
+ const claudeJsonPath = join(cwd, ".claude.json");
475
+ const mcpConfig = {
476
+ command: "npx",
477
+ args: ["openuispec-mcp"],
478
+ };
479
+
480
+ try {
481
+ let claudeJson: Record<string, any> = {};
482
+ try {
483
+ claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf-8"));
484
+ } catch {
485
+ // file doesn't exist or isn't valid JSON — start fresh
486
+ }
487
+ if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
488
+ if (!claudeJson.mcpServers.openuispec) {
489
+ claudeJson.mcpServers.openuispec = mcpConfig;
490
+ writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
491
+ console.log(` create .claude.json (MCP server configured)`);
492
+ }
493
+ } catch {
494
+ // non-critical — skip silently
495
+ }
482
496
  }
483
497
 
484
498
  /**
@@ -744,6 +758,33 @@ export async function init(argv: string[] = []): Promise<void> {
744
758
  }
745
759
  }
746
760
 
761
+ // ── MCP server configuration ────────────────────────────────────
762
+
763
+ const claudeJsonPath = join(cwd, ".claude.json");
764
+ const mcpConfig = {
765
+ command: "npx",
766
+ args: ["openuispec-mcp"],
767
+ };
768
+
769
+ try {
770
+ let claudeJson: Record<string, any> = {};
771
+ try {
772
+ claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf-8"));
773
+ } catch {
774
+ // file doesn't exist or isn't valid JSON — start fresh
775
+ }
776
+ if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
777
+ if (!claudeJson.mcpServers.openuispec) {
778
+ claudeJson.mcpServers.openuispec = mcpConfig;
779
+ writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
780
+ if (!quiet) console.log(` create .claude.json (MCP server configured)`);
781
+ } else {
782
+ if (!quiet) console.log(` skip .claude.json (openuispec MCP already configured)`);
783
+ }
784
+ } catch {
785
+ if (!quiet) console.log(` skip .claude.json (could not configure MCP server)`);
786
+ }
787
+
747
788
  if (answers.configureTargets) {
748
789
  if (!quiet) console.log("\nConfiguring target stacks...\n");
749
790
  const { runConfigureTarget } = await import("./configure-target.js");
@@ -762,10 +803,14 @@ Done! Your spec project is ready at ./${answers.specDir}/
762
803
 
763
804
  Getting started (new project):
764
805
  1. Edit ${answers.specDir}/openuispec.yaml — define your data model and API
765
- 2. Create screens in ${answers.specDir}/screens/ (one YAML per screen)
766
- 3. Create flows in ${answers.specDir}/flows/ (multi-step navigation)
767
- 4. Ask AI to generate native code from the spec
768
- 5. Run \`openuispec drift --snapshot --target ${answers.targets[0]}\` to baseline the first accepted target state after that target output directory exists
806
+ 2. Create tokens in ${answers.specDir}/tokens/ (colors, typography, spacing, etc.)
807
+ 3. Create contract extensions in ${answers.specDir}/contracts/ (visual variants for the 7 built-in contracts)
808
+ 4. Create screens in ${answers.specDir}/screens/ (one YAML per screen)
809
+ 5. Create flows in ${answers.specDir}/flows/ (multi-step navigation)
810
+ 6. Create locale files in ${answers.specDir}/locales/ (one JSON per supported locale)
811
+ 7. Run \`openuispec validate\` and \`openuispec validate semantic\` to check everything
812
+ 8. Ask AI to generate native code from the spec
813
+ 9. Run \`openuispec drift --snapshot --target ${answers.targets[0]}\` to baseline the first accepted target state after that target output directory exists
769
814
 
770
815
  Getting started (existing project):
771
816
  1. Ask AI to read your existing UI code and generate spec files:
@@ -785,6 +830,7 @@ Commands:
785
830
  openuispec drift --snapshot --target ios Save current state + git baseline after target output exists
786
831
 
787
832
  AI rules have been added to CLAUDE.md and AGENTS.md.
833
+ MCP server configured in .claude.json (AI assistants will use openuispec tools automatically).
788
834
 
789
835
  Docs: https://openuispec.rsteam.uz
790
836
  `);
package/drift/index.ts CHANGED
@@ -137,8 +137,7 @@ export function hasStatusSemantics(relPath: string): boolean {
137
137
  export function discoverSpecFiles(projectDir: string): string[] {
138
138
  const manifest = join(projectDir, "openuispec.yaml");
139
139
  if (!existsSync(manifest)) {
140
- console.error(`Error: No openuispec.yaml found in ${projectDir}`);
141
- process.exit(1);
140
+ throw new Error(`No openuispec.yaml found in ${projectDir}`);
142
141
  }
143
142
 
144
143
  const doc = YAML.parse(readFileSync(manifest, "utf-8"));
@@ -433,11 +432,10 @@ export function findProjectDir(cwd: string): string {
433
432
  return dir;
434
433
  }
435
434
  }
436
- console.error(
437
- "Error: No openuispec.yaml found.\n" +
435
+ throw new Error(
436
+ "No openuispec.yaml found. " +
438
437
  "Run from a directory containing openuispec.yaml or an openuispec/ subdirectory."
439
438
  );
440
- process.exit(1);
441
439
  }
442
440
 
443
441
  /** Read and parse the manifest YAML. */
@@ -655,8 +653,7 @@ export function loadTargetDrift(
655
653
  const projectName = readProjectName(projectDir);
656
654
  const statePath = stateFilePath(projectDir, projectName, target);
657
655
  if (!existsSync(statePath)) {
658
- console.error(missingSnapshotMessage(cwd, projectDir, projectName, target));
659
- process.exit(1);
656
+ throw new Error(missingSnapshotMessage(cwd, projectDir, projectName, target));
660
657
  }
661
658
 
662
659
  const state: StateFile = JSON.parse(readFileSync(statePath, "utf-8"));
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ /**
3
+ * OpenUISpec MCP Server
4
+ *
5
+ * Exposes OpenUISpec CLI commands as MCP tools so AI assistants
6
+ * can call them directly instead of relying on CLAUDE.md instructions.
7
+ *
8
+ * Usage:
9
+ * npx openuispec-mcp # stdio transport
10
+ * OPENUISPEC_PROJECT_DIR=/path npx openuispec-mcp # explicit project dir
11
+ */
12
+
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { z } from "zod";
16
+ import { readFileSync } from "node:fs";
17
+ import { join, dirname } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { SUPPORTED_TARGETS } from "../drift/index.js";
20
+ import { buildPrepareResult } from "../prepare/index.js";
21
+ import { buildCheckResult } from "../check/index.js";
22
+ import { buildStatusResult } from "../status/index.js";
23
+ import { buildValidateResult } from "../schema/validate.js";
24
+ import { loadTargetDrift } from "../drift/index.js";
25
+
26
+ // ── resolve project cwd ──────────────────────────────────────────────
27
+
28
+ const projectCwd = process.env.OPENUISPEC_PROJECT_DIR || process.cwd();
29
+
30
+ // ── read package version ─────────────────────────────────────────────
31
+
32
+ function getPackageVersion(): string {
33
+ try {
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
36
+ return pkg.version ?? "unknown";
37
+ } catch {
38
+ return "unknown";
39
+ }
40
+ }
41
+
42
+ // ── shared tool helpers ──────────────────────────────────────────────
43
+
44
+ const targetSchema = z.enum(SUPPORTED_TARGETS).describe("Target platform");
45
+
46
+ function formatError(err: unknown): string {
47
+ return err instanceof Error ? err.message : String(err);
48
+ }
49
+
50
+ function toolResult(data: unknown): { content: [{ type: "text"; text: string }] } {
51
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
52
+ }
53
+
54
+ function toolError(err: unknown): { content: [{ type: "text"; text: string }]; isError: true } {
55
+ return { content: [{ type: "text" as const, text: `Error: ${formatError(err)}` }], isError: true };
56
+ }
57
+
58
+ // ── create server ────────────────────────────────────────────────────
59
+
60
+ const server = new McpServer(
61
+ {
62
+ name: "openuispec",
63
+ version: getPackageVersion(),
64
+ },
65
+ {
66
+ instructions: `This project uses OpenUISpec — a semantic UI specification format.
67
+ Spec files (YAML) are the single source of truth for all UI across platforms.
68
+
69
+ MANDATORY WORKFLOW for any UI-related request (screens, navigation, layout, tokens, flows, localization):
70
+ 1. BEFORE writing or modifying any UI code, call openuispec_prepare with the target platform.
71
+ This returns the spec context, platform config, and generation constraints you must follow.
72
+ 2. If the request requires spec changes, update the spec files FIRST, then call openuispec_check to validate.
73
+ 3. Only then generate or update the platform UI code based on the prepare output.
74
+
75
+ Skip these tools ONLY when the request is purely non-UI (API logic, database, infrastructure, etc.)
76
+ or explicitly platform-specific polish that doesn't affect shared UI semantics.`,
77
+ }
78
+ );
79
+
80
+ // ── tool: openuispec_prepare ─────────────────────────────────────────
81
+
82
+ server.registerTool(
83
+ "openuispec_prepare",
84
+ {
85
+ description: "Build AI-ready work bundle for a target platform. REQUIRED before any UI code generation. Returns spec context, platform config, semantic changes, and generation constraints.",
86
+ inputSchema: { target: targetSchema },
87
+ },
88
+ async ({ target }) => {
89
+ try {
90
+ return toolResult(buildPrepareResult(target, projectCwd));
91
+ } catch (err) {
92
+ return toolError(err);
93
+ }
94
+ }
95
+ );
96
+
97
+ // ── tool: openuispec_check ───────────────────────────────────────────
98
+
99
+ server.registerTool(
100
+ "openuispec_check",
101
+ {
102
+ description: "Run composite validation: schema validation, semantic linting, and prepare readiness check. Call after spec edits to verify correctness.",
103
+ inputSchema: { target: targetSchema },
104
+ },
105
+ async ({ target }) => {
106
+ try {
107
+ return toolResult(buildCheckResult(target, projectCwd));
108
+ } catch (err) {
109
+ return toolError(err);
110
+ }
111
+ }
112
+ );
113
+
114
+ // ── tool: openuispec_status ──────────────────────────────────────────
115
+
116
+ server.registerTool(
117
+ "openuispec_status",
118
+ {
119
+ description: "Show cross-target status summary: baseline, drift, and recommended next steps for all configured targets. Good starting point to understand project state.",
120
+ },
121
+ async () => {
122
+ try {
123
+ return toolResult(buildStatusResult(projectCwd));
124
+ } catch (err) {
125
+ return toolError(err);
126
+ }
127
+ }
128
+ );
129
+
130
+ // ── tool: openuispec_validate ────────────────────────────────────────
131
+
132
+ server.registerTool(
133
+ "openuispec_validate",
134
+ {
135
+ description: "Validate spec files against JSON schemas. Returns validation errors grouped by type (manifest, tokens, screens, flows, platform, locales, contracts, semantic).",
136
+ inputSchema: {
137
+ groups: z
138
+ .array(z.enum(["manifest", "tokens", "screens", "flows", "platform", "locales", "contracts", "semantic"]))
139
+ .optional()
140
+ .describe("Specific groups to validate. If omitted, validates all groups."),
141
+ },
142
+ },
143
+ async ({ groups }) => {
144
+ try {
145
+ return toolResult(buildValidateResult(groups, projectCwd));
146
+ } catch (err) {
147
+ return toolError(err);
148
+ }
149
+ }
150
+ );
151
+
152
+ // ── tool: openuispec_drift ───────────────────────────────────────────
153
+
154
+ server.registerTool(
155
+ "openuispec_drift",
156
+ {
157
+ description: "Detect spec drift since last snapshot. Shows which spec files changed, were added, or removed. Use explain to see property-level changes.",
158
+ inputSchema: {
159
+ target: targetSchema,
160
+ explain: z.boolean().optional().default(false).describe("Include semantic explanation of changes"),
161
+ },
162
+ },
163
+ async ({ target, explain }) => {
164
+ try {
165
+ const { result } = loadTargetDrift(projectCwd, target, false, explain);
166
+ return toolResult(result);
167
+ } catch (err) {
168
+ return toolError(err);
169
+ }
170
+ }
171
+ );
172
+
173
+ // ── start server ─────────────────────────────────────────────────────
174
+
175
+ async function main() {
176
+ const transport = new StdioServerTransport();
177
+ await server.connect(transport);
178
+ }
179
+
180
+ main().catch((err) => {
181
+ console.error("Failed to start OpenUISpec MCP server:", err);
182
+ process.exit(1);
183
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
@@ -11,7 +11,9 @@
11
11
  },
12
12
  "files": [
13
13
  "cli/",
14
+ "check/",
14
15
  "drift/",
16
+ "mcp-server/",
15
17
  "prepare/",
16
18
  "status/",
17
19
  "schema/",
@@ -22,7 +24,8 @@
22
24
  "LICENSE"
23
25
  ],
24
26
  "bin": {
25
- "openuispec": "./cli/index.ts"
27
+ "openuispec": "./cli/index.ts",
28
+ "openuispec-mcp": "./mcp-server/index.ts"
26
29
  },
27
30
  "scripts": {
28
31
  "test": "node --import tsx --test tests/*.test.ts",
@@ -40,6 +43,7 @@
40
43
  "postinstall": "echo \"\\n ✓ openuispec installed — if upgrading, run: openuispec update-rules\\n\""
41
44
  },
42
45
  "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.27.1",
43
47
  "ajv": "^8.17.1",
44
48
  "ajv-formats": "^3.0.1",
45
49
  "tsx": "^4.19.4",
package/prepare/index.ts CHANGED
@@ -116,7 +116,7 @@ interface PrepareBootstrapBundle {
116
116
  reference_examples: string[];
117
117
  }
118
118
 
119
- interface PrepareResult {
119
+ export interface PrepareResult {
120
120
  mode: "bootstrap" | "update";
121
121
  project: string;
122
122
  target: string;
@@ -1180,8 +1180,7 @@ function buildUpdatePrepareResult(cwd: string, target: string): PrepareResult {
1180
1180
  };
1181
1181
  }
1182
1182
 
1183
- function buildPrepareResult(target: string): PrepareResult {
1184
- const cwd = process.cwd();
1183
+ export function buildPrepareResult(target: string, cwd: string = process.cwd()): PrepareResult {
1185
1184
  const projectDir = findProjectDir(cwd);
1186
1185
  const projectName = readProjectName(projectDir);
1187
1186
  const outputDir = resolveOutputDir(projectDir, projectName, target);
@@ -755,6 +755,36 @@ function findProjectDir(cwd: string): string {
755
755
  export { buildAjv, readIncludes, GROUPS };
756
756
  export type { JsonGroupResult, JsonError };
757
757
 
758
+ interface ValidateResult {
759
+ total_errors: number;
760
+ groups: JsonGroupResult[];
761
+ }
762
+
763
+ export function buildValidateResult(
764
+ groups?: string[],
765
+ cwd: string = process.cwd(),
766
+ ): ValidateResult {
767
+ const projectDir = findProjectDir(cwd);
768
+ const includes = readIncludes(projectDir);
769
+ const ajv = buildAjv();
770
+
771
+ const keys =
772
+ groups && groups.length > 0
773
+ ? groups.filter((k) => k in GROUPS)
774
+ : Object.keys(GROUPS);
775
+
776
+ const results: JsonGroupResult[] = [];
777
+ let totalErrors = 0;
778
+
779
+ for (const key of keys) {
780
+ const result = GROUPS[key].collectJson(ajv, projectDir, includes, key);
781
+ results.push(result);
782
+ totalErrors += result.errors.length;
783
+ }
784
+
785
+ return { total_errors: totalErrors, groups: results };
786
+ }
787
+
758
788
  export function runValidate(argv: string[]): void {
759
789
  const jsonMode = argv.includes("--json");
760
790
  const filteredArgs = argv.filter((a) => a !== "--json");
package/status/index.ts CHANGED
@@ -45,7 +45,7 @@ interface TargetStatus {
45
45
  note?: string;
46
46
  }
47
47
 
48
- interface StatusResult {
48
+ export interface StatusResult {
49
49
  project: string;
50
50
  targets: TargetStatus[];
51
51
  }
@@ -141,8 +141,7 @@ function buildTargetStatus(cwd: string, projectDir: string, projectName: string,
141
141
  };
142
142
  }
143
143
 
144
- function buildStatusResult(): StatusResult {
145
- const cwd = process.cwd();
144
+ export function buildStatusResult(cwd: string = process.cwd()): StatusResult {
146
145
  const projectDir = findProjectDir(cwd);
147
146
  const projectName = readProjectName(projectDir);
148
147
  const targets = allTargets(projectDir, projectName).map((target) =>
@@ -187,7 +186,7 @@ function printReport(result: StatusResult): void {
187
186
 
188
187
  export function runStatus(argv: string[]): void {
189
188
  const isJson = argv.includes("--json");
190
- const result = buildStatusResult();
189
+ const result = buildStatusResult(process.cwd());
191
190
 
192
191
  if (isJson) {
193
192
  console.log(JSON.stringify(result, null, 2));