jui-tools-mcp-server 2.5.0

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 (172) hide show
  1. package/.github/workflows/test.yml +35 -0
  2. package/README.md +148 -0
  3. package/data/attribute_definitions.json +4248 -0
  4. package/data/attribute_semantics.json +137 -0
  5. package/data/binding_semantics.json +115 -0
  6. package/data/component_metadata.json +769 -0
  7. package/data/coverage.json +114 -0
  8. package/data/screen_identity.json +350 -0
  9. package/dist/cli_runner.d.ts +10 -0
  10. package/dist/cli_runner.js +33 -0
  11. package/dist/config.d.ts +19 -0
  12. package/dist/config.js +67 -0
  13. package/dist/data/component_metadata.d.ts +30 -0
  14. package/dist/data/component_metadata.js +498 -0
  15. package/dist/data/derived.d.ts +198 -0
  16. package/dist/data/derived.js +293 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +130 -0
  19. package/dist/spec_loader.d.ts +110 -0
  20. package/dist/spec_loader.js +445 -0
  21. package/dist/tools/api/list_api_models.d.ts +10 -0
  22. package/dist/tools/api/list_api_models.js +37 -0
  23. package/dist/tools/api/list_api_specs.d.ts +19 -0
  24. package/dist/tools/api/list_api_specs.js +42 -0
  25. package/dist/tools/api/preview_api_model_sync.d.ts +11 -0
  26. package/dist/tools/api/preview_api_model_sync.js +34 -0
  27. package/dist/tools/context/get_project_config.d.ts +3 -0
  28. package/dist/tools/context/get_project_config.js +20 -0
  29. package/dist/tools/context/list_component_specs.d.ts +3 -0
  30. package/dist/tools/context/list_component_specs.js +35 -0
  31. package/dist/tools/context/list_layouts.d.ts +3 -0
  32. package/dist/tools/context/list_layouts.js +118 -0
  33. package/dist/tools/context/list_screen_specs.d.ts +3 -0
  34. package/dist/tools/context/list_screen_specs.js +37 -0
  35. package/dist/tools/context/read_layout_file.d.ts +3 -0
  36. package/dist/tools/context/read_layout_file.js +25 -0
  37. package/dist/tools/context/read_spec_file.d.ts +3 -0
  38. package/dist/tools/context/read_spec_file.js +27 -0
  39. package/dist/tools/doc/doc_generate_component.d.ts +3 -0
  40. package/dist/tools/doc/doc_generate_component.js +38 -0
  41. package/dist/tools/doc/doc_generate_html.d.ts +3 -0
  42. package/dist/tools/doc/doc_generate_html.js +46 -0
  43. package/dist/tools/doc/doc_generate_spec.d.ts +3 -0
  44. package/dist/tools/doc/doc_generate_spec.js +38 -0
  45. package/dist/tools/doc/doc_init_component.d.ts +3 -0
  46. package/dist/tools/doc/doc_init_component.js +30 -0
  47. package/dist/tools/doc/doc_init_spec.d.ts +3 -0
  48. package/dist/tools/doc/doc_init_spec.js +33 -0
  49. package/dist/tools/doc/doc_rules_init.d.ts +3 -0
  50. package/dist/tools/doc/doc_rules_init.js +25 -0
  51. package/dist/tools/doc/doc_rules_show.d.ts +3 -0
  52. package/dist/tools/doc/doc_rules_show.js +21 -0
  53. package/dist/tools/doc/doc_validate_component.d.ts +3 -0
  54. package/dist/tools/doc/doc_validate_component.js +39 -0
  55. package/dist/tools/doc/doc_validate_spec.d.ts +3 -0
  56. package/dist/tools/doc/doc_validate_spec.js +40 -0
  57. package/dist/tools/jui/jui_build.d.ts +3 -0
  58. package/dist/tools/jui/jui_build.js +31 -0
  59. package/dist/tools/jui/jui_generate_converter.d.ts +3 -0
  60. package/dist/tools/jui/jui_generate_converter.js +43 -0
  61. package/dist/tools/jui/jui_generate_project.d.ts +3 -0
  62. package/dist/tools/jui/jui_generate_project.js +43 -0
  63. package/dist/tools/jui/jui_generate_screen.d.ts +3 -0
  64. package/dist/tools/jui/jui_generate_screen.js +22 -0
  65. package/dist/tools/jui/jui_init.d.ts +3 -0
  66. package/dist/tools/jui/jui_init.js +42 -0
  67. package/dist/tools/jui/jui_migrate_layouts.d.ts +3 -0
  68. package/dist/tools/jui/jui_migrate_layouts.js +25 -0
  69. package/dist/tools/jui/jui_sync_tool.d.ts +3 -0
  70. package/dist/tools/jui/jui_sync_tool.js +33 -0
  71. package/dist/tools/jui/jui_verify.d.ts +3 -0
  72. package/dist/tools/jui/jui_verify.js +43 -0
  73. package/dist/tools/spec/get_binding_rules.d.ts +3 -0
  74. package/dist/tools/spec/get_binding_rules.js +8 -0
  75. package/dist/tools/spec/get_data_source.d.ts +3 -0
  76. package/dist/tools/spec/get_data_source.js +41 -0
  77. package/dist/tools/spec/get_modifier_order.d.ts +3 -0
  78. package/dist/tools/spec/get_modifier_order.js +14 -0
  79. package/dist/tools/spec/get_platform_mapping.d.ts +3 -0
  80. package/dist/tools/spec/get_platform_mapping.js +20 -0
  81. package/dist/tools/spec/get_screen_identity.d.ts +3 -0
  82. package/dist/tools/spec/get_screen_identity.js +22 -0
  83. package/dist/tools/spec/lookup_attribute.d.ts +3 -0
  84. package/dist/tools/spec/lookup_attribute.js +14 -0
  85. package/dist/tools/spec/lookup_component.d.ts +3 -0
  86. package/dist/tools/spec/lookup_component.js +15 -0
  87. package/dist/tools/spec/search_components.d.ts +3 -0
  88. package/dist/tools/spec/search_components.js +14 -0
  89. package/dist/tools/test/test_artifacts_pull.d.ts +3 -0
  90. package/dist/tools/test/test_artifacts_pull.js +48 -0
  91. package/dist/tools/test/test_artifacts_status.d.ts +3 -0
  92. package/dist/tools/test/test_artifacts_status.js +28 -0
  93. package/dist/tools/test/test_generate_description.d.ts +3 -0
  94. package/dist/tools/test/test_generate_description.js +27 -0
  95. package/dist/tools/test/test_generate_flow.d.ts +3 -0
  96. package/dist/tools/test/test_generate_flow.js +29 -0
  97. package/dist/tools/test/test_generate_screen.d.ts +3 -0
  98. package/dist/tools/test/test_generate_screen.js +29 -0
  99. package/dist/tools/test/test_mock_generate.d.ts +3 -0
  100. package/dist/tools/test/test_mock_generate.js +64 -0
  101. package/dist/tools/test/test_report.d.ts +3 -0
  102. package/dist/tools/test/test_report.js +26 -0
  103. package/dist/tools/test/test_validate.d.ts +3 -0
  104. package/dist/tools/test/test_validate.js +43 -0
  105. package/docs/design.md +227 -0
  106. package/install.sh +90 -0
  107. package/package.json +24 -0
  108. package/scripts/fetch-definitions.js +101 -0
  109. package/scripts/fetch-manifest.js +60 -0
  110. package/src/cli_runner.ts +55 -0
  111. package/src/config.ts +88 -0
  112. package/src/data/derived.ts +305 -0
  113. package/src/index.ts +157 -0
  114. package/src/spec_loader.ts +591 -0
  115. package/src/tools/api/list_api_models.ts +44 -0
  116. package/src/tools/api/list_api_specs.ts +53 -0
  117. package/src/tools/api/preview_api_model_sync.ts +41 -0
  118. package/src/tools/context/get_project_config.ts +27 -0
  119. package/src/tools/context/list_component_specs.ts +44 -0
  120. package/src/tools/context/list_layouts.ts +131 -0
  121. package/src/tools/context/list_screen_specs.ts +46 -0
  122. package/src/tools/context/read_layout_file.ts +35 -0
  123. package/src/tools/context/read_spec_file.ts +38 -0
  124. package/src/tools/doc/doc_generate_component.ts +43 -0
  125. package/src/tools/doc/doc_generate_html.ts +40 -0
  126. package/src/tools/doc/doc_generate_spec.ts +43 -0
  127. package/src/tools/doc/doc_init_component.ts +32 -0
  128. package/src/tools/doc/doc_init_spec.ts +37 -0
  129. package/src/tools/doc/doc_rules_init.ts +29 -0
  130. package/src/tools/doc/doc_rules_show.ts +27 -0
  131. package/src/tools/doc/doc_validate_component.ts +47 -0
  132. package/src/tools/doc/doc_validate_spec.ts +48 -0
  133. package/src/tools/jui/jui_build.ts +31 -0
  134. package/src/tools/jui/jui_generate_converter.ts +41 -0
  135. package/src/tools/jui/jui_generate_project.ts +37 -0
  136. package/src/tools/jui/jui_generate_screen.ts +28 -0
  137. package/src/tools/jui/jui_init.ts +38 -0
  138. package/src/tools/jui/jui_migrate_layouts.ts +29 -0
  139. package/src/tools/jui/jui_sync_tool.ts +33 -0
  140. package/src/tools/jui/jui_verify.ts +43 -0
  141. package/src/tools/spec/get_binding_rules.ts +16 -0
  142. package/src/tools/spec/get_data_source.ts +53 -0
  143. package/src/tools/spec/get_modifier_order.ts +22 -0
  144. package/src/tools/spec/get_platform_mapping.ts +28 -0
  145. package/src/tools/spec/get_screen_identity.ts +31 -0
  146. package/src/tools/spec/lookup_attribute.ts +22 -0
  147. package/src/tools/spec/lookup_component.ts +23 -0
  148. package/src/tools/spec/search_components.ts +22 -0
  149. package/src/tools/test/test_artifacts_pull.ts +47 -0
  150. package/src/tools/test/test_artifacts_status.ts +34 -0
  151. package/src/tools/test/test_generate_description.ts +33 -0
  152. package/src/tools/test/test_generate_flow.ts +33 -0
  153. package/src/tools/test/test_generate_screen.ts +33 -0
  154. package/src/tools/test/test_mock_generate.ts +58 -0
  155. package/src/tools/test/test_report.ts +32 -0
  156. package/src/tools/test/test_validate.ts +43 -0
  157. package/tests/__snapshots__/derived.test.ts.snap +279 -0
  158. package/tests/bundled_snapshot.test.ts +155 -0
  159. package/tests/cli_runner.test.ts +120 -0
  160. package/tests/config.test.ts +212 -0
  161. package/tests/derived.test.ts +114 -0
  162. package/tests/helpers.ts +272 -0
  163. package/tests/setup.ts +27 -0
  164. package/tests/snapshot_completeness.test.ts +152 -0
  165. package/tests/spec_loader.test.ts +488 -0
  166. package/tests/tools_cli.test.ts +687 -0
  167. package/tests/tools_context.test.ts +274 -0
  168. package/tests/tools_spec.test.ts +225 -0
  169. package/tests/tools_test.test.ts +284 -0
  170. package/tsconfig.json +15 -0
  171. package/uninstall.sh +46 -0
  172. package/vitest.config.ts +15 -0
@@ -0,0 +1,591 @@
1
+ import { existsSync, readFileSync, statSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join, resolve } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ import {
7
+ BINDING_RULES,
8
+ MODIFIER_ORDER,
9
+ PLATFORM_MAPPING,
10
+ categorizeCommonAttributes,
11
+ } from "./data/derived.js";
12
+
13
+ export interface ComponentMetadata {
14
+ description: string;
15
+ aliases: string[];
16
+ platforms: {
17
+ swift_generated: boolean;
18
+ swift_dynamic: boolean;
19
+ kotlin_generated: boolean;
20
+ kotlin_dynamic: boolean;
21
+ react: boolean;
22
+ };
23
+ platformSpecific: Record<string, Record<string, string>>;
24
+ rules: string[];
25
+ }
26
+
27
+ export interface ComponentSpec {
28
+ name: string;
29
+ description: string;
30
+ aliases: string[];
31
+ platforms: Record<string, boolean>;
32
+ attributes: Record<string, any>;
33
+ bindingBehavior: Record<string, any>;
34
+ platformSpecific: Record<string, any>;
35
+ rules: string[];
36
+ }
37
+
38
+ export type FallbackLayer = "env" | "cwd" | "home" | "bundled";
39
+
40
+ export interface FileInfo {
41
+ /** Which fallback layer provided the file. */
42
+ layer: FallbackLayer;
43
+ /** Absolute path actually loaded. */
44
+ path: string;
45
+ /** ISO 8601 mtime. */
46
+ lastModified: string;
47
+ /** Freshness bucket. "fresh" <= 30d, "aging" <= 90d, "stale" > 90d. */
48
+ freshness: "fresh" | "aging" | "stale";
49
+ }
50
+
51
+ export interface DataSourceInfo {
52
+ attributeDefinitions: FileInfo;
53
+ componentMetadata: FileInfo;
54
+ /// Present only when the optional canon file resolved. Reported for the
55
+ /// same reason as the two above: a caller who cannot see WHICH file the
56
+ /// running server read has no way to tell a stale snapshot from a server
57
+ /// that simply has not been restarted.
58
+ screenIdentity: FileInfo | null;
59
+ bindingSemantics: FileInfo | null;
60
+ /// shared/core/attribute_semantics.json — adjudicated attribute-semantics
61
+ /// contract (cross-effect rulings, plan 33). Optional: absent on older
62
+ /// jsonui-cli checkouts.
63
+ attributeSemantics: FileInfo | null;
64
+ /// conformance/coverage.json — declared-but-unimplemented (component,
65
+ /// attribute, platform) pairs with their recorded reasons. Optional: absent
66
+ /// on older jsonui-cli checkouts.
67
+ coverage: FileInfo | null;
68
+ componentCount: number;
69
+ commonAttributeCount: number;
70
+ }
71
+
72
+ const ALL_PLATFORMS: ComponentMetadata["platforms"] = {
73
+ swift_generated: true,
74
+ swift_dynamic: true,
75
+ kotlin_generated: true,
76
+ kotlin_dynamic: true,
77
+ react: true,
78
+ };
79
+
80
+ const DEFAULT_METADATA: ComponentMetadata = {
81
+ description: "",
82
+ aliases: [],
83
+ platforms: ALL_PLATFORMS,
84
+ platformSpecific: {},
85
+ rules: [],
86
+ };
87
+
88
+ /**
89
+ * Resolution order for each jsonui-cli data file:
90
+ *
91
+ * 1. `$JSONUI_CLI_PATH` env var (custom install / monorepo)
92
+ * 2. `./.jsonui-cli/` in process.cwd() (project-local)
93
+ * 3. `~/.jsonui-cli/` (default location of the jsonui-cli bootstrap installer)
94
+ * 4. `<mcp-server>/data/<file>` (bundled snapshot, populated by
95
+ * `scripts/fetch-definitions.js` at npm install time)
96
+ *
97
+ * A missing higher-priority layer falls through to the next. If all four fail,
98
+ * the loader throws — that's a hard misconfiguration.
99
+ */
100
+ export class SpecLoader {
101
+ private components: Map<string, ComponentSpec> = new Map();
102
+ private commonAttributesRaw: Record<string, any> = {};
103
+ private bindingSemantics: Record<string, any> | null = null;
104
+ private attributeSemantics: Record<string, any> | null = null;
105
+ /// `${component}.${attribute}` -> [{platform, reason, note}] from the
106
+ /// coverage ledger. The ledger tracks pairs that ARE declared (platform
107
+ /// field includes them) but that the platform's codegen does not consume —
108
+ /// the note explains why (backlog, alias normalization, runtime-only, ...).
109
+ private coverageGaps: Map<string, any[]> | null = null;
110
+ private screenIdentity: Record<string, any> | null = null;
111
+ private commonAttributes: any = null;
112
+ private aliasMap: Map<string, string> = new Map();
113
+ private metadata: Record<string, ComponentMetadata> = {};
114
+ private dataSource!: DataSourceInfo;
115
+ /** When this process read the files. Data is cached in memory from then
116
+ * on, so a file edited later is not what the server is serving. */
117
+ private loadedAt: string = new Date().toISOString();
118
+
119
+ constructor(private mcpRootDir: string) {
120
+ this.load();
121
+ }
122
+
123
+ // ----- public API ------------------------------------------------------
124
+
125
+ getComponent(name: string): ComponentSpec | null {
126
+ const key = name.toLowerCase();
127
+ const resolved = this.aliasMap.get(key) || key;
128
+ return this.components.get(resolved) || null;
129
+ }
130
+
131
+ getComponentWithCommon(name: string): any {
132
+ const comp = this.getComponent(name);
133
+ if (!comp) return null;
134
+ return { ...comp, commonAttributes: this.commonAttributes };
135
+ }
136
+
137
+ getAttribute(name: string): any {
138
+ if (this.commonAttributes?.categories) {
139
+ for (const [category, attrs] of Object.entries(
140
+ this.commonAttributes.categories
141
+ )) {
142
+ const attrMap = attrs as Record<string, any>;
143
+ if (attrMap[name]) {
144
+ return this.withImplementationGaps(`common.${name}`, {
145
+ name,
146
+ category,
147
+ scope: "common",
148
+ definition: attrMap[name],
149
+ components: "all",
150
+ });
151
+ }
152
+ }
153
+ }
154
+
155
+ const results: any[] = [];
156
+ for (const comp of this.components.values()) {
157
+ if (comp.attributes[name]) {
158
+ results.push(
159
+ this.withImplementationGaps(`${comp.name}.${name}`, {
160
+ name,
161
+ scope: "component",
162
+ component: comp.name,
163
+ definition: comp.attributes[name],
164
+ bindingBehavior: comp.bindingBehavior?.[name],
165
+ })
166
+ );
167
+ }
168
+ }
169
+
170
+ if (results.length === 1) return results[0];
171
+ if (results.length > 1) return { name, matches: results };
172
+ return null;
173
+ }
174
+
175
+ /// Attach the coverage ledger's record for this (component, attribute) when
176
+ /// one exists: platforms that DECLARE the attribute but whose codegen does
177
+ /// not consume it, each with the recorded reason. Complements the
178
+ /// definition's own `platform` (declared surface) and `platform_reasons`
179
+ /// (why a platform is excluded from the declaration).
180
+ private withImplementationGaps(key: string, result: Record<string, any>) {
181
+ const gaps = this.coverageGaps?.get(key);
182
+ if (gaps && gaps.length > 0) {
183
+ return { ...result, implementationGaps: gaps };
184
+ }
185
+ return result;
186
+ }
187
+
188
+ searchComponents(query: string): any[] {
189
+ const q = query.toLowerCase();
190
+ const results: any[] = [];
191
+
192
+ for (const comp of this.components.values()) {
193
+ let score = 0;
194
+ const matches: string[] = [];
195
+
196
+ if (comp.name.toLowerCase().includes(q)) {
197
+ score += 10;
198
+ matches.push(`name: ${comp.name}`);
199
+ }
200
+ if (comp.description.toLowerCase().includes(q)) {
201
+ score += 5;
202
+ matches.push(`description: ${comp.description}`);
203
+ }
204
+ for (const alias of comp.aliases) {
205
+ if (alias.toLowerCase().includes(q)) {
206
+ score += 8;
207
+ matches.push(`alias: ${alias}`);
208
+ }
209
+ }
210
+ for (const attrName of Object.keys(comp.attributes)) {
211
+ if (attrName.toLowerCase().includes(q)) {
212
+ score += 3;
213
+ matches.push(`attribute: ${attrName}`);
214
+ }
215
+ }
216
+ for (const rule of comp.rules) {
217
+ if (rule.toLowerCase().includes(q)) {
218
+ score += 2;
219
+ matches.push(`rule: ${rule}`);
220
+ }
221
+ }
222
+
223
+ if (score > 0) results.push({ component: comp.name, score, matches });
224
+ }
225
+
226
+ if (this.commonAttributes?.categories) {
227
+ for (const [category, attrs] of Object.entries(
228
+ this.commonAttributes.categories
229
+ )) {
230
+ const attrMap = attrs as Record<string, any>;
231
+ for (const [attrName, attrDef] of Object.entries(attrMap)) {
232
+ if (
233
+ attrName.toLowerCase().includes(q) ||
234
+ (attrDef as any)?.description?.toLowerCase().includes(q)
235
+ ) {
236
+ results.push({
237
+ component: "common",
238
+ score: 4,
239
+ matches: [`common.${category}.${attrName}`],
240
+ });
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ return results.sort((a, b) => b.score - a.score);
247
+ }
248
+
249
+ getModifierOrder(platform?: string): any {
250
+ if (platform) return (MODIFIER_ORDER as any)[platform] || null;
251
+ return MODIFIER_ORDER;
252
+ }
253
+
254
+ /**
255
+ * The canonical screen-identity asset, or null when this checkout predates
256
+ * it. Served verbatim: it is a declaration, not something to restate.
257
+ */
258
+ /** ISO 8601 timestamp of when this process loaded its data files. */
259
+ getLoadedAt(): string {
260
+ return this.loadedAt;
261
+ }
262
+
263
+ /**
264
+ * Paths whose mtime has moved since this process read them.
265
+ *
266
+ * Compares against the mtime recorded AT LOAD, not against a wall-clock
267
+ * timestamp: "was this file edited after I read it" is a question about
268
+ * the file, and comparing a file mtime to Date.now() is a race whenever
269
+ * the two land in the same millisecond.
270
+ */
271
+ getChangedSinceLoad(): string[] {
272
+ const tracked = [
273
+ this.dataSource.attributeDefinitions,
274
+ this.dataSource.componentMetadata,
275
+ this.dataSource.screenIdentity,
276
+ this.dataSource.bindingSemantics,
277
+ this.dataSource.attributeSemantics,
278
+ this.dataSource.coverage,
279
+ ].filter((f): f is FileInfo => f != null);
280
+
281
+ const changed: string[] = [];
282
+ for (const file of tracked) {
283
+ try {
284
+ const now = statSync(file.path).mtime.toISOString();
285
+ if (now !== file.lastModified) changed.push(file.path);
286
+ } catch {
287
+ // Disappeared since load — also a reason to restart.
288
+ changed.push(file.path);
289
+ }
290
+ }
291
+ return changed;
292
+ }
293
+
294
+ getScreenIdentity(): any {
295
+ return this.screenIdentity;
296
+ }
297
+
298
+ getBindingRules(): any {
299
+ // BINDING_RULES carries the author-facing syntax guidance; the
300
+ // canonical resolution SEMANTICS (dot paths, `??` defaults,
301
+ // unresolved-key behavior, negation, coercion — SSoT:
302
+ // shared/core/binding_semantics.json) is attached when available so
303
+ // the MCP never re-states semantics by hand again.
304
+ if (!this.bindingSemantics) return BINDING_RULES;
305
+ return {
306
+ ...BINDING_RULES,
307
+ semanticsVersion: this.bindingSemantics.version,
308
+ semantics: this.bindingSemantics,
309
+ };
310
+ }
311
+
312
+ getPlatformMapping(category?: string): any {
313
+ if (category) return (PLATFORM_MAPPING as any)[category] || null;
314
+ return PLATFORM_MAPPING;
315
+ }
316
+
317
+ listComponents(): string[] {
318
+ return Array.from(this.components.values()).map((c) => c.name);
319
+ }
320
+
321
+ getDataSource(): DataSourceInfo {
322
+ return this.dataSource;
323
+ }
324
+
325
+ // ----- load pipeline ---------------------------------------------------
326
+
327
+ private load(): void {
328
+ const attrResolution = this.resolveFile(
329
+ "shared/core/attribute_definitions.json",
330
+ "data/attribute_definitions.json"
331
+ );
332
+ const metaResolution = this.resolveFile(
333
+ "shared/core/component_metadata.json",
334
+ "data/component_metadata.json"
335
+ );
336
+
337
+ const raw = JSON.parse(readFileSync(attrResolution.path, "utf-8")) as Record<
338
+ string,
339
+ any
340
+ >;
341
+ const metaRaw = JSON.parse(
342
+ readFileSync(metaResolution.path, "utf-8")
343
+ ) as Record<string, any>;
344
+
345
+ let bindingResolution: FileInfo | null = null;
346
+ let screenIdentityResolution: FileInfo | null = null;
347
+
348
+ // Canonical binding-resolution semantics (renderer SSoT track 15).
349
+ // Optional so older jsonui-cli checkouts without the file still load.
350
+ try {
351
+ const semanticsResolution = this.resolveFile(
352
+ "shared/core/binding_semantics.json",
353
+ "data/binding_semantics.json"
354
+ );
355
+ this.bindingSemantics = JSON.parse(
356
+ readFileSync(semanticsResolution.path, "utf-8")
357
+ ) as Record<string, any>;
358
+ bindingResolution = semanticsResolution;
359
+ } catch {
360
+ this.bindingSemantics = null;
361
+ }
362
+
363
+ // Adjudicated attribute-semantics contract (cross-effect, plan 33).
364
+ // Optional for the same backward-compat reason.
365
+ let attributeSemanticsResolution: FileInfo | null = null;
366
+ try {
367
+ const attrSemResolution = this.resolveFile(
368
+ "shared/core/attribute_semantics.json",
369
+ "data/attribute_semantics.json"
370
+ );
371
+ this.attributeSemantics = JSON.parse(
372
+ readFileSync(attrSemResolution.path, "utf-8")
373
+ ) as Record<string, any>;
374
+ attributeSemanticsResolution = attrSemResolution;
375
+ } catch {
376
+ this.attributeSemantics = null;
377
+ }
378
+
379
+ // Canonical screen identity (screen-identity track). Optional so older
380
+ // jsonui-cli checkouts without the file still load.
381
+ try {
382
+ const screenResolution = this.resolveFile(
383
+ "shared/core/screen_identity.json",
384
+ "data/screen_identity.json"
385
+ );
386
+ this.screenIdentity = JSON.parse(
387
+ readFileSync(screenResolution.path, "utf-8")
388
+ ) as Record<string, any>;
389
+ screenIdentityResolution = screenResolution;
390
+ } catch {
391
+ this.screenIdentity = null;
392
+ }
393
+
394
+ // Conformance coverage ledger: declared-but-unimplemented pairs with
395
+ // recorded reasons. Optional for the same backward-compat reason.
396
+ let coverageResolution: FileInfo | null = null;
397
+ try {
398
+ const coverage = this.resolveFile(
399
+ "conformance/coverage.json",
400
+ "data/coverage.json"
401
+ );
402
+ const doc = JSON.parse(readFileSync(coverage.path, "utf-8")) as {
403
+ entries?: Array<Record<string, any>>;
404
+ };
405
+ const gaps = new Map<string, any[]>();
406
+ for (const entry of doc.entries ?? []) {
407
+ const key = `${entry.component}.${entry.attribute}`;
408
+ const list = gaps.get(key) ?? [];
409
+ for (const platform of entry.platforms ?? []) {
410
+ list.push({
411
+ platform,
412
+ reason: entry.reason ?? "",
413
+ note: entry.note ?? "",
414
+ });
415
+ }
416
+ gaps.set(key, list);
417
+ }
418
+ this.coverageGaps = gaps;
419
+ coverageResolution = coverage;
420
+ } catch {
421
+ this.coverageGaps = null;
422
+ }
423
+
424
+ this.metadata = {};
425
+ for (const [name, meta] of Object.entries(metaRaw)) {
426
+ if (name.startsWith("_")) continue;
427
+ if (typeof meta !== "object" || meta === null) continue;
428
+ this.metadata[name] = this.normalizeMetadata(meta);
429
+ }
430
+
431
+ this.commonAttributesRaw = raw.common || {};
432
+ this.commonAttributes = categorizeCommonAttributes(this.commonAttributesRaw);
433
+
434
+ let componentCount = 0;
435
+ for (const [compName, compAttrs] of Object.entries(raw)) {
436
+ if (compName.startsWith("_") || compName === "common") continue;
437
+ if (typeof compAttrs !== "object" || compAttrs === null) continue;
438
+
439
+ // Component alias (`_alias_of` pointer section, jsonui-cli B1): the
440
+ // section carries no attribute copies — register the spelling as an
441
+ // alias of the canonical component instead of building an empty
442
+ // ComponentSpec (Toggle -> Switch is NOT covered by the metadata
443
+ // aliases array, so this is the only route for it).
444
+ const aliasOf = (compAttrs as Record<string, unknown>)["_alias_of"];
445
+ if (typeof aliasOf === "string" && aliasOf.length > 0) {
446
+ this.aliasMap.set(compName.toLowerCase(), aliasOf.toLowerCase());
447
+ continue;
448
+ }
449
+
450
+ const spec = this.buildComponentSpec(compName, compAttrs);
451
+ const key = compName.toLowerCase();
452
+ this.components.set(key, spec);
453
+ for (const alias of spec.aliases) {
454
+ this.aliasMap.set(alias.toLowerCase(), key);
455
+ }
456
+ componentCount++;
457
+ }
458
+
459
+ this.dataSource = {
460
+ attributeDefinitions: attrResolution,
461
+ componentMetadata: metaResolution,
462
+ screenIdentity: screenIdentityResolution,
463
+ bindingSemantics: bindingResolution,
464
+ attributeSemantics: attributeSemanticsResolution,
465
+ coverage: coverageResolution,
466
+ componentCount,
467
+ commonAttributeCount: Object.keys(this.commonAttributesRaw).filter(
468
+ (k) => !k.startsWith("_")
469
+ ).length,
470
+ };
471
+ }
472
+
473
+ private normalizeMetadata(raw: Record<string, any>): ComponentMetadata {
474
+ return {
475
+ description: raw.description ?? "",
476
+ aliases: Array.isArray(raw.aliases) ? raw.aliases : [],
477
+ platforms: {
478
+ ...ALL_PLATFORMS,
479
+ ...(raw.platforms ?? {}),
480
+ },
481
+ platformSpecific:
482
+ typeof raw.platformSpecific === "object" && raw.platformSpecific
483
+ ? raw.platformSpecific
484
+ : {},
485
+ rules: Array.isArray(raw.rules) ? raw.rules : [],
486
+ };
487
+ }
488
+
489
+ private getMetadata(componentName: string): ComponentMetadata {
490
+ return (
491
+ this.metadata[componentName] ?? {
492
+ ...DEFAULT_METADATA,
493
+ description: `${componentName} component`,
494
+ }
495
+ );
496
+ }
497
+
498
+ private buildComponentSpec(
499
+ name: string,
500
+ attrs: Record<string, any>
501
+ ): ComponentSpec {
502
+ const meta = this.getMetadata(name);
503
+
504
+ const attributes: Record<string, any> = {};
505
+ const bindingBehavior: Record<string, any> = {};
506
+
507
+ for (const [attrName, attrDef] of Object.entries(attrs)) {
508
+ if (attrName.startsWith("_")) continue;
509
+ attributes[attrName] = attrDef;
510
+
511
+ const attrType = (attrDef as any)?.type;
512
+ let hasBinding = false;
513
+ if (Array.isArray(attrType)) hasBinding = attrType.includes("binding");
514
+ else if (attrType === "binding") hasBinding = true;
515
+
516
+ if (hasBinding) {
517
+ const declared = (attrDef as any)?.binding_direction;
518
+ bindingBehavior[attrName] = {
519
+ direction: declared === "two-way" ? "two-way" : "read-only",
520
+ };
521
+ }
522
+ }
523
+
524
+ return {
525
+ name,
526
+ description: meta.description || `${name} component`,
527
+ aliases: meta.aliases,
528
+ platforms: meta.platforms,
529
+ attributes,
530
+ bindingBehavior,
531
+ platformSpecific: meta.platformSpecific,
532
+ rules: meta.rules,
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Resolve a jsonui-cli file through the 4-layer fallback and return FileInfo.
538
+ * @param cliRelPath Path inside a jsonui-cli checkout (e.g. "shared/core/attribute_definitions.json")
539
+ * @param bundledRelPath Path inside this MCP's root (e.g. "data/attribute_definitions.json")
540
+ */
541
+ private resolveFile(cliRelPath: string, bundledRelPath: string): FileInfo {
542
+ const candidates: Array<{ path: string; layer: FallbackLayer }> = [];
543
+
544
+ const env = process.env.JSONUI_CLI_PATH;
545
+ if (env) {
546
+ candidates.push({ path: join(resolve(env), cliRelPath), layer: "env" });
547
+ }
548
+ candidates.push({
549
+ path: join(process.cwd(), ".jsonui-cli", cliRelPath),
550
+ layer: "cwd",
551
+ });
552
+ candidates.push({
553
+ path: join(homedir(), ".jsonui-cli", cliRelPath),
554
+ layer: "home",
555
+ });
556
+ candidates.push({
557
+ path: join(this.mcpRootDir, bundledRelPath),
558
+ layer: "bundled",
559
+ });
560
+
561
+ for (const candidate of candidates) {
562
+ if (existsSync(candidate.path)) {
563
+ const mtime = statSync(candidate.path).mtime;
564
+ const ageDays = (Date.now() - mtime.getTime()) / (1000 * 60 * 60 * 24);
565
+ const freshness: FileInfo["freshness"] =
566
+ ageDays <= 30 ? "fresh" : ageDays <= 90 ? "aging" : "stale";
567
+ return {
568
+ layer: candidate.layer,
569
+ path: candidate.path,
570
+ lastModified: mtime.toISOString(),
571
+ freshness,
572
+ };
573
+ }
574
+ }
575
+
576
+ const tried = candidates.map((c) => ` [${c.layer}] ${c.path}`).join("\n");
577
+ throw new Error(
578
+ `jsonui-cli file not found: ${cliRelPath}. Tried:\n` +
579
+ tried +
580
+ "\n\nInstall jsonui-cli (the bootstrap places files under ~/.jsonui-cli/),\n" +
581
+ "or set JSONUI_CLI_PATH to point at a jsonui-cli checkout,\n" +
582
+ "or reinstall jsonui-mcp-server so the postinstall script can fetch a bundled snapshot."
583
+ );
584
+ }
585
+ }
586
+
587
+ // Helper to derive the MCP server root from an import.meta.url (e.g. in index.ts).
588
+ export function mcpRootFromImportMetaUrl(url: string): string {
589
+ const dir = fileURLToPath(new URL(".", url));
590
+ return resolve(dir, "..");
591
+ }
@@ -0,0 +1,44 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { ServerConfig } from "../../config.js";
4
+ import { runCli } from "../../cli_runner.js";
5
+
6
+ /**
7
+ * Discovery wrapper around `jui ls api-models --json`.
8
+ *
9
+ * Lists generated DTO + Domain scaffold files per platform, plus orphan
10
+ * DTOs whose source schema no longer exists in the current swagger.
11
+ * Agents use this to verify project state before / after `jui build`.
12
+ */
13
+ export function register(server: McpServer, config: ServerConfig) {
14
+ server.tool(
15
+ "list_api_models",
16
+ "List generated DTO + Domain scaffold files per platform with orphan detection",
17
+ {
18
+ platform: z.enum(["ios", "android", "web"]).optional()
19
+ .describe("Restrict to a single platform"),
20
+ project_dir: z.string().optional().describe("Project directory (overrides JUI_PROJECT_DIR env)"),
21
+ },
22
+ async ({ platform, project_dir }) => {
23
+ try {
24
+ const projectDir = config.resolveProjectDir(project_dir);
25
+ const args = ["ls", "api-models", "--json"];
26
+ if (platform) {
27
+ args.push("--platform", platform);
28
+ }
29
+ const result = await runCli("jui", args, { cwd: projectDir });
30
+ if (result.exitCode !== 0) {
31
+ return {
32
+ content: [{
33
+ type: "text",
34
+ text: result.stderr || result.stdout || "jui ls api-models failed",
35
+ }],
36
+ };
37
+ }
38
+ return { content: [{ type: "text", text: result.stdout.trim() }] };
39
+ } catch (e: any) {
40
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
41
+ }
42
+ }
43
+ );
44
+ }
@@ -0,0 +1,53 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { ServerConfig } from "../../config.js";
4
+ import { runCli } from "../../cli_runner.js";
5
+
6
+ /**
7
+ * Discovery wrapper around `jui ls api-specs --json`.
8
+ *
9
+ * Lists swagger / OpenAPI files under the project's `api_directory` with
10
+ * lightweight parsed metadata (title, version, schema_count, enum_count,
11
+ * endpoint_count) plus advisory flags for polymorphism / multi-file ref
12
+ * usage (`has_one_of`, `has_multi_file_ref`).
13
+ *
14
+ * Note: `has_one_of: true` is **not** equivalent to "will halt". Field-level
15
+ * `oneOf` paired with `discriminator` + explicit `mapping` (variants `$ref`
16
+ * top-level schemas + sibling discriminator property present) is supported
17
+ * and emits Swift enum / Kotlin sealed class / TS discriminated union code.
18
+ * Other shapes (anyOf, schema-level oneOf, oneOf without discriminator,
19
+ * discriminator alone) still halt. Agents should verify each `oneOf` site
20
+ * rather than blanket-flagging the file as broken.
21
+ */
22
+ export function register(server: McpServer, config: ServerConfig) {
23
+ server.tool(
24
+ "list_api_specs",
25
+ "List swagger / OpenAPI files under api_directory with parsed metadata (title, version, schema/enum/endpoint counts, advisory polymorphism flags — see has_one_of caveat in tool doc)",
26
+ {
27
+ project_dir: z.string().optional().describe("Project directory (overrides JUI_PROJECT_DIR env)"),
28
+ },
29
+ async ({ project_dir }) => {
30
+ try {
31
+ const projectDir = config.resolveProjectDir(project_dir);
32
+ const result = await runCli(
33
+ "jui",
34
+ ["ls", "api-specs", "--json"],
35
+ { cwd: projectDir }
36
+ );
37
+ if (result.exitCode !== 0) {
38
+ return {
39
+ content: [{
40
+ type: "text",
41
+ text: result.stderr || result.stdout || "jui ls api-specs failed",
42
+ }],
43
+ };
44
+ }
45
+ // The CLI already emits a JSON document — pass it through verbatim
46
+ // so callers don't have to peel a wrapper envelope.
47
+ return { content: [{ type: "text", text: result.stdout.trim() }] };
48
+ } catch (e: any) {
49
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
50
+ }
51
+ }
52
+ );
53
+ }