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
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "jui-tools-mcp-server",
3
+ "version": "2.5.0",
4
+ "description": "MCP server for JsonUI project management - wraps jui and jsonui-doc CLIs plus component spec lookup",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "start": "node dist/index.js",
9
+ "test": "vitest run",
10
+ "test:watch": "vitest",
11
+ "fetch-definitions": "node scripts/fetch-definitions.js",
12
+ "postinstall": "node scripts/fetch-definitions.js"
13
+ },
14
+ "type": "module",
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.12.1",
17
+ "zod": "^3.23.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "typescript": "^5.7.0",
22
+ "vitest": "^4.1.9"
23
+ }
24
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-install hook: fetch the data files from the pinned jsonui-cli release
4
+ * tag and write them into `data/`. These populate the 4th-layer fallback
5
+ * in spec_loader.ts (the bundled snapshot).
6
+ *
7
+ * The list of files lives in ./fetch-manifest.js (side-effect-free, so the
8
+ * test suite can assert its completeness without triggering a fetch).
9
+ *
10
+ * Users who install jsonui-cli separately will prefer that checkout via the
11
+ * higher-priority fallback layers; users who don't have jsonui-cli installed
12
+ * get a working MCP with the bundled snapshots.
13
+ *
14
+ * The fetch is best-effort. If the network is unavailable at install time
15
+ * (CI sandbox, offline install, etc.) and no existing snapshot is present,
16
+ * the script exits 0 anyway — the MCP will surface a clear error at
17
+ * startup if none of the 4 layers resolve.
18
+ */
19
+
20
+ import dns from "dns";
21
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
22
+ import { dirname, join, resolve } from "path";
23
+ import { fileURLToPath } from "url";
24
+
25
+ import { FILES } from "./fetch-manifest.js";
26
+
27
+ // raw.githubusercontent.com's IPv6 edge sometimes serves stale 404 responses
28
+ // for newly-pushed files for ~5 min while the IPv4 edge is already fresh.
29
+ // Prefer IPv4 to avoid spurious postinstall failures on fresh installs.
30
+ dns.setDefaultResultOrder("ipv4first");
31
+
32
+ const __dirname = dirname(fileURLToPath(import.meta.url));
33
+ const PROJECT_ROOT = resolve(__dirname, "..");
34
+ const DATA_DIR = join(PROJECT_ROOT, "data");
35
+
36
+ // The jsonui-cli release tag this snapshot is pinned to. Pinning (instead of
37
+ // `main`) means an install never bundles definitions newer than the CLI a
38
+ // user gets — the snapshot only moves when the pin moves.
39
+ //
40
+ // Bump procedure (the pin update IS the review point, same idea as the
41
+ // JSONUI_CLI_REF SHA pin in JsonUIDocument's deploy.yml):
42
+ // 1. jsonui-cli: land the shared/core / conformance change and cut the tag
43
+ // 2. update JSONUI_CLI_TAG here
44
+ // 3. `npm run fetch-definitions` and commit the data/ diff together with
45
+ // this pin in one commit
46
+ // JSONUI_CLI_RAW_BASE still overrides the whole base URL for manual testing.
47
+ const JSONUI_CLI_TAG = "v1.4.0";
48
+ const BASE_URL =
49
+ process.env.JSONUI_CLI_RAW_BASE ||
50
+ `https://raw.githubusercontent.com/Tai-Kimura/jsonui-cli/${JSONUI_CLI_TAG}`;
51
+
52
+ async function fetchOne(spec) {
53
+ const target = join(DATA_DIR, spec.local);
54
+ const url = spec.envOverride || `${BASE_URL}/${spec.remote}`;
55
+
56
+ try {
57
+ console.error(`[fetch-definitions] GET ${url}`);
58
+ const res = await fetch(url, { redirect: "follow" });
59
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
60
+ const text = await res.text();
61
+ JSON.parse(text); // Validate before writing.
62
+ writeFileSync(target, text, "utf-8");
63
+ console.error(`[fetch-definitions] wrote ${target} (${text.length} bytes)`);
64
+ return true;
65
+ } catch (err) {
66
+ if (existsSync(target)) {
67
+ console.error(
68
+ `[fetch-definitions] fetch failed (${err.message}); keeping existing snapshot at ${target}.`
69
+ );
70
+ } else {
71
+ console.error(
72
+ `[fetch-definitions] fetch failed for ${spec.local} (${err.message}); no bundled snapshot written.`
73
+ );
74
+ }
75
+ return false;
76
+ }
77
+ }
78
+
79
+ async function main() {
80
+ if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
81
+
82
+ const results = await Promise.all(FILES.map(fetchOne));
83
+ const allMissing = results.every((ok, i) => {
84
+ return !ok && !existsSync(join(DATA_DIR, FILES[i].local));
85
+ });
86
+
87
+ if (allMissing) {
88
+ console.error(
89
+ "[fetch-definitions] no bundled snapshots available.\n" +
90
+ " The MCP will try JSONUI_CLI_PATH / ./.jsonui-cli/ / ~/.jsonui-cli/ at startup.\n" +
91
+ " If none resolve, the server will exit with a clear error."
92
+ );
93
+ }
94
+ // Do not fail install on fetch errors — the layered fallback handles it.
95
+ process.exit(0);
96
+ }
97
+
98
+ main().catch((err) => {
99
+ console.error(`[fetch-definitions] unexpected error: ${err?.stack || err}`);
100
+ process.exit(0);
101
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The auto-fetch manifest: every file that `scripts/fetch-definitions.js`
3
+ * refreshes into `data/` at npm install time.
4
+ *
5
+ * This lives in its own side-effect-free module so the test suite can import
6
+ * the manifest without triggering a network fetch. The invariant it exists to
7
+ * enforce (tests/snapshot_completeness.test.ts):
8
+ *
9
+ * every file bundled in data/ ⊆ this manifest ⊆ files SpecLoader reads
10
+ *
11
+ * A data/ file missing from the manifest only ever refreshes by hand and
12
+ * silently goes stale (binding_semantics.json and coverage.json each fell
13
+ * into exactly this trap). A manifest entry SpecLoader never reads is dead
14
+ * weight fetched on every install. Adding a file here without wiring it into
15
+ * spec_loader.ts (or vice versa) fails the suite.
16
+ */
17
+
18
+ export const FILES = [
19
+ {
20
+ remote: "shared/core/attribute_definitions.json",
21
+ local: "attribute_definitions.json",
22
+ envOverride: process.env.JSONUI_ATTR_DEFINITIONS_URL,
23
+ },
24
+ {
25
+ remote: "shared/core/component_metadata.json",
26
+ local: "component_metadata.json",
27
+ envOverride: process.env.JSONUI_COMPONENT_METADATA_URL,
28
+ },
29
+ // Canonical attribute-semantics contract (cross-effect rulings, plan 33).
30
+ // Same trap as binding_semantics below: bundled in data/ but hand-mirrored
31
+ // until the completeness guard caught it (third edition).
32
+ {
33
+ remote: "shared/core/attribute_semantics.json",
34
+ local: "attribute_semantics.json",
35
+ },
36
+ // Canonical binding-resolution semantics. This was bundled in data/ but
37
+ // missing from FILES, so the snapshot only ever refreshed by hand and
38
+ // silently went stale — the same trap the screen-identity asset would
39
+ // otherwise fall into.
40
+ {
41
+ remote: "shared/core/binding_semantics.json",
42
+ local: "binding_semantics.json",
43
+ envOverride: process.env.JSONUI_BINDING_SEMANTICS_URL,
44
+ },
45
+ // Canonical screen identity: what a screen is, how it is identified, how
46
+ // its presence is asserted.
47
+ {
48
+ remote: "shared/core/screen_identity.json",
49
+ local: "screen_identity.json",
50
+ envOverride: process.env.JSONUI_SCREEN_IDENTITY_URL,
51
+ },
52
+ // Conformance coverage ledger (declared-but-unimplemented gaps). The loader
53
+ // reads data/coverage.json as its fallback, so it must refresh with the rest
54
+ // — binding_semantics.json already fell into this exact stale-snapshot trap.
55
+ {
56
+ remote: "conformance/coverage.json",
57
+ local: "coverage.json",
58
+ envOverride: process.env.JSONUI_COVERAGE_URL,
59
+ },
60
+ ];
@@ -0,0 +1,55 @@
1
+ import { execFile } from "child_process";
2
+
3
+ export interface CliResult {
4
+ exitCode: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+
9
+ export async function runCli(
10
+ command: string,
11
+ args: string[],
12
+ options: { cwd: string; timeout?: number }
13
+ ): Promise<CliResult> {
14
+ const timeout = options.timeout ?? 60_000;
15
+
16
+ return new Promise((resolve) => {
17
+ execFile(
18
+ command,
19
+ args,
20
+ {
21
+ cwd: options.cwd,
22
+ timeout,
23
+ maxBuffer: 10 * 1024 * 1024,
24
+ env: { ...process.env, PYTHONIOENCODING: "utf-8" },
25
+ },
26
+ (error, stdout, stderr) => {
27
+ if (error && "killed" in error && error.killed) {
28
+ resolve({
29
+ exitCode: 1,
30
+ stdout: stdout || "",
31
+ stderr: `Command timed out after ${timeout / 1000}s`,
32
+ });
33
+ return;
34
+ }
35
+ resolve({
36
+ exitCode: error ? (error as any).code ?? 1 : 0,
37
+ stdout: stdout || "",
38
+ stderr: stderr || "",
39
+ });
40
+ }
41
+ );
42
+ });
43
+ }
44
+
45
+ export function formatResult(result: CliResult): string {
46
+ return JSON.stringify(
47
+ {
48
+ success: result.exitCode === 0,
49
+ output: result.stdout,
50
+ errors: result.stderr || undefined,
51
+ },
52
+ null,
53
+ 2
54
+ );
55
+ }
package/src/config.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { readFileSync, existsSync, realpathSync } from "fs";
2
+ import { basename, dirname, join, resolve, isAbsolute, sep } from "path";
3
+
4
+ /**
5
+ * Resolve symlinks on the longest existing prefix of an already lexically
6
+ * resolved path. Non-existent trailing segments are kept as-is so validation
7
+ * can run before existence checks without throwing; a dangling or missing
8
+ * path can't leak data (the read fails afterwards).
9
+ */
10
+ function realpathLongestExistingPrefix(path: string): string {
11
+ let existing = path;
12
+ const missingSuffix: string[] = [];
13
+ while (!existsSync(existing)) {
14
+ const parent = dirname(existing);
15
+ if (parent === existing) return path; // no existing prefix at all
16
+ missingSuffix.unshift(basename(existing));
17
+ existing = parent;
18
+ }
19
+ return join(realpathSync(existing), ...missingSuffix);
20
+ }
21
+
22
+ export interface ProjectConfig {
23
+ project_name: string;
24
+ spec_directory: string;
25
+ layouts_directory: string;
26
+ styles_directory: string;
27
+ images_directory: string;
28
+ component_spec_directory: string;
29
+ strings_file: string;
30
+ type_map_file: string;
31
+ platforms: Record<string, any>;
32
+ }
33
+
34
+ export class ServerConfig {
35
+ private defaultProjectDir: string | undefined;
36
+
37
+ constructor() {
38
+ this.defaultProjectDir = process.env.JUI_PROJECT_DIR;
39
+ }
40
+
41
+ resolveProjectDir(override?: string): string {
42
+ const dir = override || this.defaultProjectDir;
43
+ if (!dir) {
44
+ throw new Error(
45
+ "No project directory specified. Set JUI_PROJECT_DIR environment variable or pass project_dir parameter."
46
+ );
47
+ }
48
+ const resolved = resolve(dir);
49
+ if (!existsSync(resolved)) {
50
+ throw new Error(`Project directory does not exist: ${resolved}`);
51
+ }
52
+ return resolved;
53
+ }
54
+
55
+ readProjectConfig(projectDir: string): ProjectConfig {
56
+ const configPath = join(projectDir, "jui.config.json");
57
+ if (!existsSync(configPath)) {
58
+ throw new Error(
59
+ `jui.config.json not found in ${projectDir}. Run jui_init first.`
60
+ );
61
+ }
62
+ return JSON.parse(readFileSync(configPath, "utf-8"));
63
+ }
64
+
65
+ resolveDir(config: ProjectConfig, field: keyof ProjectConfig, projectDir: string): string {
66
+ const value = config[field];
67
+ if (typeof value !== "string") return projectDir;
68
+ if (isAbsolute(value)) return value;
69
+ return join(projectDir, value);
70
+ }
71
+
72
+ validatePathInProject(filePath: string, projectDir: string): string {
73
+ const resolved = isAbsolute(filePath) ? filePath : join(projectDir, filePath);
74
+ // Realpath both sides so a symlink inside the project cannot smuggle the
75
+ // path outside it (lexical resolve() does not follow symlinks).
76
+ const normalizedProject = realpathLongestExistingPrefix(resolve(projectDir));
77
+ const normalizedPath = realpathLongestExistingPrefix(resolve(resolved));
78
+ // Compare on path-segment boundaries: a bare startsWith() would accept
79
+ // sibling dirs like "<project>-evil".
80
+ const isInside =
81
+ normalizedPath === normalizedProject ||
82
+ normalizedPath.startsWith(normalizedProject + sep);
83
+ if (!isInside) {
84
+ throw new Error(`Path traversal detected: ${filePath} is outside project directory`);
85
+ }
86
+ return normalizedPath;
87
+ }
88
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Derived / static content that the MCP server exposes alongside attribute definitions.
3
+ *
4
+ * These values do not depend on
5
+ * attribute_definitions.json — they encode platform conventions (modifier order,
6
+ * binding syntax, cross-platform value mapping). Kept in TS so the MCP can serve
7
+ * them at startup without any build step.
8
+ */
9
+
10
+ // ----- categorizeCommonAttributes ----------------------------------------
11
+
12
+ const CATEGORY_KEYS: Record<string, Set<string>> = {
13
+ sizing: new Set([
14
+ "width", "height", "minWidth", "maxWidth", "minHeight", "maxHeight",
15
+ "weight", "widthWeight", "heightWeight", "maxWidthWeight", "maxHeightWeight",
16
+ "minWidthWeight", "minHeightWeight", "aspectWidth", "aspectHeight",
17
+ "aspectRatio", "frame", "rect", "wrapContent",
18
+ ]),
19
+ spacing: new Set([
20
+ "padding", "paddings", "paddingTop", "paddingBottom", "paddingLeft",
21
+ "paddingRight", "paddingStart", "paddingEnd", "rightPadding", "leftPadding",
22
+ "minTopPadding", "minBottomPadding", "minLeftPadding", "minRightPadding",
23
+ "maxTopPadding", "maxBottomPadding", "maxLeftPadding", "maxRightPadding",
24
+ "margins", "topMargin", "bottomMargin", "leftMargin", "rightMargin",
25
+ "startMargin", "endMargin", "minTopMargin", "minBottomMargin",
26
+ "minLeftMargin", "minRightMargin", "maxTopMargin", "maxBottomMargin",
27
+ "maxLeftMargin", "maxRightMargin", "innerPadding", "insets", "insetHorizontal",
28
+ ]),
29
+ visual: new Set([
30
+ "background", "tapBackground", "highlightBackground", "disabledBackground",
31
+ "defaultBackground", "cornerRadius", "borderWidth", "borderColor",
32
+ "borderStyle", "alpha", "opacity", "shadow", "clipToBounds", "tintColor",
33
+ "effectStyle",
34
+ ]),
35
+ visibility: new Set(["visibility", "hidden"]),
36
+ interaction: new Set([
37
+ "onclick", "onClick", "onLongPress", "onPan", "onPinch", "canTap",
38
+ "enabled", "userInteractionEnabled", "touchDisabledState",
39
+ "touchEnabledViewIds",
40
+ ]),
41
+ layout: new Set([
42
+ "centerInParent", "centerVertical", "centerHorizontal", "alignTop",
43
+ "alignBottom", "alignLeft", "alignRight", "alignTopOfView",
44
+ "alignBottomOfView", "alignLeftOfView", "alignRightOfView",
45
+ "alignTopView", "alignBottomView", "alignLeftView", "alignRightView",
46
+ "alignCenterVerticalView", "alignCenterHorizontalView", "toView",
47
+ "gravity", "indexBelow", "indexAbove",
48
+ "keyBottomView", "keyTopView", "keyLeftView", "keyRightView",
49
+ ]),
50
+ lifecycle: new Set(["onAppear", "onDisappear"]),
51
+ accessibility: new Set(["id", "testId", "className", "tag"]),
52
+ binding: new Set([
53
+ "binding", "bind", "binding_id", "binding_group", "propertyName",
54
+ "bindingScript", "data", "shared_data",
55
+ ]),
56
+ };
57
+
58
+ export function categorizeCommonAttributes(commonAttrs: Record<string, any>): any {
59
+ const categories: Record<string, Record<string, any>> = {
60
+ sizing: {},
61
+ spacing: {},
62
+ visual: {},
63
+ visibility: {},
64
+ interaction: {},
65
+ layout: {},
66
+ lifecycle: {},
67
+ accessibility: {},
68
+ binding: {},
69
+ other: {},
70
+ };
71
+
72
+ for (const [key, attrDef] of Object.entries(commonAttrs)) {
73
+ if (key.startsWith("_")) continue;
74
+ let placed = false;
75
+ for (const [category, keys] of Object.entries(CATEGORY_KEYS)) {
76
+ if (keys.has(key)) {
77
+ categories[category][key] = attrDef;
78
+ placed = true;
79
+ break;
80
+ }
81
+ }
82
+ if (!placed) categories.other[key] = attrDef;
83
+ }
84
+
85
+ // Drop empty categories
86
+ const filtered: Record<string, Record<string, any>> = {};
87
+ for (const [name, attrs] of Object.entries(categories)) {
88
+ if (Object.keys(attrs).length > 0) filtered[name] = attrs;
89
+ }
90
+
91
+ return {
92
+ description: "Common attributes shared across all components",
93
+ source: "attribute_definitions.json common section",
94
+ categories: filtered,
95
+ };
96
+ }
97
+
98
+ // ----- modifier order (platform rendering) -------------------------------
99
+
100
+ export const MODIFIER_ORDER = {
101
+ description:
102
+ "Platform-specific modifier application order. Order affects rendering.",
103
+ swift: {
104
+ order: [
105
+ "centerAlignment",
106
+ "edgeAlignment",
107
+ "padding",
108
+ "frameConstraints (min/max)",
109
+ "frameSize (width/height)",
110
+ "insets/insetHorizontal",
111
+ "background",
112
+ "cornerRadius",
113
+ "border",
114
+ "margins",
115
+ "alpha/opacity",
116
+ "shadow",
117
+ "clipping",
118
+ "offset",
119
+ "visibility (hidden/opacity ternary)",
120
+ "safeAreaInsets",
121
+ "disabled",
122
+ "tag (for TabView)",
123
+ "tintColor",
124
+ "onClick/onTapGesture",
125
+ "lifecycle (onAppear/onDisappear)",
126
+ "confirmationDialog",
127
+ "accessibilityIdentifier",
128
+ ],
129
+ criticalRules: [
130
+ "background MUST come after padding (background includes padding area)",
131
+ "border MUST come after cornerRadius (for rounded border)",
132
+ "frame MUST come before margins (size before external spacing)",
133
+ "Image: .resizable() must come first, then .aspectRatio",
134
+ "Label: .leading alignment added to frame for textAlign to work",
135
+ ],
136
+ },
137
+ kotlin: {
138
+ order: [
139
+ "testTag",
140
+ "margins",
141
+ "weight (caller applies)",
142
+ "size (width/height, matchParent/wrapContent, min/max/aspectRatio)",
143
+ "alpha/opacity",
144
+ "shadow/elevation",
145
+ "background (cornerRadius -> clip -> border -> bgColor)",
146
+ "clickable",
147
+ "padding",
148
+ "alignment (RowScope/ColumnScope/BoxScope)",
149
+ ],
150
+ criticalRules: [
151
+ "Modifier.then() chains left-to-right; order matters",
152
+ "margins before size (outside to inside)",
153
+ "cornerRadius must be applied as clip before background",
154
+ ],
155
+ },
156
+ react: {
157
+ note: "React uses Tailwind CSS classes; ordering is less strict",
158
+ classOrder: [
159
+ "layout (flex, flex-col/row)",
160
+ "sizing (w-*, h-*)",
161
+ "spacing (p-*, m-*)",
162
+ "typography (text-*, font-*)",
163
+ "visual (bg-*, rounded-*, border-*, shadow-*)",
164
+ "opacity",
165
+ "overflow",
166
+ "cursor/interaction",
167
+ ],
168
+ dynamicStyles: "Values not mappable to Tailwind use inline style objects",
169
+ },
170
+ };
171
+
172
+ // ----- binding rules -----------------------------------------------------
173
+
174
+ export const BINDING_RULES = {
175
+ description: "JsonUI binding syntax rules",
176
+ format: {
177
+ bindingExpression: "@{propertyName}",
178
+ negation: "@{!propertyName} (boolean value attributes only — validator error elsewhere)",
179
+ defaultValue:
180
+ "@{propertyName ?? default} (one ?? max; string default in single or double quotes, or true/false/number; null = unresolved)",
181
+ nestedPath:
182
+ "@{user.name} / @{items[0].title} (dot path + bracket index — canonical on all platforms; see semantics)",
183
+ },
184
+ directions: {
185
+ "two-way": {
186
+ description: "Component state changes reflect back to data model",
187
+ swift: "$data.propertyName",
188
+ kotlin: "mutableStateOf + updateData(mapOf(key to value))",
189
+ react: "value={data.prop} + auto-generated onChange handler",
190
+ applicableAttributes: [
191
+ "TextField.text",
192
+ "TextView.text",
193
+ "Switch.isOn",
194
+ "Toggle.isOn",
195
+ "CheckBox.checked",
196
+ "Slider.value",
197
+ "Segment.selectedIndex",
198
+ "Radio.selectedIndex",
199
+ "SelectBox.selectedIndex",
200
+ "TabView.selectedIndex",
201
+ ],
202
+ },
203
+ "read-only": {
204
+ description: "Display data values only (no write-back)",
205
+ swift: "data.propertyName",
206
+ kotlin: "data[propertyName]",
207
+ react: "{data.propertyName}",
208
+ applicableAttributes: [
209
+ "Label.text",
210
+ "*.width / *.height (frame values)",
211
+ "*.enabled",
212
+ "*.hidden / *.visibility",
213
+ "*.background / *.fontColor",
214
+ "*.fontSize",
215
+ "*.opacity / *.alpha",
216
+ ],
217
+ },
218
+ },
219
+ criticalRules: [
220
+ "Two-way binding expressions must be a single flat identifier — no dots, brackets, ??, or ! (binding-two-way-complex)",
221
+ "Frame values (width, height, etc.) MUST be read-only (data.). $data. is NOT allowed",
222
+ "Two-way binding is only for stateful input components",
223
+ "React auto-generates onPropertyNameChange for two-way bindings",
224
+ "Kotlin Dynamic uses updateData(mapOf(key to value)) for data updates",
225
+ "onclick (lowercase) = selector format (string), onClick (camelCase) = binding format (@{handler})",
226
+ ],
227
+ };
228
+
229
+ // ----- platform mapping (cross-platform value conversion) ----------------
230
+
231
+ export const PLATFORM_MAPPING = {
232
+ description: "Cross-platform attribute value conversion mapping",
233
+ values: {
234
+ matchParent: {
235
+ swift: ".infinity (frame maxWidth/maxHeight)",
236
+ kotlin: "fillMaxWidth() / fillMaxHeight()",
237
+ react: "w-full / h-full",
238
+ },
239
+ wrapContent: {
240
+ swift: "Default (no frame specified)",
241
+ kotlin: "wrapContentWidth() / wrapContentHeight()",
242
+ react: "w-fit / h-fit",
243
+ },
244
+ },
245
+ contentMode: {
246
+ aspectFit: { swift: ".fit", kotlin: "ContentScale.Fit", react: "object-contain" },
247
+ aspectFill: { swift: ".fill", kotlin: "ContentScale.Crop", react: "object-cover" },
248
+ scaleToFill: {
249
+ swift: ".fill (no aspectRatio)",
250
+ kotlin: "ContentScale.FillBounds",
251
+ react: "object-fill",
252
+ },
253
+ },
254
+ textAlign: {
255
+ left: { swift: ".leading", kotlin: "TextAlign.Start", react: "text-left" },
256
+ center: { swift: ".center", kotlin: "TextAlign.Center", react: "text-center" },
257
+ right: { swift: ".trailing", kotlin: "TextAlign.End", react: "text-right" },
258
+ },
259
+ fontWeight: {
260
+ bold: { swift: ".bold", kotlin: "FontWeight.Bold", react: "font-bold" },
261
+ light: { swift: ".light", kotlin: "FontWeight.Light", react: "font-light" },
262
+ thin: { swift: ".thin", kotlin: "FontWeight.Thin", react: "font-thin" },
263
+ medium: { swift: ".medium", kotlin: "FontWeight.Medium", react: "font-medium" },
264
+ semibold: {
265
+ swift: ".semibold",
266
+ kotlin: "FontWeight.SemiBold",
267
+ react: "font-semibold",
268
+ },
269
+ },
270
+ orientation: {
271
+ horizontal: { swift: "HStack", kotlin: "Row", react: "flex flex-row" },
272
+ vertical: { swift: "VStack", kotlin: "Column", react: "flex flex-col" },
273
+ none: { swift: "ZStack", kotlin: "Box", react: "relative" },
274
+ },
275
+ gravity: {
276
+ center: {
277
+ swift: ".center",
278
+ kotlin: "Alignment.Center",
279
+ react: "items-center justify-center",
280
+ },
281
+ centerHorizontal: {
282
+ swift_vertical: ".center HStack alignment",
283
+ kotlin_vertical: "Alignment.CenterHorizontally",
284
+ react: "items-center (flex-col)",
285
+ },
286
+ centerVertical: {
287
+ swift_horizontal: ".center VStack alignment",
288
+ kotlin_horizontal: "Alignment.CenterVertically",
289
+ react: "items-center (flex-row)",
290
+ },
291
+ },
292
+ types: {
293
+ String: { swift: "String", kotlin: "String", react: "string" },
294
+ Int: { swift: "Int", kotlin: "Int", react: "number" },
295
+ Float: { swift: "CGFloat", kotlin: "Float", react: "number" },
296
+ Double: { swift: "Double", kotlin: "Double", react: "number" },
297
+ Bool: { swift: "Bool", kotlin: "Boolean", react: "boolean" },
298
+ Array: { swift: "[T]", kotlin: "List<T>", react: "T[]" },
299
+ Dictionary: {
300
+ swift: "[String: Any]",
301
+ kotlin: "Map<String, Any>",
302
+ react: "Record<string, any>",
303
+ },
304
+ },
305
+ };