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