@miragon/mcp-toolkit-ui 0.10.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 (320) hide show
  1. package/README.md +36 -0
  2. package/dist/app/adapt-data-widget.d.ts +33 -0
  3. package/dist/app/adapt-data-widget.d.ts.map +1 -0
  4. package/dist/app/adapt-data-widget.js +35 -0
  5. package/dist/app/adapt-data-widget.js.map +1 -0
  6. package/dist/app/builder/CatalogueSheet.d.ts +13 -0
  7. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -0
  8. package/dist/app/builder/CatalogueSheet.js +33 -0
  9. package/dist/app/builder/CatalogueSheet.js.map +1 -0
  10. package/dist/app/builder/PipelineStrip.d.ts +22 -0
  11. package/dist/app/builder/PipelineStrip.d.ts.map +1 -0
  12. package/dist/app/builder/PipelineStrip.js +31 -0
  13. package/dist/app/builder/PipelineStrip.js.map +1 -0
  14. package/dist/app/builder/PreviewPane.d.ts +13 -0
  15. package/dist/app/builder/PreviewPane.d.ts.map +1 -0
  16. package/dist/app/builder/PreviewPane.js +21 -0
  17. package/dist/app/builder/PreviewPane.js.map +1 -0
  18. package/dist/app/builder/SaveDialog.d.ts +14 -0
  19. package/dist/app/builder/SaveDialog.d.ts.map +1 -0
  20. package/dist/app/builder/SaveDialog.js +14 -0
  21. package/dist/app/builder/SaveDialog.js.map +1 -0
  22. package/dist/app/builder/Toolbar.d.ts +17 -0
  23. package/dist/app/builder/Toolbar.d.ts.map +1 -0
  24. package/dist/app/builder/Toolbar.js +15 -0
  25. package/dist/app/builder/Toolbar.js.map +1 -0
  26. package/dist/app/builder/Workspace.d.ts +28 -0
  27. package/dist/app/builder/Workspace.d.ts.map +1 -0
  28. package/dist/app/builder/Workspace.js +117 -0
  29. package/dist/app/builder/Workspace.js.map +1 -0
  30. package/dist/app/builder/builder-model.d.ts +86 -0
  31. package/dist/app/builder/builder-model.d.ts.map +1 -0
  32. package/dist/app/builder/builder-model.js +174 -0
  33. package/dist/app/builder/builder-model.js.map +1 -0
  34. package/dist/app/builder/builder-reducer.d.ts +71 -0
  35. package/dist/app/builder/builder-reducer.d.ts.map +1 -0
  36. package/dist/app/builder/builder-reducer.js +201 -0
  37. package/dist/app/builder/builder-reducer.js.map +1 -0
  38. package/dist/app/builder/labels.d.ts +78 -0
  39. package/dist/app/builder/labels.d.ts.map +1 -0
  40. package/dist/app/builder/labels.js +71 -0
  41. package/dist/app/builder/labels.js.map +1 -0
  42. package/dist/app/builder/status-chip.d.ts +10 -0
  43. package/dist/app/builder/status-chip.d.ts.map +1 -0
  44. package/dist/app/builder/status-chip.js +16 -0
  45. package/dist/app/builder/status-chip.js.map +1 -0
  46. package/dist/app/host-bridge.d.ts +211 -0
  47. package/dist/app/host-bridge.d.ts.map +1 -0
  48. package/dist/app/host-bridge.js +247 -0
  49. package/dist/app/host-bridge.js.map +1 -0
  50. package/dist/app/index.d.ts +16 -0
  51. package/dist/app/index.d.ts.map +1 -0
  52. package/dist/app/index.js +18 -0
  53. package/dist/app/index.js.map +1 -0
  54. package/dist/app/layout-builder.d.ts +48 -0
  55. package/dist/app/layout-builder.d.ts.map +1 -0
  56. package/dist/app/layout-builder.js +418 -0
  57. package/dist/app/layout-builder.js.map +1 -0
  58. package/dist/app/mcp-app-view.d.ts +43 -0
  59. package/dist/app/mcp-app-view.d.ts.map +1 -0
  60. package/dist/app/mcp-app-view.js +169 -0
  61. package/dist/app/mcp-app-view.js.map +1 -0
  62. package/dist/app/mcp-toolkit-app.d.ts +18 -0
  63. package/dist/app/mcp-toolkit-app.d.ts.map +1 -0
  64. package/dist/app/mcp-toolkit-app.js +22 -0
  65. package/dist/app/mcp-toolkit-app.js.map +1 -0
  66. package/dist/app/use-host-actions.d.ts +57 -0
  67. package/dist/app/use-host-actions.d.ts.map +1 -0
  68. package/dist/app/use-host-actions.js +42 -0
  69. package/dist/app/use-host-actions.js.map +1 -0
  70. package/dist/app/widget-fixture.d.ts +180 -0
  71. package/dist/app/widget-fixture.d.ts.map +1 -0
  72. package/dist/app/widget-fixture.js +273 -0
  73. package/dist/app/widget-fixture.js.map +1 -0
  74. package/dist/app/widget-props-sheet.d.ts +20 -0
  75. package/dist/app/widget-props-sheet.d.ts.map +1 -0
  76. package/dist/app/widget-props-sheet.js +87 -0
  77. package/dist/app/widget-props-sheet.js.map +1 -0
  78. package/dist/app/widget-renderer.d.ts +87 -0
  79. package/dist/app/widget-renderer.d.ts.map +1 -0
  80. package/dist/app/widget-renderer.js +105 -0
  81. package/dist/app/widget-renderer.js.map +1 -0
  82. package/dist/components/DrillButton.d.ts +22 -0
  83. package/dist/components/DrillButton.d.ts.map +1 -0
  84. package/dist/components/DrillButton.js +13 -0
  85. package/dist/components/DrillButton.js.map +1 -0
  86. package/dist/components/FilterBar.d.ts +28 -0
  87. package/dist/components/FilterBar.d.ts.map +1 -0
  88. package/dist/components/FilterBar.js +12 -0
  89. package/dist/components/FilterBar.js.map +1 -0
  90. package/dist/components/GridLayout.d.ts +26 -0
  91. package/dist/components/GridLayout.d.ts.map +1 -0
  92. package/dist/components/GridLayout.js +31 -0
  93. package/dist/components/GridLayout.js.map +1 -0
  94. package/dist/components/GroupCard.d.ts +21 -0
  95. package/dist/components/GroupCard.d.ts.map +1 -0
  96. package/dist/components/GroupCard.js +24 -0
  97. package/dist/components/GroupCard.js.map +1 -0
  98. package/dist/components/KpiGrid.d.ts +43 -0
  99. package/dist/components/KpiGrid.d.ts.map +1 -0
  100. package/dist/components/KpiGrid.js +35 -0
  101. package/dist/components/KpiGrid.js.map +1 -0
  102. package/dist/components/ListFooter.d.ts +22 -0
  103. package/dist/components/ListFooter.d.ts.map +1 -0
  104. package/dist/components/ListFooter.js +13 -0
  105. package/dist/components/ListFooter.js.map +1 -0
  106. package/dist/components/Pills.d.ts +25 -0
  107. package/dist/components/Pills.d.ts.map +1 -0
  108. package/dist/components/Pills.js +17 -0
  109. package/dist/components/Pills.js.map +1 -0
  110. package/dist/components/SectionHeading.d.ts +16 -0
  111. package/dist/components/SectionHeading.d.ts.map +1 -0
  112. package/dist/components/SectionHeading.js +10 -0
  113. package/dist/components/SectionHeading.js.map +1 -0
  114. package/dist/components/WidgetHeader.d.ts +21 -0
  115. package/dist/components/WidgetHeader.d.ts.map +1 -0
  116. package/dist/components/WidgetHeader.js +11 -0
  117. package/dist/components/WidgetHeader.js.map +1 -0
  118. package/dist/hooks/index.d.ts +11 -0
  119. package/dist/hooks/index.d.ts.map +1 -0
  120. package/dist/hooks/index.js +7 -0
  121. package/dist/hooks/index.js.map +1 -0
  122. package/dist/hooks/use-debounced-value.d.ts +7 -0
  123. package/dist/hooks/use-debounced-value.d.ts.map +1 -0
  124. package/dist/hooks/use-debounced-value.js +15 -0
  125. package/dist/hooks/use-debounced-value.js.map +1 -0
  126. package/dist/hooks/use-mobile.d.ts +6 -0
  127. package/dist/hooks/use-mobile.d.ts.map +1 -0
  128. package/dist/hooks/use-mobile.js +19 -0
  129. package/dist/hooks/use-mobile.js.map +1 -0
  130. package/dist/hooks/use-tool-query.d.ts +21 -0
  131. package/dist/hooks/use-tool-query.d.ts.map +1 -0
  132. package/dist/hooks/use-tool-query.js +47 -0
  133. package/dist/hooks/use-tool-query.js.map +1 -0
  134. package/dist/hooks/use-tool-result-recovery.d.ts +54 -0
  135. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -0
  136. package/dist/hooks/use-tool-result-recovery.js +93 -0
  137. package/dist/hooks/use-tool-result-recovery.js.map +1 -0
  138. package/dist/hooks/use-view-data.d.ts +19 -0
  139. package/dist/hooks/use-view-data.d.ts.map +1 -0
  140. package/dist/hooks/use-view-data.js +18 -0
  141. package/dist/hooks/use-view-data.js.map +1 -0
  142. package/dist/hooks/use-view-tool-query.d.ts +18 -0
  143. package/dist/hooks/use-view-tool-query.d.ts.map +1 -0
  144. package/dist/hooks/use-view-tool-query.js +29 -0
  145. package/dist/hooks/use-view-tool-query.js.map +1 -0
  146. package/dist/index.d.ts +47 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +56 -0
  149. package/dist/index.js.map +1 -0
  150. package/dist/lib/parse-tool-result.d.ts +39 -0
  151. package/dist/lib/parse-tool-result.d.ts.map +1 -0
  152. package/dist/lib/parse-tool-result.js +66 -0
  153. package/dist/lib/parse-tool-result.js.map +1 -0
  154. package/dist/lib/tone-utils.d.ts +18 -0
  155. package/dist/lib/tone-utils.d.ts.map +1 -0
  156. package/dist/lib/tone-utils.js +25 -0
  157. package/dist/lib/tone-utils.js.map +1 -0
  158. package/dist/lib/utils.d.ts +3 -0
  159. package/dist/lib/utils.d.ts.map +1 -0
  160. package/dist/lib/utils.js +6 -0
  161. package/dist/lib/utils.js.map +1 -0
  162. package/dist/locale/index.d.ts +3 -0
  163. package/dist/locale/index.d.ts.map +1 -0
  164. package/dist/locale/index.js +7 -0
  165. package/dist/locale/index.js.map +1 -0
  166. package/dist/locale/locale-provider.d.ts +50 -0
  167. package/dist/locale/locale-provider.d.ts.map +1 -0
  168. package/dist/locale/locale-provider.js +47 -0
  169. package/dist/locale/locale-provider.js.map +1 -0
  170. package/dist/primitives/alert.d.ts +10 -0
  171. package/dist/primitives/alert.d.ts.map +1 -0
  172. package/dist/primitives/alert.js +25 -0
  173. package/dist/primitives/alert.js.map +1 -0
  174. package/dist/primitives/badge.d.ts +8 -0
  175. package/dist/primitives/badge.d.ts.map +1 -0
  176. package/dist/primitives/badge.js +34 -0
  177. package/dist/primitives/badge.js.map +1 -0
  178. package/dist/primitives/button.d.ts +11 -0
  179. package/dist/primitives/button.d.ts.map +1 -0
  180. package/dist/primitives/button.js +36 -0
  181. package/dist/primitives/button.js.map +1 -0
  182. package/dist/primitives/card.d.ts +12 -0
  183. package/dist/primitives/card.d.ts.map +1 -0
  184. package/dist/primitives/card.js +25 -0
  185. package/dist/primitives/card.js.map +1 -0
  186. package/dist/primitives/dialog.d.ts +18 -0
  187. package/dist/primitives/dialog.d.ts.map +1 -0
  188. package/dist/primitives/dialog.js +38 -0
  189. package/dist/primitives/dialog.js.map +1 -0
  190. package/dist/primitives/input.d.ts +4 -0
  191. package/dist/primitives/input.d.ts.map +1 -0
  192. package/dist/primitives/input.js +7 -0
  193. package/dist/primitives/input.js.map +1 -0
  194. package/dist/primitives/scroll-area.d.ts +6 -0
  195. package/dist/primitives/scroll-area.d.ts.map +1 -0
  196. package/dist/primitives/scroll-area.js +11 -0
  197. package/dist/primitives/scroll-area.js.map +1 -0
  198. package/dist/primitives/select.d.ts +16 -0
  199. package/dist/primitives/select.d.ts.map +1 -0
  200. package/dist/primitives/select.js +38 -0
  201. package/dist/primitives/select.js.map +1 -0
  202. package/dist/primitives/separator.d.ts +4 -0
  203. package/dist/primitives/separator.d.ts.map +1 -0
  204. package/dist/primitives/separator.js +9 -0
  205. package/dist/primitives/separator.js.map +1 -0
  206. package/dist/primitives/sheet.d.ts +15 -0
  207. package/dist/primitives/sheet.d.ts.map +1 -0
  208. package/dist/primitives/sheet.js +41 -0
  209. package/dist/primitives/sheet.js.map +1 -0
  210. package/dist/primitives/skeleton.d.ts +3 -0
  211. package/dist/primitives/skeleton.d.ts.map +1 -0
  212. package/dist/primitives/skeleton.js +7 -0
  213. package/dist/primitives/skeleton.js.map +1 -0
  214. package/dist/primitives/switch.d.ts +5 -0
  215. package/dist/primitives/switch.d.ts.map +1 -0
  216. package/dist/primitives/switch.js +8 -0
  217. package/dist/primitives/switch.js.map +1 -0
  218. package/dist/primitives/table.d.ts +11 -0
  219. package/dist/primitives/table.d.ts.map +1 -0
  220. package/dist/primitives/table.js +28 -0
  221. package/dist/primitives/table.js.map +1 -0
  222. package/dist/primitives/tabs.d.ts +12 -0
  223. package/dist/primitives/tabs.d.ts.map +1 -0
  224. package/dist/primitives/tabs.js +29 -0
  225. package/dist/primitives/tabs.js.map +1 -0
  226. package/dist/providers/query-provider.d.ts +11 -0
  227. package/dist/providers/query-provider.d.ts.map +1 -0
  228. package/dist/providers/query-provider.js +20 -0
  229. package/dist/providers/query-provider.js.map +1 -0
  230. package/dist/theme/create-theme.d.ts +130 -0
  231. package/dist/theme/create-theme.d.ts.map +1 -0
  232. package/dist/theme/create-theme.js +65 -0
  233. package/dist/theme/create-theme.js.map +1 -0
  234. package/dist/theme/index.d.ts +7 -0
  235. package/dist/theme/index.d.ts.map +1 -0
  236. package/dist/theme/index.js +7 -0
  237. package/dist/theme/index.js.map +1 -0
  238. package/dist/theme/presets.d.ts +25 -0
  239. package/dist/theme/presets.d.ts.map +1 -0
  240. package/dist/theme/presets.js +68 -0
  241. package/dist/theme/presets.js.map +1 -0
  242. package/dist/theme/theme-provider.d.ts +69 -0
  243. package/dist/theme/theme-provider.d.ts.map +1 -0
  244. package/dist/theme/theme-provider.js +79 -0
  245. package/dist/theme/theme-provider.js.map +1 -0
  246. package/package.json +71 -0
  247. package/src/app/adapt-data-widget.tsx +51 -0
  248. package/src/app/builder/CatalogueSheet.tsx +230 -0
  249. package/src/app/builder/PipelineStrip.tsx +260 -0
  250. package/src/app/builder/PreviewPane.tsx +72 -0
  251. package/src/app/builder/SaveDialog.tsx +95 -0
  252. package/src/app/builder/Toolbar.tsx +95 -0
  253. package/src/app/builder/Workspace.tsx +483 -0
  254. package/src/app/builder/builder-model.test.ts +351 -0
  255. package/src/app/builder/builder-model.ts +213 -0
  256. package/src/app/builder/builder-reducer.test.ts +441 -0
  257. package/src/app/builder/builder-reducer.ts +289 -0
  258. package/src/app/builder/labels.ts +139 -0
  259. package/src/app/builder/status-chip.tsx +37 -0
  260. package/src/app/host-bridge.test.ts +206 -0
  261. package/src/app/host-bridge.tsx +371 -0
  262. package/src/app/index.ts +51 -0
  263. package/src/app/layout-builder.tsx +872 -0
  264. package/src/app/mcp-app-view.tsx +367 -0
  265. package/src/app/mcp-toolkit-app.tsx +25 -0
  266. package/src/app/use-host-actions.test.ts +16 -0
  267. package/src/app/use-host-actions.tsx +88 -0
  268. package/src/app/widget-fixture.test.ts +108 -0
  269. package/src/app/widget-fixture.tsx +441 -0
  270. package/src/app/widget-props-sheet.tsx +285 -0
  271. package/src/app/widget-renderer.test.ts +42 -0
  272. package/src/app/widget-renderer.tsx +200 -0
  273. package/src/components/DrillButton.tsx +37 -0
  274. package/src/components/FilterBar.tsx +68 -0
  275. package/src/components/GridLayout.tsx +49 -0
  276. package/src/components/GroupCard.tsx +52 -0
  277. package/src/components/KpiGrid.tsx +134 -0
  278. package/src/components/ListFooter.tsx +48 -0
  279. package/src/components/Pills.tsx +45 -0
  280. package/src/components/SectionHeading.tsx +26 -0
  281. package/src/components/WidgetHeader.tsx +51 -0
  282. package/src/globals.css +168 -0
  283. package/src/hooks/index.ts +17 -0
  284. package/src/hooks/use-debounced-value.ts +15 -0
  285. package/src/hooks/use-mobile.ts +24 -0
  286. package/src/hooks/use-tool-query.ts +63 -0
  287. package/src/hooks/use-tool-result-recovery.test.ts +65 -0
  288. package/src/hooks/use-tool-result-recovery.ts +144 -0
  289. package/src/hooks/use-view-data.ts +31 -0
  290. package/src/hooks/use-view-tool-query.ts +38 -0
  291. package/src/index.ts +134 -0
  292. package/src/lib/parse-tool-result.test.ts +139 -0
  293. package/src/lib/parse-tool-result.ts +89 -0
  294. package/src/lib/tone-utils.test.ts +35 -0
  295. package/src/lib/tone-utils.ts +38 -0
  296. package/src/lib/utils.ts +6 -0
  297. package/src/locale/index.ts +7 -0
  298. package/src/locale/locale-provider.tsx +81 -0
  299. package/src/primitives/alert.tsx +60 -0
  300. package/src/primitives/badge.tsx +49 -0
  301. package/src/primitives/button.tsx +62 -0
  302. package/src/primitives/card.tsx +92 -0
  303. package/src/primitives/dialog.tsx +144 -0
  304. package/src/primitives/input.tsx +21 -0
  305. package/src/primitives/scroll-area.tsx +54 -0
  306. package/src/primitives/select.tsx +173 -0
  307. package/src/primitives/separator.tsx +21 -0
  308. package/src/primitives/sheet.tsx +134 -0
  309. package/src/primitives/skeleton.tsx +13 -0
  310. package/src/primitives/switch.tsx +24 -0
  311. package/src/primitives/table.tsx +87 -0
  312. package/src/primitives/tabs.tsx +79 -0
  313. package/src/providers/query-provider.tsx +38 -0
  314. package/src/theme/create-theme.test.ts +108 -0
  315. package/src/theme/create-theme.ts +158 -0
  316. package/src/theme/index.ts +9 -0
  317. package/src/theme/presets.ts +82 -0
  318. package/src/theme/theme-provider.tsx +152 -0
  319. package/src/ui-catalog.test.ts +112 -0
  320. package/ui-catalog.json +1280 -0
@@ -0,0 +1,441 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useSyncExternalStore,
8
+ type ComponentType,
9
+ type ReactNode,
10
+ } from "react"
11
+ import { useWidget } from "mcp-use/react"
12
+ import type { PipelineContext, StepResult, WidgetProps } from "@miragon/mcp-toolkit-core"
13
+ import { AppQueryProvider } from "../providers/query-provider.js"
14
+ import { HostBridgeProvider, createStandaloneHostBridge } from "./host-bridge.js"
15
+
16
+ /**
17
+ * Key `mcp-use` writes the serialized model-context string under when it flushes
18
+ * to `window.openai.setWidgetState`. Mirrors `MODEL_CONTEXT_KEY` in
19
+ * `mcp-use/react` (not exported, so duplicated here). The harness intercepts
20
+ * `setWidgetState` and reads this field to surface what the model would see.
21
+ */
22
+ const MODEL_CONTEXT_KEY = "__model_context"
23
+
24
+ /**
25
+ * Minimal shape of a tool-call response the harness hands back to widgets.
26
+ * Mirrors `mcp-use`'s `CallToolResponse` closely enough that both the toolkit's
27
+ * `parseToolResult` (structured-first) and `mcp-use`'s `useCallTool`
28
+ * (`result`/`content` text) can decode a fixture result without a real host.
29
+ *
30
+ * A fixture entry may also be a bare value: the registry wraps it into this
31
+ * envelope under `structuredContent`, which is what `parseToolResult` reads
32
+ * first — so a fixture can be written as just the data the widget expects.
33
+ */
34
+ export interface FixtureToolResult {
35
+ structuredContent?: unknown
36
+ content?: { type: string; text?: string }[]
37
+ isError?: boolean
38
+ _meta?: Record<string, unknown>
39
+ }
40
+
41
+ /**
42
+ * A handler fixture: invoked with the call args, may return a value or a
43
+ * promise (the registry `await`s it either way).
44
+ */
45
+ export type FixtureToolHandler = (args: Record<string, unknown>) => unknown
46
+
47
+ /**
48
+ * A fixture for one tool: either a static result returned verbatim, or a
49
+ * {@link FixtureToolHandler} invoked with the call args. Handlers let a fixture
50
+ * react to the widget's input — e.g. echo back a filtered list. Any registered
51
+ * function is treated as a handler (so a fixture cannot itself *be* the data
52
+ * `Function`, which is never JSON tool output anyway).
53
+ */
54
+ export type FixtureToolEntry = unknown
55
+
56
+ /**
57
+ * Pure, host-free registry of fake tool implementations. Drives every
58
+ * `callTool` path the harness exposes (`window.openai.callTool`, the toolkit's
59
+ * `AppQueryProvider` bridge). Kept free of React/DOM so its semantics are unit
60
+ * testable in a plain Node environment.
61
+ *
62
+ * Resolution rules:
63
+ * - A registered **function** entry is called with `args` and its return value
64
+ * (awaited) is wrapped into a {@link FixtureToolResult}.
65
+ * - A registered **value** entry is returned as-is when it already looks like a
66
+ * tool response (has `structuredContent`/`content`), otherwise wrapped under
67
+ * `structuredContent` so widgets that read structured data Just Work.
68
+ * - An **unknown** tool rejects with a descriptive error. This is the
69
+ * error-friendly path: the rejection flows into the widget's own error state
70
+ * (TanStack Query `error`, `mcp-use` `isError`) instead of crashing the
71
+ * harness, and the message names the missing tool so the gap is obvious.
72
+ */
73
+ export class FixtureCallToolRegistry {
74
+ private readonly entries = new Map<string, FixtureToolEntry>()
75
+
76
+ constructor(initial?: Record<string, FixtureToolEntry>) {
77
+ if (initial) {
78
+ for (const [name, entry] of Object.entries(initial)) this.register(name, entry)
79
+ }
80
+ }
81
+
82
+ /** Register (or overwrite) the fixture for a tool name. */
83
+ register(name: string, resultOrHandler: FixtureToolEntry): this {
84
+ this.entries.set(name, resultOrHandler)
85
+ return this
86
+ }
87
+
88
+ /** True if a fixture is registered for `name`. */
89
+ has(name: string): boolean {
90
+ return this.entries.has(name)
91
+ }
92
+
93
+ /** Registered tool names, for diagnostics / the harness log. */
94
+ names(): string[] {
95
+ return [...this.entries.keys()]
96
+ }
97
+
98
+ /**
99
+ * Resolve a tool call against the registered fixtures. Always returns a
100
+ * promise so callers can `await` uniformly regardless of whether the fixture
101
+ * was a static value or an async handler.
102
+ */
103
+ async call(name: string, args: Record<string, unknown> = {}): Promise<FixtureToolResult> {
104
+ if (!this.entries.has(name)) {
105
+ throw new Error(
106
+ `[widget-fixture] No fixture registered for tool "${name}". ` +
107
+ `Registered: ${this.names().length ? this.names().join(", ") : "(none)"}.`,
108
+ )
109
+ }
110
+ const entry = this.entries.get(name)
111
+ const raw = typeof entry === "function" ? await (entry as FixtureToolHandler)(args) : entry
112
+ return toToolResult(raw)
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Wrap an arbitrary fixture value into a {@link FixtureToolResult}. Values that
118
+ * already carry `structuredContent` or `content` are passed through untouched
119
+ * so a fixture can opt into the full envelope; everything else is placed under
120
+ * `structuredContent` (the field `parseToolResult` reads first) and mirrored
121
+ * into a `content` text block so `mcp-use`'s text-oriented decoder also sees it.
122
+ */
123
+ function toToolResult(raw: unknown): FixtureToolResult {
124
+ if (raw != null && typeof raw === "object" && ("structuredContent" in raw || "content" in raw)) {
125
+ return raw as FixtureToolResult
126
+ }
127
+ return {
128
+ structuredContent: raw,
129
+ content: [{ type: "text", text: typeof raw === "string" ? raw : JSON.stringify(raw) }],
130
+ }
131
+ }
132
+
133
+ /**
134
+ * A host action a widget asked the host to perform, surfaced to the harness so
135
+ * it can render an activity log. Mirrors the `mcp-use` bridge verbs that
136
+ * {@link useHostActions} and `useWidget` route through `window.openai`.
137
+ */
138
+ export type HostActionLog =
139
+ | { type: "callTool"; name: string; args: Record<string, unknown> }
140
+ | { type: "sendFollowUpMessage"; prompt: string }
141
+ | { type: "openExternal"; href: string }
142
+ | { type: "requestDisplayMode"; mode: string }
143
+ | { type: "setWidgetState"; state: Record<string, unknown> }
144
+
145
+ interface FixtureHostValue {
146
+ registry: FixtureCallToolRegistry
147
+ }
148
+
149
+ const FixtureHostContext = createContext<FixtureHostValue | null>(null)
150
+
151
+ /**
152
+ * Access the active fixture registry from inside a {@link WidgetFixtureHost}.
153
+ * Returns `null` outside a harness — callers should treat that as "no fixture
154
+ * host present" rather than an error.
155
+ */
156
+ export function useFixtureHost(): FixtureHostValue | null {
157
+ return useContext(FixtureHostContext)
158
+ }
159
+
160
+ /**
161
+ * Default host-context globals a widget sees in the harness. Chosen to be
162
+ * harmless and inert: inline display, light theme, desktop, no safe-area
163
+ * insets. Overridable per-mount via {@link WidgetFixtureHostProps.globals}.
164
+ */
165
+ const DEFAULT_GLOBALS = {
166
+ theme: "light",
167
+ displayMode: "inline",
168
+ locale: "en-US",
169
+ maxHeight: 4096,
170
+ safeArea: { insets: { top: 0, right: 0, bottom: 0, left: 0 } },
171
+ userAgent: {
172
+ device: { type: "desktop" },
173
+ capabilities: { hover: true, touch: false },
174
+ },
175
+ } as const
176
+
177
+ /**
178
+ * Build the `window.openai` shim the `mcp-use` React hooks read from. Routes
179
+ * `callTool` through the fixture registry and reports every other bridge verb
180
+ * to `onHostAction`. Returns `null` outside a browser-like environment (SSR)
181
+ * so the harness degrades to a plain render rather than throwing.
182
+ */
183
+ function buildOpenAiShim(
184
+ registry: FixtureCallToolRegistry,
185
+ toolOutput: Record<string, unknown> | null,
186
+ toolInput: Record<string, unknown>,
187
+ globals: Record<string, unknown>,
188
+ onHostAction?: (action: HostActionLog) => void,
189
+ onModelContext?: (text: string) => void,
190
+ ): Record<string, unknown> | null {
191
+ if (typeof window === "undefined") return null
192
+ return {
193
+ ...DEFAULT_GLOBALS,
194
+ ...globals,
195
+ toolInput,
196
+ toolOutput,
197
+ toolResponseMetadata: null,
198
+ widgetState: null,
199
+ callTool: async (name: string, args: Record<string, unknown>) => {
200
+ onHostAction?.({ type: "callTool", name, args })
201
+ return registry.call(name, args)
202
+ },
203
+ sendFollowUpMessage: (args: { prompt: string }) => {
204
+ onHostAction?.({ type: "sendFollowUpMessage", prompt: args.prompt })
205
+ return Promise.resolve()
206
+ },
207
+ openExternal: (payload: { href: string }) => {
208
+ onHostAction?.({ type: "openExternal", href: payload.href })
209
+ },
210
+ requestDisplayMode: (args: { mode: string }) => {
211
+ onHostAction?.({ type: "requestDisplayMode", mode: args.mode })
212
+ return Promise.resolve({ mode: args.mode })
213
+ },
214
+ setWidgetState: (state: Record<string, unknown>) => {
215
+ onHostAction?.({ type: "setWidgetState", state })
216
+ // `mcp-use` flushes the serialized `<ModelContext>` tree through
217
+ // setWidgetState under MODEL_CONTEXT_KEY — surface it as the model view.
218
+ const ctx = state?.[MODEL_CONTEXT_KEY]
219
+ if (typeof ctx === "string") onModelContext?.(ctx)
220
+ return Promise.resolve()
221
+ },
222
+ notifyIntrinsicHeight: () => Promise.resolve(),
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Renders nothing but mounts a `useWidget()` so `mcp-use` registers its
228
+ * model-context flush handler — even for widgets that never call `useWidget`
229
+ * themselves (e.g. a simple `({ keys })` card). Without this, a widget's
230
+ * `<ModelContext>` would render but never flush, and `onModelContext` would
231
+ * stay silent. The flush routes through our `setWidgetState` shim above.
232
+ */
233
+ function ModelContextPump(): null {
234
+ useWidget()
235
+ return null
236
+ }
237
+
238
+ /**
239
+ * Either an `adaptDataWidget`-style component (receives {@link WidgetProps}) or
240
+ * a "raw" single-data component (`({ data }) | ({ keys })`). The harness builds
241
+ * a `WidgetProps` envelope from the fixture and spreads it so both shapes work
242
+ * without the author declaring which they wrote.
243
+ */
244
+ export type FixtureWidget = ComponentType<WidgetProps> | ComponentType<Record<string, unknown>>
245
+
246
+ export interface WidgetFixtureHostProps {
247
+ /** The widget component under development. */
248
+ widget: FixtureWidget
249
+ /**
250
+ * Fixture data for the widget. Shaped however the widget reads it:
251
+ * - widgets reading `keys["ns:thing"]` (the common pattern) → pass the keys map
252
+ * - `adaptDataWidget` components → also pass {@link WidgetFixtureHostProps.dataType}
253
+ * so the harness seeds a matching pipeline step the adapter can find
254
+ * - raw `({ data })` widgets → the same object is forwarded as `data`
255
+ */
256
+ data?: Record<string, unknown>
257
+ /**
258
+ * `dataType` to register the fixture step under, for `adaptDataWidget`
259
+ * components (which look up the step whose `_dataType` matches). When set,
260
+ * `data` is placed in `context.steps[<id>].data` so the adapter resolves it.
261
+ */
262
+ dataType?: string
263
+ /**
264
+ * Tool fixtures keyed by tool name. Each value is a static result or a
265
+ * handler — see {@link FixtureToolEntry}. Drives both `mcp-use`'s
266
+ * `useCallTool` and the toolkit's `useToolQuery`.
267
+ */
268
+ tools?: Record<string, FixtureToolEntry>
269
+ /**
270
+ * Host context globals to override (theme, displayMode, locale, …). Merged
271
+ * over {@link DEFAULT_GLOBALS}.
272
+ */
273
+ globals?: Record<string, unknown>
274
+ /**
275
+ * Called with the serialized model-context string the widget reports via
276
+ * `<ModelContext>` / `modelContext.set`. This is exactly what the model would
277
+ * see in a real host — surfacing it in the harness is the didactic point.
278
+ */
279
+ onModelContext?: (text: string) => void
280
+ /** Called for every host action the widget triggers (tool call, link, …). */
281
+ onHostAction?: (action: HostActionLog) => void
282
+ }
283
+
284
+ /**
285
+ * Build the `WidgetProps` envelope the harness passes to the widget. Seeds a
286
+ * single pipeline step from `data` (keyed by `dataType` when given) and uses
287
+ * `data` as the `keys` map, so widgets that read `keys[...]` and adapted
288
+ * widgets that scan `context.steps` are both satisfied from one fixture.
289
+ *
290
+ * Exported for unit testing the envelope shape without rendering React.
291
+ */
292
+ export function buildFixtureWidgetProps(
293
+ data: Record<string, unknown>,
294
+ dataType?: string,
295
+ ): WidgetProps {
296
+ const steps: Record<string, StepResult> = {}
297
+ if (dataType) {
298
+ steps.fixture = {
299
+ _app: "fixture",
300
+ _step: "fixture",
301
+ _dataType: dataType,
302
+ data,
303
+ keys: data,
304
+ }
305
+ }
306
+ const context: PipelineContext = { steps, keys: data, errors: [] }
307
+ return { keys: data, context }
308
+ }
309
+
310
+ /**
311
+ * Storybook-style harness for MCP widgets. Renders a single widget with fixture
312
+ * data and a mocked host environment so it can be developed in isolation —
313
+ * without booting a real MCP host or backend.
314
+ *
315
+ * ### What it mocks
316
+ * - **`window.openai`** — the `mcp-use` Apps-SDK bridge `useWidget`/`useCallTool`
317
+ * read from. Installed on mount, removed on unmount. `callTool` is routed to a
318
+ * {@link FixtureCallToolRegistry} built from `tools`; `sendFollowUpMessage`,
319
+ * `openExternal`, etc. are reported to `onHostAction`.
320
+ * - **The toolkit `AppQueryProvider`** — so widgets that fetch via `useToolQuery`
321
+ * (e.g. generated `use*` hooks) resolve against the same registry.
322
+ * - **`ModelContext`** — a flush handler captures the serialized context string
323
+ * and forwards it to `onModelContext`, so you can *see* what the model would.
324
+ * - **A {@link HostBridge}** — a `createStandaloneHostBridge` over the same
325
+ * registry and callbacks, provided via `HostBridgeProvider`, so widgets
326
+ * written against `useHostBridge` (the host-portable pattern) are simulated
327
+ * from the same single source as the `window.openai` shim above.
328
+ *
329
+ * ### Boundaries
330
+ * `mcp-use`'s hooks select their provider from `window.openai` presence; this
331
+ * harness deliberately drives the Apps-SDK path (sets `window.openai`) rather
332
+ * than the `postMessage` bridge, which only activates inside a cross-origin
333
+ * iframe. The shim is intentionally minimal — it is not a host, and host
334
+ * features beyond the bridge verbs above are out of scope.
335
+ */
336
+ export function WidgetFixtureHost({
337
+ widget: Widget,
338
+ data = {},
339
+ dataType,
340
+ tools,
341
+ globals,
342
+ onModelContext,
343
+ onHostAction,
344
+ }: WidgetFixtureHostProps): ReactNode {
345
+ // One registry per `tools` identity. Rebuilt when the fixture map changes so
346
+ // edits in the playground take effect without a full remount.
347
+ const registry = useMemo(() => new FixtureCallToolRegistry(tools), [tools])
348
+
349
+ // Latest callbacks without re-installing the shim on every render. Updated in
350
+ // an effect (not during render) so the shim's closures always see the newest
351
+ // handler while the `window.openai` install effect stays stable.
352
+ const onHostActionRef = useRef(onHostAction)
353
+ const onModelContextRef = useRef(onModelContext)
354
+ useEffect(() => {
355
+ onHostActionRef.current = onHostAction
356
+ onModelContextRef.current = onModelContext
357
+ })
358
+
359
+ const widgetProps = useMemo(() => buildFixtureWidgetProps(data, dataType), [data, dataType])
360
+
361
+ // Install the `window.openai` shim for the lifetime of this mount. Wake any
362
+ // already-mounted `mcp-use` hooks via the `set_globals` event they listen for.
363
+ useEffect(() => {
364
+ if (typeof window === "undefined") return
365
+ const shim = buildOpenAiShim(
366
+ registry,
367
+ data ?? null,
368
+ data ?? {},
369
+ globals ?? {},
370
+ (action) => onHostActionRef.current?.(action),
371
+ (text) => onModelContextRef.current?.(text),
372
+ )
373
+ if (!shim) return
374
+ const prev = (window as { openai?: unknown }).openai
375
+ ;(window as { openai?: unknown }).openai = shim
376
+ window.dispatchEvent(new CustomEvent("openai:set_globals", { detail: { globals: shim } }))
377
+ return () => {
378
+ ;(window as { openai?: unknown }).openai = prev
379
+ }
380
+ }, [registry, data, globals])
381
+
382
+ const callTool = useMemo(
383
+ () => async (name: string, args: object) =>
384
+ registry.call(name, (args as Record<string, unknown>) ?? {}),
385
+ [registry],
386
+ )
387
+
388
+ // A standalone host bridge over the same registry + callbacks, so portable
389
+ // widgets written against `useHostBridge` (rather than the `mcp-use` host
390
+ // hooks) are simulated from the *same* source as the `window.openai` shim.
391
+ // It logs through the same `HostActionLog` channel, so the playground's
392
+ // activity log shows tool calls, follow-ups, and links identically whether the
393
+ // widget uses the mcp-use path or the host bridge. Reading globals.theme keeps
394
+ // the bridge's reported theme in sync with the shim. Depends on the callback
395
+ // props directly (not the refs) — a fresh Provider value on callback change is
396
+ // fine for a dev harness, and keeps the bridge's closures pure of ref reads.
397
+ const bridgeTheme = (globals?.theme ?? DEFAULT_GLOBALS.theme) === "dark" ? "dark" : "light"
398
+ const bridge = useMemo(
399
+ () =>
400
+ createStandaloneHostBridge({
401
+ callTool: (name, args) => {
402
+ onHostAction?.({ type: "callTool", name, args })
403
+ return registry.call(name, args)
404
+ },
405
+ getData: () => data ?? null,
406
+ onFollowup: (prompt) => onHostAction?.({ type: "sendFollowUpMessage", prompt }),
407
+ onOpenExternal: (href) => onHostAction?.({ type: "openExternal", href }),
408
+ onModelContext: (text) => onModelContext?.(text),
409
+ theme: bridgeTheme,
410
+ }),
411
+ [registry, data, bridgeTheme, onHostAction, onModelContext],
412
+ )
413
+
414
+ const value = useMemo<FixtureHostValue>(() => ({ registry }), [registry])
415
+
416
+ // Mount the model-context pump only on the client. The pump calls `mcp-use`'s
417
+ // `useWidget`, whose `useSyncExternalStore` lacks a server snapshot and throws
418
+ // under `react-dom/server` — gating it keeps the harness (and any widget
419
+ // rendered through it) SSR-safe. `useSyncExternalStore` with a `false` server
420
+ // snapshot is the idiomatic "are we on the client?" check (no setState-in-effect).
421
+ const isClient = useSyncExternalStore(
422
+ () => () => {},
423
+ () => true,
424
+ () => false,
425
+ )
426
+
427
+ // Spread `widgetProps` (keys/context) AND a `data` alias so both adapted
428
+ // widgets and raw `({ data })` widgets read their fixture from one mount.
429
+ const RenderWidget = Widget as ComponentType<Record<string, unknown>>
430
+
431
+ return (
432
+ <FixtureHostContext.Provider value={value}>
433
+ {isClient && <ModelContextPump />}
434
+ <HostBridgeProvider bridge={bridge}>
435
+ <AppQueryProvider callTool={callTool}>
436
+ <RenderWidget {...widgetProps} data={data} />
437
+ </AppQueryProvider>
438
+ </HostBridgeProvider>
439
+ </FixtureHostContext.Provider>
440
+ )
441
+ }