@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,672 @@
1
+ /**
2
+ * MCP tool: preview (M4-05 / DRO-267).
3
+ *
4
+ * Returns a human-readable summary — the live viewer URL plus a local-only
5
+ * `file://` fallback — as `content`. The tool advertises the static
6
+ * `ui://genie/grid` app shell in tools/list for standards-compliant discovery;
7
+ * the host then delivers this call's input/result to that shell. The result
8
+ * also retains its query-bearing resource URI for legacy/result-level hosts.
9
+ *
10
+ * ── Contract (RFC §6.2.3 / §8.3; this issue's AC1–AC7) ───────────────────────
11
+ * input : { kitId, componentName?, group? } (AC2)
12
+ * output: { content: [{ type:"text", text }],
13
+ * _meta: { ui: { resourceUri: "ui://genie/grid?…" } } } (AC3/AC4)
14
+ * The `ui://genie/grid` RESOURCE itself is registered by M4-06. The static
15
+ * tools/list association and query-bearing result URI are emitted
16
+ * unconditionally; a host without MCP Apps support simply renders the text.
17
+ *
18
+ * ── Viewer boot seam (AC5/AC6) ───────────────────────────────────────────────
19
+ * Booting the Vite dev server is behind an injectable `ViewerBooter` so the
20
+ * tool has NO hard dependency on `@ambitresearch/genie-viewer` or Vite — the server core
21
+ * stays independent of the preview framework (CLAUDE.md; RFC §4). The default
22
+ * booter lazily `import()`s `@ambitresearch/genie-viewer` through a NON-LITERAL specifier
23
+ * (so `tsc` never hard-resolves it) and degrades gracefully when it is absent —
24
+ * exactly the OPTIONAL-peer-dependency pattern `refine` uses for Playwright.
25
+ * A failed boot (Vite missing, port unbindable, kit dir invalid) is caught and
26
+ * turned into AC6's local `file://<kitDir>/index.html` fallback; MCP Apps can
27
+ * instead receive a CSP-safe embedded manifest when a previews origin is
28
+ * configured. `_meta` is still emitted. A `ViewerRegistry` caches one running
29
+ * viewer per kit dir (AC5).
30
+ */
31
+ import { lstat, readFile } from "node:fs/promises";
32
+ import { join } from "node:path";
33
+ import { pathToFileURL } from "node:url";
34
+ import { z } from "zod";
35
+ import { ensureManifest } from "../manifest/index.js";
36
+ import { getServerTransportKind } from "../transport.js";
37
+ import { filterManifest, GRID_RESOURCE_URI, normalizePreviewsBaseUrl, rewriteCardPaths, } from "../ui/grid-resource.js";
38
+ import { KIT_ID_PATTERN } from "./get_kit.js";
39
+ export const PREVIEW_TOOL_NAME = "mcp__genie__preview";
40
+ /**
41
+ * Vite dev-server default port. Duplicated as a local literal (rather than
42
+ * imported from `@ambitresearch/genie-viewer`) so this module carries no build-time edge to
43
+ * the viewer package — the whole point of the lazy boot seam below. Kept in
44
+ * sync with `@ambitresearch/genie-viewer`'s `DEFAULT_VIEWER_PORT` (RFC §6.9/§14).
45
+ */
46
+ export const DEFAULT_VIEWER_PORT = 5173;
47
+ /**
48
+ * The MCP Apps extension identifier (ext-apps spec 2026-01-26, "Client<>Server
49
+ * Capability Negotiation"): a host that renders `ui://` app resources
50
+ * advertises `capabilities.extensions["io.modelcontextprotocol/ui"]` in its
51
+ * `initialize` request, with `mimeTypes` naming the profiles it can render.
52
+ */
53
+ export const UI_EXTENSION_ID = "io.modelcontextprotocol/ui";
54
+ /** The MCP-Apps HTML profile the grid resource serves (spec §Resource Discovery). */
55
+ export const MCP_APP_MIME = "text/html;profile=mcp-app";
56
+ /** Widget-only tool-result metadata carrying CSP-authorized card routes. */
57
+ export const PREVIEW_EMBEDDED_MANIFEST_META_KEY = "genie/embeddedManifest";
58
+ /**
59
+ * Authoritative `ui://` capability check: true when the client's `initialize`
60
+ * capabilities carry the MCP Apps extension with a `mimeTypes` array that
61
+ * includes {@link MCP_APP_MIME}. This is the spec-defined negotiation. `caps`
62
+ * is typed `unknown` because the SDK's `ClientCapabilities` models
63
+ * `extensions` as a passthrough bag — every level is checked defensively.
64
+ */
65
+ export function hasUiExtensionCapability(caps) {
66
+ return getUiExtensionCapability(caps) === true;
67
+ }
68
+ /**
69
+ * Resolve MCP Apps negotiation without collapsing "not advertised" and
70
+ * "explicitly not supported." A missing `extensions` mechanism returns
71
+ * `undefined`. This is distinct from an explicit negative because metadata-linked
72
+ * MCP Apps hosts may omit the extension capability even when they advertise
73
+ * unrelated extensions. Only a present UI extension with a malformed or
74
+ * unsupported MIME declaration is a negotiated negative.
75
+ */
76
+ export function getUiExtensionCapability(caps) {
77
+ if (typeof caps !== "object" || caps === null)
78
+ return undefined;
79
+ if (!Object.prototype.hasOwnProperty.call(caps, "extensions"))
80
+ return undefined;
81
+ const extensions = caps.extensions;
82
+ if (typeof extensions !== "object" || extensions === null)
83
+ return false;
84
+ if (!Object.prototype.hasOwnProperty.call(extensions, UI_EXTENSION_ID))
85
+ return undefined;
86
+ const ext = extensions[UI_EXTENSION_ID];
87
+ if (typeof ext !== "object" || ext === null)
88
+ return false;
89
+ const mimeTypes = ext.mimeTypes;
90
+ return Array.isArray(mimeTypes) && mimeTypes.includes(MCP_APP_MIME);
91
+ }
92
+ // ─── Structured stderr log (never stdout — it IS the stdio JSON-RPC stream) ───
93
+ /**
94
+ * Emit one structured audit/telemetry line to stderr. On the stdio transport
95
+ * stdout carries the JSON-RPC frames, so any diagnostic MUST go to stderr —
96
+ * the same convention `plan-guard`, `plan`, and the LLM verbs follow.
97
+ */
98
+ function logStderr(payload) {
99
+ process.stderr.write(JSON.stringify(payload) + "\n");
100
+ }
101
+ /**
102
+ * Build the `ui://genie/grid?…` resource URI (AC3/AC4). The query string always
103
+ * carries `kitId` and appends the optional `componentName` / `group` filters in
104
+ * a STABLE order (kitId → componentName → group) so the URI — and thus the
105
+ * resource M4-06 resolves and any cache keyed on it — is deterministic for a
106
+ * given argument set. Values are percent-encoded via `URLSearchParams`.
107
+ */
108
+ export function buildResourceUri(params) {
109
+ const uri = new URL(GRID_RESOURCE_URI);
110
+ uri.searchParams.set("kitId", params.kitId);
111
+ if (params.componentName !== undefined)
112
+ uri.searchParams.set("componentName", params.componentName);
113
+ if (params.group !== undefined)
114
+ uri.searchParams.set("group", params.group);
115
+ return uri.toString();
116
+ }
117
+ // ─── kitId → kit dir (path safety) ───────────────────────────────────────────
118
+ /** A `kitId` that fails the shared `KIT_ID_PATTERN` shape (traversal guard). */
119
+ export class InvalidKitIdError extends Error {
120
+ kitId;
121
+ code = "InvalidKitIdError";
122
+ constructor(kitId) {
123
+ super(`Invalid kitId "${kitId}": must be a 3-64 char lowercase slug ([a-z0-9-]). ` +
124
+ "This guards against path traversal into the kits root.");
125
+ this.kitId = kitId;
126
+ this.name = "InvalidKitIdError";
127
+ }
128
+ }
129
+ /** A well-formed kitId that does not identify an existing kit directory. */
130
+ export class KitNotFoundError extends Error {
131
+ kitId;
132
+ code = "KitNotFoundError";
133
+ constructor(kitId) {
134
+ super(`Kit "${kitId}" does not exist. Call list_kits or create_kit first.`);
135
+ this.kitId = kitId;
136
+ this.name = "KitNotFoundError";
137
+ }
138
+ }
139
+ /**
140
+ * Resolve a `kitId` to its on-disk kit directory under `kitsRoot`, rejecting
141
+ * any id that isn't a well-formed slug BEFORE the `join` — an unvalidated id
142
+ * like `../../etc` would otherwise escape the kits root (RFC §10 T-13, the same
143
+ * traversal class `write_files`/`read_file` guard). Only shape is checked here;
144
+ * Existence is checked by {@link runPreview} before compilation so a missing
145
+ * slug cannot create a phantom kit through manifest persistence.
146
+ */
147
+ export function resolveKitDir(kitsRoot, kitId) {
148
+ if (!KIT_ID_PATTERN.test(kitId)) {
149
+ throw new InvalidKitIdError(kitId);
150
+ }
151
+ return join(kitsRoot, kitId);
152
+ }
153
+ /**
154
+ * Caches one running viewer per kit directory so repeated `preview` calls reuse
155
+ * it instead of booting a fresh Vite each time (AC5). Keyed on the resolved kit
156
+ * dir; concurrent calls for the same dir share ONE in-flight boot (the promise
157
+ * is cached, not just the resolved value). A boot that REJECTS is evicted so a
158
+ * later call retries rather than replaying the dead promise forever.
159
+ */
160
+ export class ViewerRegistry {
161
+ booter;
162
+ viewers = new Map();
163
+ constructor(booter) {
164
+ this.booter = booter;
165
+ }
166
+ queueBrowserOpen(entry, kitDir, target) {
167
+ const previous = entry.browserOpen?.promise ?? Promise.resolve();
168
+ const promise = Promise.all([entry.viewer, previous]).then(async ([viewer]) => {
169
+ try {
170
+ await viewer.open(target ?? undefined);
171
+ }
172
+ catch (error) {
173
+ logStderr({
174
+ event: "preview.browser.open-failed",
175
+ kitDir,
176
+ ...(target !== null ? { target } : {}),
177
+ error: String(error),
178
+ });
179
+ }
180
+ });
181
+ entry.browserOpen = { target, promise };
182
+ return promise;
183
+ }
184
+ /**
185
+ * Return the viewer for `kitDir`, booting it on first request. Subsequent
186
+ * calls for the same dir return the cached (in-flight or resolved) promise.
187
+ *
188
+ * Booting and browser opening are tracked separately. Direct callers can use
189
+ * `open:true` for the base URL, while {@link runPreview}'s local path boots or
190
+ * reuses headlessly and calls {@link ViewerRegistry.open} after constructing
191
+ * its filtered target. Repeated requests for the same target share one
192
+ * promise; a changed filter target is opened again so the requested component
193
+ * is not hidden behind an older tab.
194
+ */
195
+ ensure(kitDir, port = DEFAULT_VIEWER_PORT, open = false) {
196
+ const existing = this.viewers.get(kitDir);
197
+ if (existing !== undefined) {
198
+ if (open) {
199
+ const browserOpen = existing.browserOpen?.target === null
200
+ ? existing.browserOpen.promise
201
+ : this.queueBrowserOpen(existing, kitDir, null);
202
+ return Promise.all([existing.viewer, browserOpen]).then(([viewer]) => viewer);
203
+ }
204
+ return existing.viewer;
205
+ }
206
+ const booting = this.booter({ kitDir, port, open }).catch((error) => {
207
+ // Evict the failed boot so the next call retries with a fresh attempt
208
+ // rather than being handed this same rejected promise (AC5 durability).
209
+ this.viewers.delete(kitDir);
210
+ throw error;
211
+ });
212
+ const entry = { viewer: booting };
213
+ if (open) {
214
+ entry.browserOpen = { target: null, promise: booting.then(() => undefined) };
215
+ }
216
+ this.viewers.set(kitDir, entry);
217
+ return booting;
218
+ }
219
+ async open(kitDir, target) {
220
+ const entry = this.viewers.get(kitDir);
221
+ if (entry === undefined) {
222
+ throw new Error(`Viewer for "${kitDir}" must be booted before it can be opened.`);
223
+ }
224
+ const browserOpen = entry.browserOpen?.target === target
225
+ ? entry.browserOpen.promise
226
+ : this.queueBrowserOpen(entry, kitDir, target);
227
+ await browserOpen;
228
+ }
229
+ /** Tear down every booted viewer (best-effort). For clean shutdown/tests. */
230
+ async closeAll() {
231
+ const entries = Array.from(this.viewers.values());
232
+ this.viewers.clear();
233
+ await Promise.allSettled(entries.map(async ({ viewer: viewerPromise, browserOpen }) => {
234
+ const viewer = await viewerPromise;
235
+ try {
236
+ await browserOpen?.promise;
237
+ }
238
+ finally {
239
+ await viewer.close();
240
+ }
241
+ }));
242
+ }
243
+ }
244
+ /**
245
+ * The default `ViewerBooter`. Lazily loads `@ambitresearch/genie-viewer` and boots its Vite
246
+ * dev server against the kit dir and honors {@link BootRequest.open} for direct
247
+ * callers. On its local path, {@link runPreview} deliberately passes `false`,
248
+ * constructs the filter-bearing viewer URL, and invokes
249
+ * {@link ViewerRegistry.open} separately so browser fallback never opens the
250
+ * unfiltered base URL. Absent → `false` (never opens), preserving the safe
251
+ * headless default.
252
+ * ALL of the viewer's own stdout is routed to STDERR — on the stdio transport
253
+ * the tool's stdout is the JSON-RPC stream, so the viewer's `Preview:` banner
254
+ * would corrupt framing if left on stdout.
255
+ */
256
+ export const defaultViewerBooter = async ({ kitDir, port, open }) => {
257
+ // Non-literal specifier: keeps `tsc` from resolving the optional dep at build.
258
+ const specifier = "@ambitresearch/genie-viewer";
259
+ let mod;
260
+ try {
261
+ mod = (await import(specifier));
262
+ }
263
+ catch (error) {
264
+ logStderr({
265
+ event: "preview.viewer.unavailable",
266
+ reason: "viewer-package-not-installed",
267
+ error: String(error),
268
+ });
269
+ throw error;
270
+ }
271
+ // Route the viewer's stdout to stderr; keep its stderr on stderr.
272
+ const io = {
273
+ stdout: (chunk) => process.stderr.write(chunk),
274
+ stderr: (chunk) => process.stderr.write(chunk),
275
+ };
276
+ const handle = await mod.bootViewer({ root: kitDir, port: port ?? DEFAULT_VIEWER_PORT, open: open ?? false }, io);
277
+ return {
278
+ url: handle.url,
279
+ port: handle.port,
280
+ open: async (target) => {
281
+ if (handle.open === undefined) {
282
+ logStderr({
283
+ event: "preview.browser.open-unavailable",
284
+ reason: "viewer-handle-does-not-support-open",
285
+ url: handle.url,
286
+ });
287
+ return;
288
+ }
289
+ await handle.open(target);
290
+ },
291
+ close: () => handle.close(),
292
+ };
293
+ };
294
+ /** Point filtered cards at one broker-owned kit origin without changing bytes. */
295
+ function rewriteCardPathsForBroker(manifest, registeredKit) {
296
+ return {
297
+ ...manifest,
298
+ components: manifest.components.map((card) => ({
299
+ ...card,
300
+ sourcePath: card.path,
301
+ path: registeredKit.urlFor(card.path),
302
+ })),
303
+ };
304
+ }
305
+ /**
306
+ * True when the operator has opted OUT of preview's server-side browser
307
+ * auto-open by setting `GENIE_PREVIEW_NO_OPEN` to a truthy value (design
308
+ * 2026-07-05, piece B — auto-open is opt-OUT, on by default for non-`ui://`
309
+ * hosts). Any non-empty value other than `0`/`false` (case-insensitive) counts
310
+ * as set, so `GENIE_PREVIEW_NO_OPEN=1`, `=true`, `=yes` all disable it.
311
+ */
312
+ export function autoOpenDisabledByEnv(env = process.env) {
313
+ const raw = env.GENIE_PREVIEW_NO_OPEN;
314
+ if (raw === undefined)
315
+ return false;
316
+ const v = raw.trim().toLowerCase();
317
+ return v !== "" && v !== "0" && v !== "false";
318
+ }
319
+ /**
320
+ * Decide whether {@link runPreview} should ask the booter to open a browser.
321
+ * Only an explicit negotiated negative authorizes auto-open. An omitted
322
+ * capability is ambiguous (Codex Desktop and tools-only Codex currently look
323
+ * identical to a custom MCP server), so it receives a viewer URL but never a
324
+ * surprise system-browser launch.
325
+ */
326
+ export function shouldAutoOpen(uiCapable, env = process.env, transportKind) {
327
+ if (uiCapable !== false)
328
+ return false;
329
+ if (transportKind !== "stdio")
330
+ return false;
331
+ return !autoOpenDisabledByEnv(env);
332
+ }
333
+ /**
334
+ * Core `preview` logic (exported for direct unit testing without the MCP
335
+ * transport). Compiles + persists the kit's manifest (piece A) so all three
336
+ * vehicles render real cards, resolves the kit dir, tries to boot/reuse the
337
+ * viewer — opening a browser server-side for non-`ui://` hosts (piece B) — and
338
+ * builds the text summary: the live URL when the viewer is up (AC5), else a
339
+ * `file://` fallback (AC6). `_meta.ui.resourceUri` is emitted either way
340
+ * (AC3/AC4), and one `preview.request` line is logged recording the client and
341
+ * its `ui://` support (AC7). Remote results never expose server-local file
342
+ * paths; a failed local viewer can deliver a CSP-safe manifest to an MCP App
343
+ * when a previews origin is configured.
344
+ */
345
+ export async function runPreview(deps, args, ctx) {
346
+ // Path-safety BEFORE anything touches the filesystem or boots a server.
347
+ const kitDir = resolveKitDir(deps.kitsRoot, args.kitId);
348
+ let kitStat;
349
+ try {
350
+ kitStat = await lstat(kitDir);
351
+ }
352
+ catch (error) {
353
+ const code = error.code;
354
+ if (code === "ENOENT" || code === "ENOTDIR") {
355
+ throw new KitNotFoundError(args.kitId);
356
+ }
357
+ throw error;
358
+ }
359
+ if (!kitStat.isDirectory()) {
360
+ throw new KitNotFoundError(args.kitId);
361
+ }
362
+ // Piece A — compile + persist `.genie/manifest.json` at request time so the
363
+ // Vite / file:// / ui:// vehicles all render real cards (and list_components
364
+ // sees them) without depending on a prior resource read. An existing empty
365
+ // kit compiles to an empty manifest; genuine read/write failures propagate.
366
+ const compile = deps.ensureManifest ?? ensureManifest;
367
+ const manifest = await compile(kitDir);
368
+ const filteredManifest = filterManifest(manifest, {
369
+ componentName: args.componentName,
370
+ group: args.group,
371
+ });
372
+ const env = deps.env ?? process.env;
373
+ const locality = ctx.locality ?? (ctx.transportKind === "stdio" ? "local" : "remote");
374
+ const previewsBaseUrl = deps.previewsBaseUrl ?? env.GENIE_PREVIEWS_BASE_URL;
375
+ const normalizedPreviewsBaseUrl = normalizePreviewsBaseUrl(previewsBaseUrl);
376
+ const canDeliverEmbeddedManifest = normalizedPreviewsBaseUrl !== undefined || filteredManifest.components.length === 0;
377
+ // Keep the handshake tri-state. Explicit support is inline-only, explicit
378
+ // rejection is viewer-only, and omission gets BOTH so metadata-linked hosts
379
+ // can mount the canvas while tools-only hosts still have a usable URL.
380
+ const prepareInline = ctx.uiCapable !== false;
381
+ const prepareViewerFallback = ctx.uiCapable !== true;
382
+ const getCardAssetBroker = deps.getCardAssetBroker;
383
+ const useLocalCardBroker = locality === "local" &&
384
+ ctx.transportKind === "stdio" &&
385
+ prepareInline &&
386
+ normalizedPreviewsBaseUrl === undefined &&
387
+ getCardAssetBroker !== undefined;
388
+ const useConfiguredPreviewsOrigin = locality === "local" && prepareInline && normalizedPreviewsBaseUrl !== undefined;
389
+ const readPreviewBytes = deps.readPreviewBytes ??
390
+ (async (root, path) => {
391
+ try {
392
+ return await readFile(join(root, path));
393
+ }
394
+ catch {
395
+ return null;
396
+ }
397
+ });
398
+ let embeddedManifest;
399
+ let embeddedError;
400
+ if (locality === "remote") {
401
+ if (!prepareInline) {
402
+ embeddedError =
403
+ "Client does not support MCP Apps; remote-locality previews have no server-side browser fallback.";
404
+ }
405
+ else if (canDeliverEmbeddedManifest) {
406
+ embeddedManifest = await rewriteCardPaths(filteredManifest, {
407
+ kitId: args.kitId,
408
+ kitDir,
409
+ previewsBaseUrl: normalizedPreviewsBaseUrl?.toString(),
410
+ readPreviewBytes,
411
+ });
412
+ }
413
+ else {
414
+ embeddedError =
415
+ previewsBaseUrl !== undefined && previewsBaseUrl !== ""
416
+ ? "Invalid GENIE_PREVIEWS_BASE_URL; remote embedded previews require an absolute http(s) origin."
417
+ : "Remote embedded previews require GENIE_PREVIEWS_BASE_URL; " +
418
+ "the query-bearing resource remains available to result-level hosts.";
419
+ }
420
+ }
421
+ const resourceUri = buildResourceUri({
422
+ kitId: args.kitId,
423
+ componentName: args.componentName,
424
+ group: args.group,
425
+ });
426
+ const autoOpen = locality === "local" && shouldAutoOpen(ctx.uiCapable, deps.env, ctx.transportKind);
427
+ // AC7 — one structured line per request; the sniffed client + its ui:// support.
428
+ logStderr({
429
+ event: "preview.request",
430
+ kitId: args.kitId,
431
+ client: ctx.clientName ?? null,
432
+ uiCapable: ctx.uiCapable ?? null,
433
+ uiCapability: ctx.uiCapable === true ? "supported" : ctx.uiCapable === false ? "unsupported" : "unknown",
434
+ inlineDelivery: prepareInline,
435
+ viewerFallback: prepareViewerFallback,
436
+ localCardBroker: useLocalCardBroker,
437
+ autoOpen,
438
+ transport: ctx.transportKind ?? null,
439
+ ...(args.group !== undefined ? { group: args.group } : {}),
440
+ ...(args.componentName !== undefined ? { componentName: args.componentName } : {}),
441
+ });
442
+ let text;
443
+ let viewerUrl;
444
+ let fileUrl;
445
+ if (locality === "remote") {
446
+ text =
447
+ embeddedManifest !== undefined
448
+ ? "Preview ready in the inline MCP App."
449
+ : `Remote preview unavailable: ${embeddedError ?? "no declared previews origin"}`;
450
+ }
451
+ else {
452
+ fileUrl = pathToFileURL(join(kitDir, "index.html")).href;
453
+ if (prepareInline) {
454
+ if (useConfiguredPreviewsOrigin) {
455
+ embeddedManifest = await rewriteCardPaths(filteredManifest, {
456
+ kitId: args.kitId,
457
+ kitDir,
458
+ previewsBaseUrl: normalizedPreviewsBaseUrl.toString(),
459
+ readPreviewBytes,
460
+ });
461
+ }
462
+ else if (useLocalCardBroker) {
463
+ // A local UI host already owns the visible MCP App canvas. Give it the
464
+ // filtered manifest with exact broker origins instead of nesting the
465
+ // dynamic Vite viewer (which the host's fixed CSP cannot authorize).
466
+ try {
467
+ const broker = await getCardAssetBroker();
468
+ const registeredKit = await broker.registerKit(args.kitId, kitDir);
469
+ embeddedManifest = rewriteCardPathsForBroker(filteredManifest, registeredKit);
470
+ }
471
+ catch (error) {
472
+ logStderr({
473
+ event: "preview.fallback",
474
+ kitId: args.kitId,
475
+ reason: "card-asset-broker-failed",
476
+ error: String(error),
477
+ });
478
+ embeddedError = "The local card asset broker could not start.";
479
+ }
480
+ }
481
+ else if (canDeliverEmbeddedManifest) {
482
+ // An empty result has no card bytes or child-script hashes to authorize,
483
+ // so it is safe to deliver through an already-mounted tool-result shell.
484
+ embeddedManifest = await rewriteCardPaths(filteredManifest, {
485
+ kitId: args.kitId,
486
+ kitDir,
487
+ previewsBaseUrl: normalizedPreviewsBaseUrl?.toString(),
488
+ readPreviewBytes,
489
+ });
490
+ }
491
+ else {
492
+ embeddedError =
493
+ "No CSP-safe inline card origin is available; configure GENIE_PREVIEWS_BASE_URL.";
494
+ }
495
+ }
496
+ if (prepareViewerFallback) {
497
+ try {
498
+ const viewer = await deps.registry.ensure(kitDir, DEFAULT_VIEWER_PORT, false); // AC5 + piece B
499
+ const url = new URL(viewer.url);
500
+ if (args.componentName !== undefined)
501
+ url.searchParams.set("componentName", args.componentName);
502
+ if (args.group !== undefined)
503
+ url.searchParams.set("group", args.group);
504
+ viewerUrl = url.toString();
505
+ if (autoOpen)
506
+ await deps.registry.open(kitDir, viewerUrl);
507
+ }
508
+ catch (error) {
509
+ // AC6 — the local viewer could not boot (Vite/@ambitresearch/genie-viewer absent,
510
+ // port unbindable, …). Preserve any independently prepared inline
511
+ // manifest, otherwise degrade to the file:// vehicle.
512
+ logStderr({
513
+ event: "preview.fallback",
514
+ kitId: args.kitId,
515
+ reason: "viewer-boot-failed",
516
+ error: String(error),
517
+ });
518
+ if (embeddedManifest === undefined && embeddedError === undefined) {
519
+ embeddedError =
520
+ "The local preview viewer could not start; use the returned file URL or start it manually.";
521
+ }
522
+ }
523
+ }
524
+ if (embeddedManifest !== undefined && viewerUrl !== undefined) {
525
+ text =
526
+ `Preview ready in the inline MCP App.\n` +
527
+ `Tools-only fallback running at ${viewerUrl}\n` +
528
+ `Or open the kit directly: ${fileUrl}`;
529
+ }
530
+ else if (embeddedManifest !== undefined) {
531
+ text = `Preview ready in the inline MCP App.\nOr open the kit directly: ${fileUrl}`;
532
+ }
533
+ else if (viewerUrl !== undefined) {
534
+ text =
535
+ (embeddedError === undefined ? "" : `Inline preview unavailable: ${embeddedError}\n`) +
536
+ `Preview running at ${viewerUrl}\n` +
537
+ `Or open the kit directly: ${fileUrl}`;
538
+ }
539
+ else {
540
+ text =
541
+ `Preview unavailable: ${embeddedError ?? "no supported delivery path"}\n` +
542
+ `Open the kit directly: ${fileUrl}`;
543
+ }
544
+ }
545
+ return {
546
+ content: [{ type: "text", text }],
547
+ structuredContent: {
548
+ kitId: args.kitId,
549
+ ...(args.componentName !== undefined ? { componentName: args.componentName } : {}),
550
+ ...(args.group !== undefined ? { group: args.group } : {}),
551
+ ...(viewerUrl !== undefined ? { viewerUrl } : {}),
552
+ ...(fileUrl !== undefined ? { fileUrl } : {}),
553
+ ...(ctx.transportKind !== undefined ? { transportKind: ctx.transportKind } : {}),
554
+ locality,
555
+ ...(embeddedError !== undefined ? { embeddedError } : {}),
556
+ },
557
+ // Same resourceUri under both the MCP-Apps key (`ui.resourceUri`) and the
558
+ // ChatGPT Apps SDK key (`openai/outputTemplate`, AC6) — cross-vendor link.
559
+ _meta: {
560
+ ui: { resourceUri },
561
+ "openai/outputTemplate": resourceUri,
562
+ ...(embeddedManifest !== undefined
563
+ ? { [PREVIEW_EMBEDDED_MANIFEST_META_KEY]: embeddedManifest }
564
+ : {}),
565
+ },
566
+ };
567
+ }
568
+ // ─── MCP registration ────────────────────────────────────────────────────────
569
+ const previewInputSchema = {
570
+ kitId: z
571
+ .string()
572
+ .regex(KIT_ID_PATTERN)
573
+ .describe("The kit to preview (a genie kitId — lowercase slug)."),
574
+ componentName: z
575
+ .string()
576
+ .min(1)
577
+ .optional()
578
+ .describe("Optional single-component focus; carried into the ui:// resource query."),
579
+ group: z
580
+ .string()
581
+ .min(1)
582
+ .optional()
583
+ .describe("Optional group filter; carried into the ui:// resource query. Empty string rejected."),
584
+ };
585
+ const previewOutputSchema = {
586
+ kitId: z.string(),
587
+ componentName: z.string().optional(),
588
+ group: z.string().optional(),
589
+ viewerUrl: z.string().optional(),
590
+ fileUrl: z.string().optional(),
591
+ transportKind: z.enum(["stdio", "http"]).optional(),
592
+ locality: z.enum(["local", "remote"]),
593
+ embeddedError: z.string().optional(),
594
+ };
595
+ /**
596
+ * Extract the requesting harness's client name from `params._meta.client.name`
597
+ * (AC7). MCP's `RequestMeta` is a loose object, so this custom key survives to
598
+ * the handler's `extra._meta`. Defensive about shape — any missing/oddly-typed
599
+ * level yields `undefined` (→ treated as a non-ui:// host).
600
+ */
601
+ function sniffClientName(meta) {
602
+ if (typeof meta !== "object" || meta === null)
603
+ return undefined;
604
+ const client = meta.client;
605
+ if (typeof client !== "object" || client === null)
606
+ return undefined;
607
+ const name = client.name;
608
+ return typeof name === "string" ? name : undefined;
609
+ }
610
+ /**
611
+ * Register `mcp__genie__preview`. One {@link ViewerRegistry} is created per
612
+ * registration and shared across every call so viewers are reused (AC5). The
613
+ * handler resolves the client's identity (per-request `_meta` escape hatch,
614
+ * else the initialize handshake) and its MCP Apps capability, then delegates
615
+ * to {@link runPreview}.
616
+ */
617
+ export function registerPreviewTool(server, options) {
618
+ const registry = new ViewerRegistry(options.booter ?? defaultViewerBooter);
619
+ const deps = {
620
+ kitsRoot: options.kitsRoot,
621
+ registry,
622
+ env: options.env,
623
+ ensureManifest: options.ensureManifest,
624
+ readPreviewBytes: options.readPreviewBytes,
625
+ previewsBaseUrl: options.previewsBaseUrl,
626
+ getCardAssetBroker: options.getCardAssetBroker,
627
+ };
628
+ server.registerTool(PREVIEW_TOOL_NAME, {
629
+ title: "Preview kit",
630
+ description: "Preview a UI kit as a live card grid — call this to SHOW the user a component " +
631
+ "(typically right after write_files persists one). Compiles + persists the kit " +
632
+ "manifest so the grid always reflects what's on disk, then returns the viewer URL " +
633
+ "(booting the Vite viewer on demand and reusing it across calls) plus a local-only " +
634
+ "file:// fallback. MCP Apps-capable hosts get the inline grid via _meta.ui.resourceUri. " +
635
+ "Local stdio clients that explicitly negotiate no UI support get a server-opened browser " +
636
+ "tab (disable with GENIE_PREVIEW_NO_OPEN=1); HTTP deployments never auto-open on " +
637
+ "the server machine. Optionally focus one component or group.",
638
+ inputSchema: previewInputSchema,
639
+ outputSchema: previewOutputSchema,
640
+ // Standards-compliant discovery happens in tools/list through this static
641
+ // app shell. The host sends tool input/result to the shell, which renders
642
+ // the invocation's viewer URL. Keep the query-bearing result URI below for
643
+ // legacy/result-level hosts and the self-contained embedded resource path.
644
+ _meta: {
645
+ ui: { resourceUri: GRID_RESOURCE_URI },
646
+ "openai/outputTemplate": GRID_RESOURCE_URI,
647
+ },
648
+ }, async (args, extra) => {
649
+ // Client identity: prefer the per-request `_meta.client.name` escape
650
+ // hatch (tests, exotic proxies), else the `initialize` handshake's
651
+ // clientInfo — what real hosts actually send (the per-request key was
652
+ // the original AC7 sniff's blind spot: no production host populates it).
653
+ const clientName = sniffClientName(extra._meta) ?? server.server.getClientVersion()?.name;
654
+ // Authoritative MCP Apps capability from the same handshake.
655
+ const uiCapable = getUiExtensionCapability(server.server.getClientCapabilities());
656
+ const transportKind = options.transportKind ?? getServerTransportKind(server);
657
+ const locality = options.locality ?? (transportKind === "stdio" ? "local" : "remote");
658
+ const result = await runPreview(deps, args, {
659
+ clientName,
660
+ uiCapable,
661
+ transportKind,
662
+ locality,
663
+ });
664
+ return {
665
+ content: result.content,
666
+ structuredContent: result.structuredContent,
667
+ _meta: result._meta,
668
+ };
669
+ });
670
+ return registry;
671
+ }
672
+ //# sourceMappingURL=preview.js.map