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.
- package/.github/workflows/test.yml +35 -0
- package/README.md +148 -0
- package/data/attribute_definitions.json +4248 -0
- package/data/attribute_semantics.json +137 -0
- package/data/binding_semantics.json +115 -0
- package/data/component_metadata.json +769 -0
- package/data/coverage.json +114 -0
- package/data/screen_identity.json +350 -0
- package/dist/cli_runner.d.ts +10 -0
- package/dist/cli_runner.js +33 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +67 -0
- package/dist/data/component_metadata.d.ts +30 -0
- package/dist/data/component_metadata.js +498 -0
- package/dist/data/derived.d.ts +198 -0
- package/dist/data/derived.js +293 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +130 -0
- package/dist/spec_loader.d.ts +110 -0
- package/dist/spec_loader.js +445 -0
- package/dist/tools/api/list_api_models.d.ts +10 -0
- package/dist/tools/api/list_api_models.js +37 -0
- package/dist/tools/api/list_api_specs.d.ts +19 -0
- package/dist/tools/api/list_api_specs.js +42 -0
- package/dist/tools/api/preview_api_model_sync.d.ts +11 -0
- package/dist/tools/api/preview_api_model_sync.js +34 -0
- package/dist/tools/context/get_project_config.d.ts +3 -0
- package/dist/tools/context/get_project_config.js +20 -0
- package/dist/tools/context/list_component_specs.d.ts +3 -0
- package/dist/tools/context/list_component_specs.js +35 -0
- package/dist/tools/context/list_layouts.d.ts +3 -0
- package/dist/tools/context/list_layouts.js +118 -0
- package/dist/tools/context/list_screen_specs.d.ts +3 -0
- package/dist/tools/context/list_screen_specs.js +37 -0
- package/dist/tools/context/read_layout_file.d.ts +3 -0
- package/dist/tools/context/read_layout_file.js +25 -0
- package/dist/tools/context/read_spec_file.d.ts +3 -0
- package/dist/tools/context/read_spec_file.js +27 -0
- package/dist/tools/doc/doc_generate_component.d.ts +3 -0
- package/dist/tools/doc/doc_generate_component.js +38 -0
- package/dist/tools/doc/doc_generate_html.d.ts +3 -0
- package/dist/tools/doc/doc_generate_html.js +46 -0
- package/dist/tools/doc/doc_generate_spec.d.ts +3 -0
- package/dist/tools/doc/doc_generate_spec.js +38 -0
- package/dist/tools/doc/doc_init_component.d.ts +3 -0
- package/dist/tools/doc/doc_init_component.js +30 -0
- package/dist/tools/doc/doc_init_spec.d.ts +3 -0
- package/dist/tools/doc/doc_init_spec.js +33 -0
- package/dist/tools/doc/doc_rules_init.d.ts +3 -0
- package/dist/tools/doc/doc_rules_init.js +25 -0
- package/dist/tools/doc/doc_rules_show.d.ts +3 -0
- package/dist/tools/doc/doc_rules_show.js +21 -0
- package/dist/tools/doc/doc_validate_component.d.ts +3 -0
- package/dist/tools/doc/doc_validate_component.js +39 -0
- package/dist/tools/doc/doc_validate_spec.d.ts +3 -0
- package/dist/tools/doc/doc_validate_spec.js +40 -0
- package/dist/tools/jui/jui_build.d.ts +3 -0
- package/dist/tools/jui/jui_build.js +31 -0
- package/dist/tools/jui/jui_generate_converter.d.ts +3 -0
- package/dist/tools/jui/jui_generate_converter.js +43 -0
- package/dist/tools/jui/jui_generate_project.d.ts +3 -0
- package/dist/tools/jui/jui_generate_project.js +43 -0
- package/dist/tools/jui/jui_generate_screen.d.ts +3 -0
- package/dist/tools/jui/jui_generate_screen.js +22 -0
- package/dist/tools/jui/jui_init.d.ts +3 -0
- package/dist/tools/jui/jui_init.js +42 -0
- package/dist/tools/jui/jui_migrate_layouts.d.ts +3 -0
- package/dist/tools/jui/jui_migrate_layouts.js +25 -0
- package/dist/tools/jui/jui_sync_tool.d.ts +3 -0
- package/dist/tools/jui/jui_sync_tool.js +33 -0
- package/dist/tools/jui/jui_verify.d.ts +3 -0
- package/dist/tools/jui/jui_verify.js +43 -0
- package/dist/tools/spec/get_binding_rules.d.ts +3 -0
- package/dist/tools/spec/get_binding_rules.js +8 -0
- package/dist/tools/spec/get_data_source.d.ts +3 -0
- package/dist/tools/spec/get_data_source.js +41 -0
- package/dist/tools/spec/get_modifier_order.d.ts +3 -0
- package/dist/tools/spec/get_modifier_order.js +14 -0
- package/dist/tools/spec/get_platform_mapping.d.ts +3 -0
- package/dist/tools/spec/get_platform_mapping.js +20 -0
- package/dist/tools/spec/get_screen_identity.d.ts +3 -0
- package/dist/tools/spec/get_screen_identity.js +22 -0
- package/dist/tools/spec/lookup_attribute.d.ts +3 -0
- package/dist/tools/spec/lookup_attribute.js +14 -0
- package/dist/tools/spec/lookup_component.d.ts +3 -0
- package/dist/tools/spec/lookup_component.js +15 -0
- package/dist/tools/spec/search_components.d.ts +3 -0
- package/dist/tools/spec/search_components.js +14 -0
- package/dist/tools/test/test_artifacts_pull.d.ts +3 -0
- package/dist/tools/test/test_artifacts_pull.js +48 -0
- package/dist/tools/test/test_artifacts_status.d.ts +3 -0
- package/dist/tools/test/test_artifacts_status.js +28 -0
- package/dist/tools/test/test_generate_description.d.ts +3 -0
- package/dist/tools/test/test_generate_description.js +27 -0
- package/dist/tools/test/test_generate_flow.d.ts +3 -0
- package/dist/tools/test/test_generate_flow.js +29 -0
- package/dist/tools/test/test_generate_screen.d.ts +3 -0
- package/dist/tools/test/test_generate_screen.js +29 -0
- package/dist/tools/test/test_mock_generate.d.ts +3 -0
- package/dist/tools/test/test_mock_generate.js +64 -0
- package/dist/tools/test/test_report.d.ts +3 -0
- package/dist/tools/test/test_report.js +26 -0
- package/dist/tools/test/test_validate.d.ts +3 -0
- package/dist/tools/test/test_validate.js +43 -0
- package/docs/design.md +227 -0
- package/install.sh +90 -0
- package/package.json +24 -0
- package/scripts/fetch-definitions.js +101 -0
- package/scripts/fetch-manifest.js +60 -0
- package/src/cli_runner.ts +55 -0
- package/src/config.ts +88 -0
- package/src/data/derived.ts +305 -0
- package/src/index.ts +157 -0
- package/src/spec_loader.ts +591 -0
- package/src/tools/api/list_api_models.ts +44 -0
- package/src/tools/api/list_api_specs.ts +53 -0
- package/src/tools/api/preview_api_model_sync.ts +41 -0
- package/src/tools/context/get_project_config.ts +27 -0
- package/src/tools/context/list_component_specs.ts +44 -0
- package/src/tools/context/list_layouts.ts +131 -0
- package/src/tools/context/list_screen_specs.ts +46 -0
- package/src/tools/context/read_layout_file.ts +35 -0
- package/src/tools/context/read_spec_file.ts +38 -0
- package/src/tools/doc/doc_generate_component.ts +43 -0
- package/src/tools/doc/doc_generate_html.ts +40 -0
- package/src/tools/doc/doc_generate_spec.ts +43 -0
- package/src/tools/doc/doc_init_component.ts +32 -0
- package/src/tools/doc/doc_init_spec.ts +37 -0
- package/src/tools/doc/doc_rules_init.ts +29 -0
- package/src/tools/doc/doc_rules_show.ts +27 -0
- package/src/tools/doc/doc_validate_component.ts +47 -0
- package/src/tools/doc/doc_validate_spec.ts +48 -0
- package/src/tools/jui/jui_build.ts +31 -0
- package/src/tools/jui/jui_generate_converter.ts +41 -0
- package/src/tools/jui/jui_generate_project.ts +37 -0
- package/src/tools/jui/jui_generate_screen.ts +28 -0
- package/src/tools/jui/jui_init.ts +38 -0
- package/src/tools/jui/jui_migrate_layouts.ts +29 -0
- package/src/tools/jui/jui_sync_tool.ts +33 -0
- package/src/tools/jui/jui_verify.ts +43 -0
- package/src/tools/spec/get_binding_rules.ts +16 -0
- package/src/tools/spec/get_data_source.ts +53 -0
- package/src/tools/spec/get_modifier_order.ts +22 -0
- package/src/tools/spec/get_platform_mapping.ts +28 -0
- package/src/tools/spec/get_screen_identity.ts +31 -0
- package/src/tools/spec/lookup_attribute.ts +22 -0
- package/src/tools/spec/lookup_component.ts +23 -0
- package/src/tools/spec/search_components.ts +22 -0
- package/src/tools/test/test_artifacts_pull.ts +47 -0
- package/src/tools/test/test_artifacts_status.ts +34 -0
- package/src/tools/test/test_generate_description.ts +33 -0
- package/src/tools/test/test_generate_flow.ts +33 -0
- package/src/tools/test/test_generate_screen.ts +33 -0
- package/src/tools/test/test_mock_generate.ts +58 -0
- package/src/tools/test/test_report.ts +32 -0
- package/src/tools/test/test_validate.ts +43 -0
- package/tests/__snapshots__/derived.test.ts.snap +279 -0
- package/tests/bundled_snapshot.test.ts +155 -0
- package/tests/cli_runner.test.ts +120 -0
- package/tests/config.test.ts +212 -0
- package/tests/derived.test.ts +114 -0
- package/tests/helpers.ts +272 -0
- package/tests/setup.ts +27 -0
- package/tests/snapshot_completeness.test.ts +152 -0
- package/tests/spec_loader.test.ts +488 -0
- package/tests/tools_cli.test.ts +687 -0
- package/tests/tools_context.test.ts +274 -0
- package/tests/tools_spec.test.ts +225 -0
- package/tests/tools_test.test.ts +284 -0
- package/tsconfig.json +15 -0
- package/uninstall.sh +46 -0
- 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
|
+
}
|