@ambitresearch/genie 1.3.1

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 (301) hide show
  1. package/README.md +15 -0
  2. package/dist/auth/bearer.d.ts +49 -0
  3. package/dist/auth/bearer.d.ts.map +1 -0
  4. package/dist/auth/bearer.js +159 -0
  5. package/dist/auth/bearer.js.map +1 -0
  6. package/dist/auth/oauth/authorize.d.ts +19 -0
  7. package/dist/auth/oauth/authorize.d.ts.map +1 -0
  8. package/dist/auth/oauth/authorize.js +111 -0
  9. package/dist/auth/oauth/authorize.js.map +1 -0
  10. package/dist/auth/oauth/config.d.ts +10 -0
  11. package/dist/auth/oauth/config.d.ts.map +1 -0
  12. package/dist/auth/oauth/config.js +16 -0
  13. package/dist/auth/oauth/config.js.map +1 -0
  14. package/dist/auth/oauth/dcr.d.ts +15 -0
  15. package/dist/auth/oauth/dcr.d.ts.map +1 -0
  16. package/dist/auth/oauth/dcr.js +38 -0
  17. package/dist/auth/oauth/dcr.js.map +1 -0
  18. package/dist/auth/oauth/jwt.d.ts +13 -0
  19. package/dist/auth/oauth/jwt.d.ts.map +1 -0
  20. package/dist/auth/oauth/jwt.js +46 -0
  21. package/dist/auth/oauth/jwt.js.map +1 -0
  22. package/dist/auth/oauth/metadata.d.ts +9 -0
  23. package/dist/auth/oauth/metadata.d.ts.map +1 -0
  24. package/dist/auth/oauth/metadata.js +19 -0
  25. package/dist/auth/oauth/metadata.js.map +1 -0
  26. package/dist/auth/oauth/router.d.ts +32 -0
  27. package/dist/auth/oauth/router.d.ts.map +1 -0
  28. package/dist/auth/oauth/router.js +159 -0
  29. package/dist/auth/oauth/router.js.map +1 -0
  30. package/dist/auth/oauth/store.d.ts +55 -0
  31. package/dist/auth/oauth/store.d.ts.map +1 -0
  32. package/dist/auth/oauth/store.js +63 -0
  33. package/dist/auth/oauth/store.js.map +1 -0
  34. package/dist/auth/oauth/token.d.ts +21 -0
  35. package/dist/auth/oauth/token.d.ts.map +1 -0
  36. package/dist/auth/oauth/token.js +77 -0
  37. package/dist/auth/oauth/token.js.map +1 -0
  38. package/dist/auth/oidc/group-policy.d.ts +43 -0
  39. package/dist/auth/oidc/group-policy.d.ts.map +1 -0
  40. package/dist/auth/oidc/group-policy.js +54 -0
  41. package/dist/auth/oidc/group-policy.js.map +1 -0
  42. package/dist/auth/oidc/verifier.d.ts +42 -0
  43. package/dist/auth/oidc/verifier.d.ts.map +1 -0
  44. package/dist/auth/oidc/verifier.js +145 -0
  45. package/dist/auth/oidc/verifier.js.map +1 -0
  46. package/dist/auth/token-cli.d.ts +9 -0
  47. package/dist/auth/token-cli.d.ts.map +1 -0
  48. package/dist/auth/token-cli.js +98 -0
  49. package/dist/auth/token-cli.js.map +1 -0
  50. package/dist/cli.d.ts +3 -0
  51. package/dist/cli.d.ts.map +1 -0
  52. package/dist/cli.js +149 -0
  53. package/dist/cli.js.map +1 -0
  54. package/dist/config/redact.d.ts +20 -0
  55. package/dist/config/redact.d.ts.map +1 -0
  56. package/dist/config/redact.js +77 -0
  57. package/dist/config/redact.js.map +1 -0
  58. package/dist/config/secrets.d.ts +100 -0
  59. package/dist/config/secrets.d.ts.map +1 -0
  60. package/dist/config/secrets.js +130 -0
  61. package/dist/config/secrets.js.map +1 -0
  62. package/dist/framework/html.d.ts +103 -0
  63. package/dist/framework/html.d.ts.map +1 -0
  64. package/dist/framework/html.js +271 -0
  65. package/dist/framework/html.js.map +1 -0
  66. package/dist/framework/interface.d.ts +130 -0
  67. package/dist/framework/interface.d.ts.map +1 -0
  68. package/dist/framework/interface.js +112 -0
  69. package/dist/framework/interface.js.map +1 -0
  70. package/dist/framework/react.d.ts +133 -0
  71. package/dist/framework/react.d.ts.map +1 -0
  72. package/dist/framework/react.js +243 -0
  73. package/dist/framework/react.js.map +1 -0
  74. package/dist/framework/vue.d.ts +99 -0
  75. package/dist/framework/vue.d.ts.map +1 -0
  76. package/dist/framework/vue.js +375 -0
  77. package/dist/framework/vue.js.map +1 -0
  78. package/dist/index.d.ts +3 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +3 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/llm/client.d.ts +81 -0
  83. package/dist/llm/client.d.ts.map +1 -0
  84. package/dist/llm/client.js +141 -0
  85. package/dist/llm/client.js.map +1 -0
  86. package/dist/llm/component-response.d.ts +111 -0
  87. package/dist/llm/component-response.d.ts.map +1 -0
  88. package/dist/llm/component-response.js +167 -0
  89. package/dist/llm/component-response.js.map +1 -0
  90. package/dist/llm/generated-files.d.ts +10 -0
  91. package/dist/llm/generated-files.d.ts.map +1 -0
  92. package/dist/llm/generated-files.js +26 -0
  93. package/dist/llm/generated-files.js.map +1 -0
  94. package/dist/llm/prompts/CHANGELOG.md +70 -0
  95. package/dist/llm/prompts/generate-component.system.md +99 -0
  96. package/dist/llm/prompts/refine-component.system.md +112 -0
  97. package/dist/llm/prompts.d.ts +66 -0
  98. package/dist/llm/prompts.d.ts.map +1 -0
  99. package/dist/llm/prompts.js +78 -0
  100. package/dist/llm/prompts.js.map +1 -0
  101. package/dist/llm/retry.d.ts +183 -0
  102. package/dist/llm/retry.d.ts.map +1 -0
  103. package/dist/llm/retry.js +475 -0
  104. package/dist/llm/retry.js.map +1 -0
  105. package/dist/llm/schema.d.ts +129 -0
  106. package/dist/llm/schema.d.ts.map +1 -0
  107. package/dist/llm/schema.js +147 -0
  108. package/dist/llm/schema.js.map +1 -0
  109. package/dist/llm/validate.d.ts +107 -0
  110. package/dist/llm/validate.d.ts.map +1 -0
  111. package/dist/llm/validate.js +174 -0
  112. package/dist/llm/validate.js.map +1 -0
  113. package/dist/manifest/compiler.d.ts +112 -0
  114. package/dist/manifest/compiler.d.ts.map +1 -0
  115. package/dist/manifest/compiler.js +306 -0
  116. package/dist/manifest/compiler.js.map +1 -0
  117. package/dist/manifest/ensure.d.ts +46 -0
  118. package/dist/manifest/ensure.d.ts.map +1 -0
  119. package/dist/manifest/ensure.js +49 -0
  120. package/dist/manifest/ensure.js.map +1 -0
  121. package/dist/manifest/index.d.ts +13 -0
  122. package/dist/manifest/index.d.ts.map +1 -0
  123. package/dist/manifest/index.js +13 -0
  124. package/dist/manifest/index.js.map +1 -0
  125. package/dist/middleware/plan-guard.d.ts +167 -0
  126. package/dist/middleware/plan-guard.d.ts.map +1 -0
  127. package/dist/middleware/plan-guard.js +190 -0
  128. package/dist/middleware/plan-guard.js.map +1 -0
  129. package/dist/plans/index.d.ts +71 -0
  130. package/dist/plans/index.d.ts.map +1 -0
  131. package/dist/plans/index.js +252 -0
  132. package/dist/plans/index.js.map +1 -0
  133. package/dist/schemas/component.schema.json +114 -0
  134. package/dist/server.d.ts +96 -0
  135. package/dist/server.d.ts.map +1 -0
  136. package/dist/server.js +258 -0
  137. package/dist/server.js.map +1 -0
  138. package/dist/store/empty-manifest.d.ts +58 -0
  139. package/dist/store/empty-manifest.d.ts.map +1 -0
  140. package/dist/store/empty-manifest.js +59 -0
  141. package/dist/store/empty-manifest.js.map +1 -0
  142. package/dist/store/git-host.d.ts +120 -0
  143. package/dist/store/git-host.d.ts.map +1 -0
  144. package/dist/store/git-host.js +796 -0
  145. package/dist/store/git-host.js.map +1 -0
  146. package/dist/store/index.d.ts +5 -0
  147. package/dist/store/index.d.ts.map +1 -0
  148. package/dist/store/index.js +4 -0
  149. package/dist/store/index.js.map +1 -0
  150. package/dist/store/interface.d.ts +322 -0
  151. package/dist/store/interface.d.ts.map +1 -0
  152. package/dist/store/interface.js +100 -0
  153. package/dist/store/interface.js.map +1 -0
  154. package/dist/store/kit-files.d.ts +88 -0
  155. package/dist/store/kit-files.d.ts.map +1 -0
  156. package/dist/store/kit-files.js +199 -0
  157. package/dist/store/kit-files.js.map +1 -0
  158. package/dist/store/local.d.ts +70 -0
  159. package/dist/store/local.d.ts.map +1 -0
  160. package/dist/store/local.js +651 -0
  161. package/dist/store/local.js.map +1 -0
  162. package/dist/store/manifest.d.ts +44 -0
  163. package/dist/store/manifest.d.ts.map +1 -0
  164. package/dist/store/manifest.js +116 -0
  165. package/dist/store/manifest.js.map +1 -0
  166. package/dist/store/viewer-assets.d.ts +20 -0
  167. package/dist/store/viewer-assets.d.ts.map +1 -0
  168. package/dist/store/viewer-assets.js +118 -0
  169. package/dist/store/viewer-assets.js.map +1 -0
  170. package/dist/sync/anchor.d.ts +125 -0
  171. package/dist/sync/anchor.d.ts.map +1 -0
  172. package/dist/sync/anchor.js +210 -0
  173. package/dist/sync/anchor.js.map +1 -0
  174. package/dist/sync/index.d.ts +15 -0
  175. package/dist/sync/index.d.ts.map +1 -0
  176. package/dist/sync/index.js +15 -0
  177. package/dist/sync/index.js.map +1 -0
  178. package/dist/sync/orchestrator.d.ts +160 -0
  179. package/dist/sync/orchestrator.d.ts.map +1 -0
  180. package/dist/sync/orchestrator.js +288 -0
  181. package/dist/sync/orchestrator.js.map +1 -0
  182. package/dist/tools/bind_kit.d.ts +29 -0
  183. package/dist/tools/bind_kit.d.ts.map +1 -0
  184. package/dist/tools/bind_kit.js +70 -0
  185. package/dist/tools/bind_kit.js.map +1 -0
  186. package/dist/tools/conjure.d.ts +141 -0
  187. package/dist/tools/conjure.d.ts.map +1 -0
  188. package/dist/tools/conjure.js +655 -0
  189. package/dist/tools/conjure.js.map +1 -0
  190. package/dist/tools/conjure_screen.d.ts +174 -0
  191. package/dist/tools/conjure_screen.d.ts.map +1 -0
  192. package/dist/tools/conjure_screen.js +458 -0
  193. package/dist/tools/conjure_screen.js.map +1 -0
  194. package/dist/tools/create_kit.d.ts +32 -0
  195. package/dist/tools/create_kit.d.ts.map +1 -0
  196. package/dist/tools/create_kit.js +131 -0
  197. package/dist/tools/create_kit.js.map +1 -0
  198. package/dist/tools/create_project.d.ts +267 -0
  199. package/dist/tools/create_project.d.ts.map +1 -0
  200. package/dist/tools/create_project.js +546 -0
  201. package/dist/tools/create_project.js.map +1 -0
  202. package/dist/tools/delete_files.d.ts +93 -0
  203. package/dist/tools/delete_files.d.ts.map +1 -0
  204. package/dist/tools/delete_files.js +276 -0
  205. package/dist/tools/delete_files.js.map +1 -0
  206. package/dist/tools/delete_project.d.ts +43 -0
  207. package/dist/tools/delete_project.d.ts.map +1 -0
  208. package/dist/tools/delete_project.js +118 -0
  209. package/dist/tools/delete_project.js.map +1 -0
  210. package/dist/tools/get_kit.d.ts +28 -0
  211. package/dist/tools/get_kit.d.ts.map +1 -0
  212. package/dist/tools/get_kit.js +95 -0
  213. package/dist/tools/get_kit.js.map +1 -0
  214. package/dist/tools/get_project.d.ts +37 -0
  215. package/dist/tools/get_project.d.ts.map +1 -0
  216. package/dist/tools/get_project.js +71 -0
  217. package/dist/tools/get_project.js.map +1 -0
  218. package/dist/tools/list_components.d.ts +42 -0
  219. package/dist/tools/list_components.d.ts.map +1 -0
  220. package/dist/tools/list_components.js +125 -0
  221. package/dist/tools/list_components.js.map +1 -0
  222. package/dist/tools/list_files.d.ts +32 -0
  223. package/dist/tools/list_files.d.ts.map +1 -0
  224. package/dist/tools/list_files.js +109 -0
  225. package/dist/tools/list_files.js.map +1 -0
  226. package/dist/tools/list_kits.d.ts +14 -0
  227. package/dist/tools/list_kits.d.ts.map +1 -0
  228. package/dist/tools/list_kits.js +41 -0
  229. package/dist/tools/list_kits.js.map +1 -0
  230. package/dist/tools/list_projects.d.ts +23 -0
  231. package/dist/tools/list_projects.d.ts.map +1 -0
  232. package/dist/tools/list_projects.js +66 -0
  233. package/dist/tools/list_projects.js.map +1 -0
  234. package/dist/tools/plan.d.ts +16 -0
  235. package/dist/tools/plan.d.ts.map +1 -0
  236. package/dist/tools/plan.js +146 -0
  237. package/dist/tools/plan.js.map +1 -0
  238. package/dist/tools/preview.d.ts +289 -0
  239. package/dist/tools/preview.d.ts.map +1 -0
  240. package/dist/tools/preview.js +672 -0
  241. package/dist/tools/preview.js.map +1 -0
  242. package/dist/tools/read_file.d.ts +40 -0
  243. package/dist/tools/read_file.d.ts.map +1 -0
  244. package/dist/tools/read_file.js +114 -0
  245. package/dist/tools/read_file.js.map +1 -0
  246. package/dist/tools/refine.d.ts +220 -0
  247. package/dist/tools/refine.d.ts.map +1 -0
  248. package/dist/tools/refine.js +501 -0
  249. package/dist/tools/refine.js.map +1 -0
  250. package/dist/tools/validate.d.ts +74 -0
  251. package/dist/tools/validate.d.ts.map +1 -0
  252. package/dist/tools/validate.js +224 -0
  253. package/dist/tools/validate.js.map +1 -0
  254. package/dist/tools/write_files.d.ts +158 -0
  255. package/dist/tools/write_files.d.ts.map +1 -0
  256. package/dist/tools/write_files.js +541 -0
  257. package/dist/tools/write_files.js.map +1 -0
  258. package/dist/transport.d.ts +77 -0
  259. package/dist/transport.d.ts.map +1 -0
  260. package/dist/transport.js +448 -0
  261. package/dist/transport.js.map +1 -0
  262. package/dist/ui/card-asset-broker.d.ts +42 -0
  263. package/dist/ui/card-asset-broker.d.ts.map +1 -0
  264. package/dist/ui/card-asset-broker.js +479 -0
  265. package/dist/ui/card-asset-broker.js.map +1 -0
  266. package/dist/ui/grid-resource.d.ts +252 -0
  267. package/dist/ui/grid-resource.d.ts.map +1 -0
  268. package/dist/ui/grid-resource.js +680 -0
  269. package/dist/ui/grid-resource.js.map +1 -0
  270. package/dist/ui/viewer-static/index.html +95 -0
  271. package/dist/ui/viewer-static/viewer.css +451 -0
  272. package/dist/ui/viewer-static/viewer.js +1405 -0
  273. package/dist/validate/full-scan.d.ts +87 -0
  274. package/dist/validate/full-scan.d.ts.map +1 -0
  275. package/dist/validate/full-scan.js +299 -0
  276. package/dist/validate/full-scan.js.map +1 -0
  277. package/dist/validate/index.d.ts +15 -0
  278. package/dist/validate/index.d.ts.map +1 -0
  279. package/dist/validate/index.js +18 -0
  280. package/dist/validate/index.js.map +1 -0
  281. package/dist/validate/marker.d.ts +89 -0
  282. package/dist/validate/marker.d.ts.map +1 -0
  283. package/dist/validate/marker.js +87 -0
  284. package/dist/validate/marker.js.map +1 -0
  285. package/dist/validate/phash.d.ts +135 -0
  286. package/dist/validate/phash.d.ts.map +1 -0
  287. package/dist/validate/phash.js +307 -0
  288. package/dist/validate/phash.js.map +1 -0
  289. package/dist/validate/render.d.ts +38 -0
  290. package/dist/validate/render.d.ts.map +1 -0
  291. package/dist/validate/render.js +155 -0
  292. package/dist/validate/render.js.map +1 -0
  293. package/dist/watch/index.d.ts +12 -0
  294. package/dist/watch/index.d.ts.map +1 -0
  295. package/dist/watch/index.js +12 -0
  296. package/dist/watch/index.js.map +1 -0
  297. package/dist/watch/watcher.d.ts +65 -0
  298. package/dist/watch/watcher.d.ts.map +1 -0
  299. package/dist/watch/watcher.js +238 -0
  300. package/dist/watch/watcher.js.map +1 -0
  301. package/package.json +70 -0
@@ -0,0 +1,680 @@
1
+ /**
2
+ * MCP-Apps resource: `ui://genie/grid` (M4-06 / DRO-268).
3
+ *
4
+ * Registers the embedded preview surface a `ui://`-capable harness (Claude,
5
+ * VS Code ≥Jan 2026, ChatGPT, Cursor, …) renders inside its own sandboxed
6
+ * iframe. The `preview` tool (M4-05 / DRO-267) advertises the bare URI as its
7
+ * standards-compliant app shell and retains a query-bearing result URI for
8
+ * legacy hosts. THIS module answers both resource forms.
9
+ *
10
+ * ── Contract (this issue's AC1–AC6; RFC §6.5) ────────────────────────────────
11
+ * AC1 register `ui://genie/grid`, MIME `text/html;profile=mcp-app`.
12
+ * AC2 handler resolves `?kitId=…`, compiles the manifest (M3-03), inlines it
13
+ * as `<script type="application/json" id="manifest">…</script>` — the
14
+ * sandboxed iframe needs NO fetch (its CSP is `connect-src 'none'`).
15
+ * AC3 viewer.js / viewer.css are inlined byte-for-byte into the one raw HTML
16
+ * resource a compliant MCP Apps host sends to its sandbox proxy.
17
+ * AC4 each card's iframe `src` is rewritten to an absolute `https://` URL on
18
+ * a separate-origin previews host, or (solo dev) a `data:` inline URL.
19
+ * AC5 the CSP allow-list (`connectDomains` / `resourceDomains` /
20
+ * `frameDomains`) is declared at canonical `_meta.ui.csp`.
21
+ * (AC6 — `_meta["openai/outputTemplate"]` on the *tool* result — lives on the
22
+ * `preview` tool in `../tools/preview.ts`, not here.)
23
+ *
24
+ * ── Deviation from the issue's Implementation Notes (flagged for review) ──────
25
+ * The notes say "Use `@modelcontextprotocol/ext-apps/server`". That package is
26
+ * NOT installed in this repo (only `@modelcontextprotocol/sdk@^1.29`), so this
27
+ * module registers through the SDK's own `server.registerResource`. The
28
+ * MCP-Apps CSP shape ext-apps would carry is reproduced verbatim in `_meta`
29
+ * (see {@link buildCspMeta}), so an ext-apps adoption later is a lift-and-shift,
30
+ * not a rewrite.
31
+ *
32
+ * ── Self-contained host contract ──────────────────────────────────────────────
33
+ * MCP Apps hosts receive raw HTML in `resources/read` and do not translate
34
+ * browser-relative URLs into additional MCP resource reads. The shipped shell's
35
+ * relative asset tags are therefore replaced with exact inline bytes, each
36
+ * allow-listed by SHA-256 in the injected CSP. Sibling resources remain
37
+ * registered only for backwards compatibility with older host experiments.
38
+ *
39
+ * ── Byte-identical cards (RFC G-5) ───────────────────────────────────────────
40
+ * Only the card *transport* differs per vehicle: `file://`/localhost fetch the
41
+ * manifest and use relative preview paths; the embedded `ui://` tier inlines
42
+ * the manifest and rewrites each preview path to an absolute/`data:` URL (AC4).
43
+ * The preview HTML bytes themselves are untouched — the card renders identically
44
+ * in all three vehicles.
45
+ */
46
+ import { createHash, randomBytes } from "node:crypto";
47
+ import { createRequire } from "node:module";
48
+ import { lstat, readFile } from "node:fs/promises";
49
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
50
+ import { fileURLToPath } from "node:url";
51
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
52
+ import { parse } from "parse5";
53
+ import { ensureManifest } from "../manifest/index.js";
54
+ import { KIT_ID_PATTERN } from "../tools/get_kit.js";
55
+ // ─── Public constants (AC1) ──────────────────────────────────────────────────
56
+ /**
57
+ * The embedded preview resource URI. `v` identifies the wire contract while
58
+ * the process-scoped nonce prevents hosts from reusing a stale app document
59
+ * after the MCP server restarts. It remains stable for every read in one
60
+ * process, so resource registration and tool metadata always agree.
61
+ */
62
+ export const GRID_RESOURCE_URI = `ui://genie/grid?v=2&instance=${randomBytes(16).toString("hex")}`;
63
+ /** The spec-mandated MCP-Apps MIME (stable spec 2026-01-26; RFC §6.5). */
64
+ export const GRID_RESOURCE_MIME = "text/html;profile=mcp-app";
65
+ /** The DOM id `viewer.js` reads the inlined manifest from (must match it). */
66
+ export const MANIFEST_ELEMENT_ID = "manifest";
67
+ export const TOOL_RESULT_SHELL_META = "genie-tool-result-shell";
68
+ /** Legacy sibling asset URIs retained for older experimental hosts. */
69
+ export const VIEWER_JS_URI = "ui://genie/viewer.js";
70
+ export const VIEWER_CSS_URI = "ui://genie/viewer.css";
71
+ // ─── kitId → kit dir (path-traversal guard, shared with `preview`) ────────────
72
+ /**
73
+ * Resolve `kitId` to its on-disk dir under `kitsRoot`, or `null` when the id is
74
+ * absent or fails {@link KIT_ID_PATTERN}. Returning `null` (rather than
75
+ * throwing) lets the handler degrade to an EMPTY grid instead of erroring a
76
+ * host's `resources/read` — a hostile or malformed `kitId` can never escape the
77
+ * kits root (RFC §10 T-13, the same guard `preview`/`read_file` apply).
78
+ */
79
+ export function resolveKitDir(kitsRoot, kitId) {
80
+ if (kitId === undefined || !KIT_ID_PATTERN.test(kitId))
81
+ return null;
82
+ return join(kitsRoot, kitId);
83
+ }
84
+ // ─── AC2 manifest filter ─────────────────────────────────────────────────────
85
+ /**
86
+ * Narrow a manifest to an optional single component and/or group (the
87
+ * `componentName` / `group` query params `preview` forwards). Exact,
88
+ * case-sensitive matches — the fuzzy substring search is the viewer's job
89
+ * (`#q`); this pre-filter is deterministic so the inlined payload is stable for
90
+ * a given URI. `groups` is recomputed from what survives so no empty section is
91
+ * inlined. A filter that matches nothing yields an empty-but-valid manifest
92
+ * (the viewer renders its empty state).
93
+ */
94
+ export function filterManifest(manifest, filter) {
95
+ const { componentName, group } = filter;
96
+ if (componentName === undefined && group === undefined)
97
+ return manifest;
98
+ const components = manifest.components.filter((c) => (group === undefined || c.group === group) &&
99
+ (componentName === undefined || c.name === componentName));
100
+ const survivingGroups = new Set(components.map((c) => c.group));
101
+ return {
102
+ ...manifest,
103
+ groups: manifest.groups.filter((g) => survivingGroups.has(g)),
104
+ components,
105
+ };
106
+ }
107
+ // ─── AC4 card `src` rewrite (byte-identical content; transport differs) ───────
108
+ /**
109
+ * Validate + normalise a configured previews base URL. Returns the parsed
110
+ * `URL` (its `href` forced to end in `/` so a later relative-join appends
111
+ * rather than replacing the last path segment) when `raw` is a well-formed
112
+ * ABSOLUTE `http(s)` URL, else `undefined`. A malformed value (a bare host, a
113
+ * typo, a non-http scheme) degrades to `undefined` — the caller then falls
114
+ * back to the solo-dev `data:` transport instead of throwing. Centralising the
115
+ * one `new URL()` parse here is what keeps `rewriteCardPaths` /`buildCspMeta`
116
+ * from crashing `resources/read` or server startup on bad config (the exact
117
+ * failure the reviewer flagged): validate once, branch on the result.
118
+ */
119
+ export function normalizePreviewsBaseUrl(raw) {
120
+ if (raw === undefined || raw === "")
121
+ return undefined;
122
+ let url;
123
+ try {
124
+ url = new URL(raw);
125
+ }
126
+ catch {
127
+ return undefined;
128
+ }
129
+ if (url.protocol !== "https:" && url.protocol !== "http:")
130
+ return undefined;
131
+ if (!url.pathname.endsWith("/"))
132
+ url.pathname += "/";
133
+ return url;
134
+ }
135
+ /**
136
+ * Preserve every card's kit-relative identity as `sourcePath`, then rewrite
137
+ * `path` to the URL the EMBEDDED tier's iframe should load (AC4). With a valid
138
+ * `previewsBaseUrl`, that's an absolute `https://` URL on
139
+ * the separate previews origin (`${base}/${kitId}/${path}`) — the cross-origin
140
+ * isolation the RFC §6/T-09 recommends. Without one — OR when the configured
141
+ * base URL is malformed (see {@link normalizePreviewsBaseUrl}) — the preview's
142
+ * bytes are read and encoded as a `data:text/html;base64,…` URL so the grid is
143
+ * fully self-contained. A preview whose bytes can't be read keeps its relative
144
+ * `path` (harmless: that card simply won't resolve in the embedded tier — a
145
+ * strictly better degradation than dropping the card).
146
+ *
147
+ * Card CONTENT is never altered here — only the URL the iframe points at — so
148
+ * the card stays byte-identical to the `file://`/localhost vehicles (G-5).
149
+ */
150
+ export async function rewriteCardPaths(manifest, opts) {
151
+ const { kitId, kitDir, previewsBaseUrl, readPreviewBytes, cardAssetKit, onPreviewHtml } = opts;
152
+ // Validate the previews base URL ONCE up front; a malformed value degrades to
153
+ // the solo-dev `data:` path rather than throwing per-card.
154
+ const base = normalizePreviewsBaseUrl(previewsBaseUrl);
155
+ const components = await Promise.all(manifest.components.map(async (card) => {
156
+ const sourcePath = card.path;
157
+ if (base !== undefined) {
158
+ // Absolute https:// on the separate previews origin. `base` already ends
159
+ // in `/` (normalizePreviewsBaseUrl) so the kitId segment is appended,
160
+ // not replacing the last path part; `URL` percent-encodes the join.
161
+ const src = new URL(`${encodeURIComponent(kitId)}/${card.path}`, base).toString();
162
+ return { ...card, sourcePath, path: src };
163
+ }
164
+ if (cardAssetKit !== undefined) {
165
+ return { ...card, sourcePath, path: cardAssetKit.urlFor(card.path) };
166
+ }
167
+ // Solo-dev fallback: inline the preview bytes as a data: URL.
168
+ const bytes = await readPreviewBytes(kitDir, card.path);
169
+ if (bytes === null)
170
+ return { ...card, sourcePath }; // keep relative path; degrade gracefully
171
+ onPreviewHtml?.(bytes.toString("utf8"));
172
+ const src = `data:text/html;base64,${bytes.toString("base64")}`;
173
+ return { ...card, sourcePath, path: src };
174
+ }));
175
+ return { ...manifest, components };
176
+ }
177
+ // ─── AC2 manifest inlining (XSS-safe) ────────────────────────────────────────
178
+ /**
179
+ * Escape a JSON string for safe embedding inside `<script type="application/
180
+ * json">`. In JSON, `<`/`>`/`&` only ever occur INSIDE string literals, so
181
+ * replacing them with their `\uXXXX` escapes yields equivalent, still-valid
182
+ * JSON that no HTML parser can misread as a `</script>` terminator or a comment
183
+ * (`<!--`). U+2028/U+2029 are escaped too (defensive; they're legal in JSON but
184
+ * not in JS string literals, and some embedders re-parse). This is the
185
+ * canonical safe-embedding transform.
186
+ */
187
+ export function escapeJsonForScript(json) {
188
+ return json
189
+ .replace(/</g, "\\u003c")
190
+ .replace(/>/g, "\\u003e")
191
+ .replace(/&/g, "\\u0026")
192
+ .replace(/\u2028/g, "\\u2028")
193
+ .replace(/\u2029/g, "\\u2029");
194
+ }
195
+ /**
196
+ * Inject the compiled manifest into the viewer's `index.html` as an inline
197
+ * `<script type="application/json" id="manifest">` (AC2), placed just before
198
+ * `</head>` so it parses before `viewer.js` runs. If no `</head>` is present
199
+ * (defensive — the shipped shell always has one) the script is prepended, which
200
+ * still parses before the body script. The manifest is JSON-escaped so a
201
+ * hostile component name/path can't break out of the script element.
202
+ */
203
+ export function inlineManifest(indexHtml, manifest) {
204
+ const json = escapeJsonForScript(JSON.stringify(manifest));
205
+ const tag = `<script type="application/json" id="${MANIFEST_ELEMENT_ID}">${json}</script>`;
206
+ const headClose = indexHtml.indexOf("</head>");
207
+ if (headClose === -1)
208
+ return tag + indexHtml;
209
+ return indexHtml.slice(0, headClose) + tag + indexHtml.slice(headClose);
210
+ }
211
+ function cspSha256(content) {
212
+ return `'sha256-${createHash("sha256").update(content, "utf8").digest("base64")}'`;
213
+ }
214
+ function rawText(node) {
215
+ if (node.nodeName === "#text")
216
+ return node.value ?? "";
217
+ return (node.childNodes ?? []).map(rawText).join("");
218
+ }
219
+ /**
220
+ * Parse HTML and hash its trusted inline `<script>` / `<style>` text exactly as
221
+ * the browser sees it. External scripts have no inline bytes and are ignored.
222
+ */
223
+ export function collectInlineCspHashes(html) {
224
+ const scripts = new Set();
225
+ const styles = new Set();
226
+ const root = parse(html);
227
+ function visit(node) {
228
+ if (node.tagName === "script") {
229
+ const hasSrc = node.attrs?.some((attr) => attr.name.toLowerCase() === "src") ?? false;
230
+ const content = rawText(node);
231
+ if (!hasSrc && content !== "")
232
+ scripts.add(cspSha256(content));
233
+ }
234
+ else if (node.tagName === "style") {
235
+ const content = rawText(node);
236
+ if (content !== "")
237
+ styles.add(cspSha256(content));
238
+ }
239
+ for (const child of node.childNodes ?? [])
240
+ visit(child);
241
+ }
242
+ visit(root);
243
+ return { scriptHashes: [...scripts], styleHashes: [...styles] };
244
+ }
245
+ function escapeRawTextEndTag(content, tagName) {
246
+ return content.replace(new RegExp(`</${tagName}`, "gi"), `<\\/${tagName}`);
247
+ }
248
+ /**
249
+ * Make the MCP App document self-contained. A host receives one raw HTML
250
+ * resource; browser-relative `ui://` siblings are not additional resources/read
251
+ * calls, so the exact viewer JS/CSS bytes must travel inside that document.
252
+ */
253
+ export function inlineViewerAssets(indexHtml, viewerJs, viewerCss) {
254
+ const script = escapeRawTextEndTag(viewerJs, "script");
255
+ const style = escapeRawTextEndTag(viewerCss, "style");
256
+ const styleTag = `<style>${style}</style>`;
257
+ const scriptTag = `<script>${script}</script>`;
258
+ const cssLink = /<link\b[^>]*href=["']\.\/viewer\.css["'][^>]*>/i;
259
+ const jsScript = /<script\b[^>]*src=["']\.\/viewer\.js["'][^>]*><\/script>/i;
260
+ let html = cssLink.test(indexHtml)
261
+ ? indexHtml.replace(cssLink, styleTag)
262
+ : injectBeforeClosingTag(indexHtml, "head", styleTag);
263
+ html = jsScript.test(html)
264
+ ? html.replace(jsScript, scriptTag)
265
+ : injectBeforeClosingTag(html, "body", scriptTag);
266
+ return {
267
+ html,
268
+ hashes: {
269
+ scriptHashes: [cspSha256(script)],
270
+ styleHashes: [cspSha256(style)],
271
+ },
272
+ };
273
+ }
274
+ function injectBeforeClosingTag(html, tagName, content) {
275
+ const close = `</${tagName}>`;
276
+ const at = html.indexOf(close);
277
+ return at === -1 ? html + content : html.slice(0, at) + content + html.slice(at);
278
+ }
279
+ /**
280
+ * Build the AC5 domain allow-list and strict hash-based document policy.
281
+ *
282
+ * The card iframes load from the previews origin when configured, else from
283
+ * `data:` (solo dev). `connectDomains` is always empty — `connect-src 'none'`
284
+ * — because the manifest travels inline (AC2); there is nothing to fetch.
285
+ * `img-src` permits `data:`/`https:` for card thumbnails. The domain shape
286
+ * matches the stable MCP Apps 2026-01-26 `_meta.ui.csp` contract.
287
+ *
288
+ * Hardening applied vs the pre-M4-07 policy:
289
+ * • Removed `'unsafe-inline'` / `'unsafe-eval'` (AC3). The exact inlined
290
+ * viewer assets and legitimate inline blocks inside data-backed cards are
291
+ * allow-listed by SHA-256. Event-handler attributes remain blocked because
292
+ * the policy does not opt into `'unsafe-hashes'`.
293
+ * • Added `base-uri 'none'` — no `<base href>` can redirect URLs.
294
+ * • Added `form-action 'none'` and `object-src 'none'` — deny `<form>` /
295
+ * `<object>` / `<embed>` per AC1's "deny form/object/embed" line.
296
+ * Resources/read has no HTTP-header channel, so no raw header policy or
297
+ * `frame-ancestors` claim is advertised.
298
+ */
299
+ export function buildCspMeta(previewsBaseUrl, inlineHashes = { scriptHashes: [], styleHashes: [] }, exactFrameDomains) {
300
+ // Validate via the shared normaliser so a malformed GENIE_PREVIEWS_BASE_URL
301
+ // NEVER throws here — an invalid value degrades to the solo-dev `data:` frame
302
+ // origin exactly as `rewriteCardPaths` does, keeping the two in lockstep and
303
+ // never crashing `registerGridResource`/server startup (reviewer flag).
304
+ const base = normalizePreviewsBaseUrl(previewsBaseUrl);
305
+ const frameDomains = base !== undefined
306
+ ? [base.origin]
307
+ : exactFrameDomains === undefined
308
+ ? ["data:"]
309
+ : [...new Set(exactFrameDomains)];
310
+ const resourceDomains = [];
311
+ const frameSrc = frameDomains.length === 0 ? "'none'" : frameDomains.join(" ");
312
+ const scriptHashes = validCspHashes(inlineHashes.scriptHashes);
313
+ const styleHashes = validCspHashes(inlineHashes.styleHashes);
314
+ const scriptSrc = scriptHashes.length > 0 ? scriptHashes.join(" ") : "'none'";
315
+ const styleSrc = styleHashes.length > 0 ? styleHashes.join(" ") : "'none'";
316
+ const core = [
317
+ "default-src 'none'",
318
+ `script-src ${scriptSrc}`,
319
+ `style-src ${styleSrc}`,
320
+ "img-src data: https:",
321
+ `frame-src ${frameSrc}`,
322
+ "connect-src 'none'",
323
+ "object-src 'none'",
324
+ "base-uri 'none'",
325
+ "form-action 'none'",
326
+ ];
327
+ const metaPolicy = core.join("; ");
328
+ return { connectDomains: [], resourceDomains, frameDomains, metaPolicy };
329
+ }
330
+ function validCspHashes(hashes) {
331
+ return [...new Set(hashes.filter((hash) => /^'sha256-[A-Za-z0-9+/]+={0,2}'$/.test(hash)))];
332
+ }
333
+ /**
334
+ * Render a `<meta http-equiv="Content-Security-Policy" content="…">` tag for
335
+ * the given policy meta. Uses {@link GridCspMeta.metaPolicy}.
336
+ *
337
+ * The `content` attribute value is HTML-attribute-escaped (`&`, `"`, `<`, `>`)
338
+ * defensively. In current use the policy string is server-authored with no
339
+ * user input, so the escape is defence-in-depth against a future change that
340
+ * accidentally embeds a `"` — without it the attribute would terminate early
341
+ * and the policy would silently degrade.
342
+ */
343
+ export function cspMetaTag(meta) {
344
+ const attr = escapeHtmlAttribute(meta.metaPolicy);
345
+ return `<meta http-equiv="Content-Security-Policy" content="${attr}">`;
346
+ }
347
+ /**
348
+ * Escape a string for safe embedding inside a `"`-quoted HTML attribute value.
349
+ * Handles the four characters `&` (must be first, else it double-escapes
350
+ * subsequent `&`s), `"` (attribute delimiter), `<`, `>`. Defence-in-depth for
351
+ * {@link cspMetaTag} — the policy is server-authored today, but a future
352
+ * directive that legitimately contains a `"` (e.g. a hostname in quotes) must
353
+ * not break the attribute.
354
+ */
355
+ function escapeHtmlAttribute(str) {
356
+ return str
357
+ .replace(/&/g, "&amp;")
358
+ .replace(/"/g, "&quot;")
359
+ .replace(/</g, "&lt;")
360
+ .replace(/>/g, "&gt;");
361
+ }
362
+ /**
363
+ * Inject the enforced CSP meta into an assembled document. Placed at the very
364
+ * top of `<head>` so every subsequent element — the manifest data island,
365
+ * inlined `viewer.js`/`viewer.css`, any inline attributes — is parsed under
366
+ * the policy. If the shell has no `<head>` (defensive; every shipped shell
367
+ * does) the tag is prepended so it still parses first.
368
+ */
369
+ export function injectCspMeta(html, meta) {
370
+ const tag = cspMetaTag(meta);
371
+ const headOpen = html.indexOf("<head>");
372
+ if (headOpen === -1)
373
+ return tag + html;
374
+ return html.slice(0, headOpen + "<head>".length) + tag + html.slice(headOpen + "<head>".length);
375
+ }
376
+ /** Canonical MCP Apps resource metadata (stable 2026-01-26 shape). */
377
+ export function gridResourceMeta(previewsBaseUrl, exactFrameDomains) {
378
+ const { connectDomains, resourceDomains, frameDomains } = buildCspMeta(previewsBaseUrl, undefined, exactFrameDomains);
379
+ return {
380
+ ui: { csp: { connectDomains, resourceDomains, frameDomains } },
381
+ "openai/widgetCSP": {
382
+ connect_domains: connectDomains,
383
+ resource_domains: resourceDomains,
384
+ frame_domains: frameDomains,
385
+ },
386
+ };
387
+ }
388
+ // ─── Default seams ───────────────────────────────────────────────────────────
389
+ /** Default compiler: the shared `ensureManifest` seam (compiles + persists). */
390
+ const defaultCompile = async (kitDir) => {
391
+ return ensureManifest(kitDir);
392
+ };
393
+ /**
394
+ * Default asset reader: prefer the viewer assets copied beside the compiled
395
+ * server module by `copy-viewer-assets.mjs`. This guarantees the published
396
+ * server's tool-result shell remains executable even when `@ambitresearch/genie-viewer` is
397
+ * not installed at runtime. Source/tsx development falls back to resolving the
398
+ * workspace viewer package without creating a build-time import edge. Results
399
+ * are cached per process.
400
+ */
401
+ function makeDefaultAssetReader() {
402
+ const cache = new Map();
403
+ const require = createRequire(import.meta.url);
404
+ const bundledStaticDir = resolve(dirname(fileURLToPath(import.meta.url)), "viewer-static");
405
+ return async (name) => {
406
+ const cached = cache.get(name);
407
+ if (cached !== undefined)
408
+ return cached;
409
+ let text;
410
+ try {
411
+ text = await readFile(join(bundledStaticDir, name), "utf8");
412
+ }
413
+ catch (error) {
414
+ const code = error.code;
415
+ if (code !== "ENOENT" && code !== "ENOTDIR")
416
+ throw error;
417
+ const pkgJson = require.resolve("@ambitresearch/genie-viewer/package.json");
418
+ const staticDir = resolve(dirname(pkgJson), "static");
419
+ text = await readFile(join(staticDir, name), "utf8");
420
+ }
421
+ cache.set(name, text);
422
+ return text;
423
+ };
424
+ }
425
+ /** Default preview reader: read `join(kitDir, relPath)`; null on any error. */
426
+ const defaultReadPreviewBytes = async (kitDir, relPath) => {
427
+ try {
428
+ // `relPath` originates from the compiled manifest (already inside the kit);
429
+ // still re-check containment defensively before reading. Uses the SAME
430
+ // segment-aware guard as `store/local.ts`'s `safePath` (path.relative + sep
431
+ // + isAbsolute) rather than a `startsWith(root + "/")` prefix test, which
432
+ // is wrong on Windows (`\` separator) and can false-negative a legitimate
433
+ // sibling dir sharing a name prefix.
434
+ const root = resolve(kitDir);
435
+ const abs = resolve(root, relPath);
436
+ if (!isInside(root, abs))
437
+ return null;
438
+ return await readFile(abs);
439
+ }
440
+ catch {
441
+ return null;
442
+ }
443
+ };
444
+ /**
445
+ * True when `child` is `parent` itself or a descendant of it, using the
446
+ * repo's established segment-aware containment test (mirrors `safePath` in
447
+ * `store/local.ts`): the relative path escapes only when it IS `..`, starts
448
+ * with `..` + a path separator, or is absolute. Cross-platform (honours the
449
+ * OS `sep`), unlike a `startsWith(parent + "/")` prefix check.
450
+ */
451
+ function isInside(parent, child) {
452
+ const rel = relative(parent, child);
453
+ if (rel === "")
454
+ return true; // child === parent
455
+ return rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel);
456
+ }
457
+ /** A last-resort, dependency-free shell used only when both the packaged assets
458
+ * and source-workspace fallback are unreadable (or a test injects a failing
459
+ * reader). Normal built deployments always carry executable assets beside this
460
+ * module. The emergency shell keeps `resources/read` valid and carries the SAME
461
+ * enforced CSP meta: corruption must not downgrade security (M4-07 AC1). */
462
+ function fallbackShell(manifest, cspMeta) {
463
+ const json = escapeJsonForScript(JSON.stringify(manifest));
464
+ return ('<!doctype html><html lang="en"><head>' +
465
+ cspMetaTag(cspMeta) +
466
+ '<meta charset="utf-8" />' +
467
+ `<title>genie — preview</title>` +
468
+ `<script type="application/json" id="${MANIFEST_ELEMENT_ID}">${json}</script>` +
469
+ '</head><body><main id="grid"></main>' +
470
+ "<p>genie viewer assets unavailable in this environment.</p>" +
471
+ "</body></html>");
472
+ }
473
+ /**
474
+ * Produce the embedded grid HTML for a resource URI's query params. Resolves
475
+ * the kit (path-guarded), compiles + filters the manifest, rewrites card paths
476
+ * for the embedded tier (AC4), inlines the result into the viewer shell
477
+ * (AC2/AC3), and injects the enforced CSP meta (M4-07 AC1) so browsers apply
478
+ * the hardened policy at parse time. Every failure mode degrades to a still-
479
+ * valid, still-hardened document: an absent/invalid `kitId` or an
480
+ * uncompilable kit yields an EMPTY manifest (the viewer's empty state);
481
+ * unreadable viewer assets yield the {@link fallbackShell} (also CSP-injected).
482
+ */
483
+ export async function buildGridDocument(deps, params) {
484
+ const kitDir = deps.denyRequestedKit ? null : resolveKitDir(deps.kitsRoot, params.kitId);
485
+ const scriptHashes = new Set();
486
+ const styleHashes = new Set();
487
+ const addHashes = (html) => {
488
+ const hashes = collectInlineCspHashes(html);
489
+ for (const hash of hashes.scriptHashes)
490
+ scriptHashes.add(hash);
491
+ for (const hash of hashes.styleHashes)
492
+ styleHashes.add(hash);
493
+ };
494
+ const currentHashes = () => ({
495
+ scriptHashes: [...scriptHashes],
496
+ styleHashes: [...styleHashes],
497
+ });
498
+ let manifest = emptyManifest();
499
+ if (kitDir !== null) {
500
+ try {
501
+ manifest = await deps.compile(kitDir);
502
+ }
503
+ catch {
504
+ manifest = emptyManifest(); // uncompiled/missing kit → empty grid, not an error
505
+ }
506
+ manifest = filterManifest(manifest, {
507
+ componentName: params.componentName,
508
+ group: params.group,
509
+ });
510
+ manifest = await rewriteCardPaths(manifest, {
511
+ kitId: params.kitId, // non-null: resolveKitDir only returns a dir for a valid id
512
+ kitDir,
513
+ previewsBaseUrl: deps.previewsBaseUrl,
514
+ readPreviewBytes: deps.readPreviewBytes,
515
+ cardAssetKit: deps.cardAssetKit,
516
+ onPreviewHtml: addHashes,
517
+ });
518
+ }
519
+ let indexHtml;
520
+ let viewerJs;
521
+ let viewerCss;
522
+ try {
523
+ [indexHtml, viewerJs, viewerCss] = await Promise.all([
524
+ deps.readAsset("index.html"),
525
+ deps.readAsset("viewer.js"),
526
+ deps.readAsset("viewer.css"),
527
+ ]);
528
+ }
529
+ catch {
530
+ const fallback = fallbackShell(manifest, buildCspMeta(deps.previewsBaseUrl, currentHashes(), deps.exactFrameDomains));
531
+ return params.kitId === undefined ? markToolResultShell(fallback) : fallback;
532
+ }
533
+ const inlinedAssets = inlineViewerAssets(indexHtml, viewerJs, viewerCss);
534
+ for (const hash of inlinedAssets.hashes.scriptHashes)
535
+ scriptHashes.add(hash);
536
+ for (const hash of inlinedAssets.hashes.styleHashes)
537
+ styleHashes.add(hash);
538
+ const cspMeta = buildCspMeta(deps.previewsBaseUrl, currentHashes(), deps.exactFrameDomains);
539
+ const document = inlineManifest(inlinedAssets.html, manifest);
540
+ return injectCspMeta(params.kitId === undefined ? markToolResultShell(document) : document, cspMeta);
541
+ }
542
+ function markToolResultShell(html) {
543
+ const marker = `<meta name="${TOOL_RESULT_SHELL_META}" content="true">`;
544
+ const headOpen = html.indexOf("<head>");
545
+ if (headOpen === -1)
546
+ return marker + html;
547
+ return (html.slice(0, headOpen + "<head>".length) + marker + html.slice(headOpen + "<head>".length));
548
+ }
549
+ /** An empty-but-valid manifest (viewer renders its empty state). */
550
+ function emptyManifest() {
551
+ return { version: 1, name: "", generatedAt: "", groups: [], components: [] };
552
+ }
553
+ // ─── MCP registration (AC1 + AC3) ────────────────────────────────────────────
554
+ /** Parse the three recognised query params off a resolved resource `URL`. */
555
+ function paramsFromUri(uri) {
556
+ const get = (k) => uri.searchParams.get(k) ?? undefined;
557
+ return { kitId: get("kitId"), componentName: get("componentName"), group: get("group") };
558
+ }
559
+ async function registerRequestedCardAssetKit(broker, kitsRoot, kitId) {
560
+ const kitDir = resolveKitDir(kitsRoot, kitId);
561
+ if (kitDir === null)
562
+ return undefined;
563
+ let info;
564
+ try {
565
+ info = await lstat(kitDir);
566
+ }
567
+ catch (error) {
568
+ if (error instanceof Error &&
569
+ "code" in error &&
570
+ (error.code === "ENOENT" ||
571
+ error.code === "ENOTDIR")) {
572
+ return undefined;
573
+ }
574
+ throw error;
575
+ }
576
+ if (!info.isDirectory())
577
+ return undefined;
578
+ return await broker.registerKit(kitId, kitDir);
579
+ }
580
+ async function prepareCardBroker(deps, params) {
581
+ if (deps.getCardAssetBroker === undefined ||
582
+ normalizePreviewsBaseUrl(deps.previewsBaseUrl) !== undefined) {
583
+ return undefined;
584
+ }
585
+ let broker;
586
+ try {
587
+ broker = await deps.getCardAssetBroker();
588
+ }
589
+ catch {
590
+ return {
591
+ exactFrameDomains: [],
592
+ denyRequestedKit: params.kitId !== undefined,
593
+ };
594
+ }
595
+ if (params.kitId === undefined) {
596
+ return { exactFrameDomains: broker.frameOrigins() };
597
+ }
598
+ if (!KIT_ID_PATTERN.test(params.kitId)) {
599
+ return { exactFrameDomains: broker.frameOrigins() };
600
+ }
601
+ const cardAssetKit = await registerRequestedCardAssetKit(broker, deps.kitsRoot, params.kitId);
602
+ if (cardAssetKit === undefined) {
603
+ return {
604
+ exactFrameDomains: broker.frameOrigins(),
605
+ denyRequestedKit: true,
606
+ };
607
+ }
608
+ return { cardAssetKit, exactFrameDomains: broker.frameOrigins() };
609
+ }
610
+ /**
611
+ * Register `ui://genie/grid` and its sibling assets on `server`.
612
+ *
613
+ * Routing (verified against the installed SDK): the SDK matches a static
614
+ * resource by EXACT `uri.toString()` and a template via `UriTemplate.match`.
615
+ * `preview` emits `ui://genie/grid?kitId=…` with optional extra params, so a
616
+ * single static registration (no query) or a rigid `{?kitId,…}` template (all-
617
+ * or-nothing) would miss most real URIs. We therefore register BOTH a static
618
+ * `ui://genie/grid` (the bare URI) and a `{+rest}` catch-all template that
619
+ * matches any query-bearing URI; both dispatch to the same handler, which reads
620
+ * its params off the full `URL`. Legacy sibling assets remain plain statics.
621
+ */
622
+ export function registerGridResource(server, options) {
623
+ const deps = {
624
+ kitsRoot: options.kitsRoot,
625
+ compile: options.compile ?? defaultCompile,
626
+ readAsset: options.readAsset ?? makeDefaultAssetReader(),
627
+ readPreviewBytes: options.readPreviewBytes ?? defaultReadPreviewBytes,
628
+ previewsBaseUrl: options.previewsBaseUrl ?? process.env.GENIE_PREVIEWS_BASE_URL,
629
+ getCardAssetBroker: options.getCardAssetBroker,
630
+ };
631
+ const hasLocalCardBroker = deps.getCardAssetBroker !== undefined &&
632
+ normalizePreviewsBaseUrl(deps.previewsBaseUrl) === undefined;
633
+ // Resource-list metadata is necessarily static. A broker is lazy, so its
634
+ // concrete origins are attached to resources/read after discovery; publish
635
+ // an empty (deny-all) exact list here rather than a wildcard or `data:`.
636
+ const registrationMeta = gridResourceMeta(deps.previewsBaseUrl, hasLocalCardBroker ? [] : undefined);
637
+ const baseGridConfig = {
638
+ title: "genie preview grid",
639
+ description: "Embedded MCP-Apps preview of a genie UI kit — a card grid rendered inside " +
640
+ "the host's sandboxed iframe. Referenced by the `preview` tool's " +
641
+ "_meta.ui.resourceUri.",
642
+ mimeType: GRID_RESOURCE_MIME,
643
+ };
644
+ const readGrid = async (uri) => {
645
+ const params = paramsFromUri(uri);
646
+ const prepared = await prepareCardBroker(deps, params);
647
+ const readDeps = prepared === undefined
648
+ ? deps
649
+ : {
650
+ ...deps,
651
+ cardAssetKit: prepared.cardAssetKit,
652
+ exactFrameDomains: prepared.exactFrameDomains,
653
+ denyRequestedKit: prepared.denyRequestedKit,
654
+ };
655
+ const meta = gridResourceMeta(deps.previewsBaseUrl, prepared?.exactFrameDomains);
656
+ const html = await buildGridDocument(readDeps, params);
657
+ return {
658
+ contents: [{ uri: uri.toString(), mimeType: GRID_RESOURCE_MIME, text: html, _meta: meta }],
659
+ };
660
+ };
661
+ // Bare URI (exact match) — e.g. a host that reads `ui://genie/grid` directly.
662
+ server.registerResource("genie-grid", GRID_RESOURCE_URI, { ...baseGridConfig, _meta: registrationMeta }, (uri) => readGrid(uri));
663
+ // Query-bearing URIs (`?kitId=…[&componentName=…][&group=…]`) via a catch-all
664
+ // template. `list: undefined` — the bare static above is what `resources/list`
665
+ // advertises; the template is a routing device, not a discoverable resource.
666
+ const template = new ResourceTemplate(`${GRID_RESOURCE_URI}{+rest}`, { list: undefined });
667
+ server.registerResource("genie-grid-query", template, { ...baseGridConfig, _meta: registrationMeta }, (uri) => readGrid(uri));
668
+ // Compatibility for older experimental hosts. The standard MCP Apps payload
669
+ // is self-contained and does not reference these sibling resources.
670
+ registerAsset(server, "genie-viewer-js", VIEWER_JS_URI, "text/javascript", () => deps.readAsset("viewer.js"));
671
+ registerAsset(server, "genie-viewer-css", VIEWER_CSS_URI, "text/css", () => deps.readAsset("viewer.css"));
672
+ }
673
+ /** Register one static text asset resource (viewer.js / viewer.css). */
674
+ function registerAsset(server, name, uri, mimeType, read) {
675
+ server.registerResource(name, uri, { title: name, mimeType }, async (u) => {
676
+ const text = await read();
677
+ return { contents: [{ uri: u.toString(), mimeType, text }] };
678
+ });
679
+ }
680
+ //# sourceMappingURL=grid-resource.js.map