@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,367 @@
1
+ import { useState, useCallback } from "react"
2
+ import { useWidget } from "mcp-use/react"
3
+ import { Maximize2, Minimize2, Pencil, RefreshCw } from "lucide-react"
4
+ import type { LayoutConfig, PipelineStepRef } from "@miragon/mcp-toolkit-core"
5
+ import { Skeleton } from "../primitives/skeleton.js"
6
+ import { AppQueryProvider, queryClient } from "../providers/query-provider.js"
7
+ import { useToolResultRecovery } from "../hooks/use-tool-result-recovery.js"
8
+ import { parseToolResult } from "../lib/parse-tool-result.js"
9
+ import { LayoutBuilder } from "./layout-builder.js"
10
+ import { WidgetRenderer, type WidgetComponent } from "./widget-renderer.js"
11
+
12
+ export interface McpAppViewLabels {
13
+ loading?: string
14
+ refresh?: string
15
+ refreshing?: string
16
+ enterFullscreen?: string
17
+ exitFullscreen?: string
18
+ build?: string
19
+ }
20
+
21
+ const DEFAULT_LABELS: Required<McpAppViewLabels> = {
22
+ loading: "Waiting for pipeline result...",
23
+ refresh: "Refresh",
24
+ refreshing: "Loading...",
25
+ enterFullscreen: "Fullscreen",
26
+ exitFullscreen: "Collapse",
27
+ build: "Build",
28
+ }
29
+
30
+ interface RefreshParams {
31
+ keys?: Record<string, unknown>
32
+ steps?: PipelineStepRef[]
33
+ layout?: LayoutConfig
34
+ title?: string
35
+ }
36
+
37
+ interface ViewData {
38
+ _refreshParams?: RefreshParams
39
+ title?: string
40
+ context: {
41
+ keys: Record<string, unknown>
42
+ stepIds: string[]
43
+ stepData: Record<
44
+ string,
45
+ { data: unknown; keys: Record<string, unknown>; _app: string; _dataType: string }
46
+ >
47
+ errors: { stepId: string; reason: string }[]
48
+ }
49
+ layout?: LayoutConfig
50
+ /**
51
+ * Whether the server's in-iframe builder platform is usable (set by
52
+ * `render-view` from `createFrameworkApp`'s `app.builder`). The shell gates
53
+ * its Build affordance on this so the edit button never appears against a
54
+ * server whose `get-builder-catalogue` tool isn't registered.
55
+ */
56
+ builderAvailable?: boolean
57
+ }
58
+
59
+ /**
60
+ * A renderable view payload: the envelope `render-view`/`show_*` tools emit
61
+ * as `structuredContent`. Module-level so it is referentially stable as the
62
+ * recovery hook's `isValid` dependency.
63
+ */
64
+ function isCompleteViewData(value: unknown): value is ViewData {
65
+ const v = value as ViewData | null | undefined
66
+ return Boolean(v?.context && v?.layout)
67
+ }
68
+
69
+ export interface McpAppViewProps {
70
+ /**
71
+ * Map of widget ID to React component. The consumer aggregates this from
72
+ * each module's widget exports and passes it in at the bundled `main.tsx`
73
+ * entry point. All widgets are host-bundled — the framework never fetches
74
+ * widget code at render time.
75
+ */
76
+ widgets: Record<string, WidgetComponent>
77
+ /**
78
+ * Name of the MCP tool that the refresh button invokes. Consumers typically
79
+ * register this tool as a thin wrapper around `renderView(...)` from
80
+ * `@miragon/mcp-toolkit-core`. Defaults to `"refresh-view"`.
81
+ */
82
+ refreshToolName?: string
83
+ /**
84
+ * Explicit override for the Build / edit (Pencil) affordance. Leave it
85
+ * `undefined` (the default) to let the shell follow the server's own
86
+ * signal — `render-view` reports `structuredContent.builderAvailable`,
87
+ * derived from `createFrameworkApp`'s `app.builder`, so the button appears
88
+ * only when the `get-builder-catalogue` tool is actually registered. Set it
89
+ * to `true` / `false` only to force the affordance on or off regardless of
90
+ * that signal (e.g. a custom server that registers the catalogue tool by
91
+ * hand). Even when forced on, the LayoutBuilder's catalogue fetch fails
92
+ * soft (no crash) if the tool is absent.
93
+ */
94
+ builderEnabled?: boolean
95
+ /**
96
+ * Override UI strings (loading, refresh, fullscreen, build).
97
+ * Defaults to English.
98
+ */
99
+ labels?: McpAppViewLabels
100
+ }
101
+
102
+ export function McpAppView({
103
+ widgets,
104
+ refreshToolName = "refresh-view",
105
+ builderEnabled,
106
+ labels,
107
+ }: McpAppViewProps) {
108
+ const effectiveLabels = { ...DEFAULT_LABELS, ...labels }
109
+ const {
110
+ props: initialViewData,
111
+ isPending,
112
+ callTool,
113
+ displayMode: currentDisplayMode,
114
+ requestDisplayMode,
115
+ safeArea,
116
+ toolInput,
117
+ hostContext,
118
+ } = useWidget<ViewData>()
119
+
120
+ const [viewData, setViewData] = useState<ViewData | null>(null)
121
+ const [displayMode, setDisplayMode] = useState<string>(currentDisplayMode ?? "inline")
122
+ const [prevCurrentDisplayMode, setPrevCurrentDisplayMode] = useState(currentDisplayMode)
123
+ if (currentDisplayMode !== prevCurrentDisplayMode) {
124
+ setPrevCurrentDisplayMode(currentDisplayMode)
125
+ if (currentDisplayMode) setDisplayMode(currentDisplayMode)
126
+ }
127
+ const [isRefreshing, setIsRefreshing] = useState(false)
128
+
129
+ // In-iframe build mode is purely a UI state — the LLM never sees the
130
+ // catalogue, the user toggles into build by clicking the toolbar button.
131
+ const [buildMode, setBuildMode] = useState(false)
132
+
133
+ // Sync host-provided initialViewData → local viewData via render-phase
134
+ // setState (rather than an effect) so the React Compiler doesn't flag a
135
+ // cascading render. The guard against transient empty toolOutput stays:
136
+ // widgetProps may briefly drop `context`/`layout` when displayMode toggles,
137
+ // and we don't want that to clobber a valid viewData.
138
+ //
139
+ // `prev` MUST start as `null`, not `initialViewData`: hosts that expose the
140
+ // complete tool output before the app's first render (e.g. window.openai
141
+ // globals injected ahead of script execution) would otherwise never trip the
142
+ // identity check — initialViewData never changes again, the sync never runs,
143
+ // and the app idles on the loading skeleton forever.
144
+ const [prevInitialViewData, setPrevInitialViewData] = useState<typeof initialViewData | null>(
145
+ null,
146
+ )
147
+ if (initialViewData !== prevInitialViewData) {
148
+ setPrevInitialViewData(initialViewData)
149
+ if (!isPending && isCompleteViewData(initialViewData)) {
150
+ setViewData(initialViewData)
151
+ }
152
+ }
153
+
154
+ // Hosts that strip `structuredContent` from the tool-result notification
155
+ // (claude.ai / Claude Desktop) leave `initialViewData` empty forever; the
156
+ // recovery re-executes the originating tool once via `callTool` (responses
157
+ // carry `structuredContent` intact) and fills the gap. Conforming hosts
158
+ // deliver a valid payload up front, so this never fires there.
159
+ const recovery = useToolResultRecovery<ViewData>({
160
+ resultReady: !isPending,
161
+ props: initialViewData,
162
+ isValid: isCompleteViewData,
163
+ toolInput,
164
+ hostContext,
165
+ callTool,
166
+ })
167
+ const [prevRecovered, setPrevRecovered] = useState<ViewData | null>(null)
168
+ if (recovery.data !== prevRecovered) {
169
+ setPrevRecovered(recovery.data)
170
+ // The host-delivered payload always wins; recovery only fills the gap.
171
+ if (recovery.data && !viewData) setViewData(recovery.data)
172
+ }
173
+
174
+ const toggleFullscreen = useCallback(async () => {
175
+ const newMode = displayMode === "fullscreen" ? "inline" : "fullscreen"
176
+ try {
177
+ const result = await requestDisplayMode(newMode)
178
+ setDisplayMode(result.mode)
179
+ } catch (e) {
180
+ console.error("Failed to toggle display mode:", e)
181
+ }
182
+ }, [displayMode, requestDisplayMode])
183
+
184
+ // Stable callTool callback (avoids a new object reference on every render)
185
+ const callToolFn = useCallback(
186
+ async (name: string, args: object) => {
187
+ return callTool(name, args as Record<string, unknown>)
188
+ },
189
+ [callTool],
190
+ )
191
+
192
+ const refreshView = useCallback(async () => {
193
+ if (!viewData?._refreshParams) return
194
+ setIsRefreshing(true)
195
+ try {
196
+ const result = await callTool(
197
+ refreshToolName,
198
+ viewData._refreshParams as unknown as Record<string, unknown>,
199
+ )
200
+ // Decode through the shared structured-first parser (one convention with
201
+ // `useToolQuery`); a refresh returns the view envelope as `structuredContent`.
202
+ const refreshed = parseToolResult<ViewData | null>(result)
203
+ if (refreshed?.context && refreshed?.layout) {
204
+ setViewData(refreshed)
205
+ }
206
+ void queryClient.invalidateQueries()
207
+ } catch (e) {
208
+ console.error("Failed to refresh view:", e)
209
+ } finally {
210
+ setIsRefreshing(false)
211
+ }
212
+ }, [callTool, refreshToolName, viewData])
213
+
214
+ // Called by LayoutBuilder when the user clicks Done. Commits the draft
215
+ // layout (and any keys/steps edits) into the parent viewData so the
216
+ // WidgetRenderer takes over showing the just-built layout.
217
+ const exitBuildMode = useCallback(
218
+ (commit?: {
219
+ layout: LayoutConfig
220
+ keys?: Record<string, unknown>
221
+ steps?: PipelineStepRef[]
222
+ title?: string
223
+ context?: ViewData["context"]
224
+ }) => {
225
+ if (commit) {
226
+ setViewData((prev) =>
227
+ prev
228
+ ? {
229
+ ...prev,
230
+ layout: commit.layout,
231
+ title: commit.title ?? prev.title,
232
+ context: commit.context ?? prev.context,
233
+ _refreshParams: {
234
+ keys: commit.keys,
235
+ steps: commit.steps,
236
+ layout: commit.layout,
237
+ title: commit.title ?? prev.title,
238
+ },
239
+ }
240
+ : prev,
241
+ )
242
+ }
243
+ setBuildMode(false)
244
+ },
245
+ [],
246
+ )
247
+
248
+ if (isPending || !viewData) {
249
+ return (
250
+ <div className="flex flex-col gap-4 p-4">
251
+ <Skeleton className="h-8 w-48" />
252
+ <Skeleton className="h-32 w-full" />
253
+ <p className="text-muted-foreground text-sm">{effectiveLabels.loading}</p>
254
+ </div>
255
+ )
256
+ }
257
+
258
+ const showHeader = !buildMode
259
+
260
+ // Follow the server's own signal by default: render-view reports
261
+ // `builderAvailable` (derived from `app.builder`), so the Build button only
262
+ // shows when the catalogue/dashboard tools are actually registered. An
263
+ // explicit `builderEnabled` prop forces the affordance on or off.
264
+ const builderAvailable = builderEnabled ?? viewData.builderAvailable ?? false
265
+
266
+ return (
267
+ <main
268
+ style={{
269
+ // Fullscreen: the widget owns the viewport, so we take the full window
270
+ // and handle our own scrolling. Inline: the host (Claude, ChatGPT) owns
271
+ // sizing + scrolling and is notified of our intrinsic height via
272
+ // `McpUseProvider`, so we let content dictate height and avoid setting
273
+ // overflowY (which would clip our own content inside a host that's
274
+ // already auto-sized to us).
275
+ ...(displayMode === "fullscreen"
276
+ ? { minHeight: "100vh", overflowY: "auto" as const }
277
+ : null),
278
+ paddingTop: safeArea?.insets?.top,
279
+ paddingRight: safeArea?.insets?.right,
280
+ paddingBottom: safeArea?.insets?.bottom,
281
+ paddingLeft: safeArea?.insets?.left,
282
+ }}
283
+ >
284
+ {showHeader && (
285
+ <div className="mb-4 flex items-center justify-between">
286
+ {viewData.title && <h2 className="text-xl font-bold">{viewData.title}</h2>}
287
+ <div className="ml-auto flex items-center gap-2">
288
+ {viewData._refreshParams && viewData.layout && (
289
+ <button
290
+ onClick={() => {
291
+ void refreshView()
292
+ }}
293
+ disabled={isRefreshing}
294
+ className="hover:bg-accent hover:text-accent-foreground inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors disabled:opacity-50"
295
+ >
296
+ <RefreshCw
297
+ aria-hidden="true"
298
+ className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
299
+ />
300
+ {isRefreshing ? effectiveLabels.refreshing : effectiveLabels.refresh}
301
+ </button>
302
+ )}
303
+ {builderAvailable && viewData._refreshParams && (
304
+ <button
305
+ onClick={() => setBuildMode(true)}
306
+ className="hover:bg-accent hover:text-accent-foreground inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors"
307
+ >
308
+ <Pencil className="h-4 w-4" />
309
+ {effectiveLabels.build}
310
+ </button>
311
+ )}
312
+ <button
313
+ onClick={() => {
314
+ void toggleFullscreen()
315
+ }}
316
+ className="hover:bg-accent hover:text-accent-foreground inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors"
317
+ >
318
+ {displayMode === "fullscreen" ? (
319
+ <Minimize2 aria-hidden="true" className="h-4 w-4" />
320
+ ) : (
321
+ <Maximize2 aria-hidden="true" className="h-4 w-4" />
322
+ )}
323
+ {displayMode === "fullscreen"
324
+ ? effectiveLabels.exitFullscreen
325
+ : effectiveLabels.enterFullscreen}
326
+ </button>
327
+ </div>
328
+ </div>
329
+ )}
330
+
331
+ {!buildMode && viewData.context.errors.length > 0 && (
332
+ <div className="mb-4 flex flex-col gap-2">
333
+ {viewData.context.errors.map((err) => (
334
+ <div
335
+ key={err.stepId}
336
+ className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm"
337
+ >
338
+ <strong>{err.stepId}:</strong> {err.reason}
339
+ </div>
340
+ ))}
341
+ </div>
342
+ )}
343
+
344
+ <AppQueryProvider callTool={callToolFn}>
345
+ {buildMode ? (
346
+ <LayoutBuilder
347
+ initialLayout={viewData.layout}
348
+ title={viewData.title}
349
+ initialKeys={viewData._refreshParams?.keys}
350
+ initialSteps={viewData._refreshParams?.steps}
351
+ widgets={widgets}
352
+ callTool={callToolFn}
353
+ onExit={exitBuildMode}
354
+ />
355
+ ) : viewData.layout ? (
356
+ <WidgetRenderer
357
+ layout={viewData.layout}
358
+ keys={viewData.context.keys}
359
+ stepData={viewData.context.stepData}
360
+ errors={viewData.context.errors}
361
+ widgets={widgets}
362
+ />
363
+ ) : null}
364
+ </AppQueryProvider>
365
+ </main>
366
+ )
367
+ }
@@ -0,0 +1,25 @@
1
+ import { McpUseProvider } from "mcp-use/react"
2
+ import { McpAppView, type McpAppViewProps } from "./mcp-app-view.js"
3
+
4
+ /**
5
+ * Consumer-facing root for a toolkit-rendered MCP App. Wraps `McpAppView`
6
+ * in `McpUseProvider` so the host (Claude, ChatGPT Apps SDK) receives
7
+ * intrinsic-height notifications and the widget auto-sizes inside the
8
+ * host's inline container.
9
+ *
10
+ * Usage at the bundle entry point:
11
+ *
12
+ * ```tsx
13
+ * createRoot(root).render(<McpToolkitApp widgets={widgets} />)
14
+ * ```
15
+ *
16
+ * `McpUseProvider` also installs StrictMode, a default ErrorBoundary, and
17
+ * the theme plumbing used by the toolkit primitives.
18
+ */
19
+ export function McpToolkitApp(props: McpAppViewProps) {
20
+ return (
21
+ <McpUseProvider>
22
+ <McpAppView {...props} />
23
+ </McpUseProvider>
24
+ )
25
+ }
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { buildShowWidgetIntent } from "./use-host-actions.js"
3
+
4
+ describe("buildShowWidgetIntent", () => {
5
+ it("appends the tool-name hint in parentheses so the agent can disambiguate", () => {
6
+ expect(buildShowWidgetIntent("show_process_detail", "Show the process detail for `abc`")).toBe(
7
+ "Show the process detail for `abc` (use show_process_detail)",
8
+ )
9
+ })
10
+
11
+ it("keeps the description verbatim ahead of the hint", () => {
12
+ expect(buildShowWidgetIntent("analytics_show_kpis", "Open the analytics dashboard")).toBe(
13
+ "Open the analytics dashboard (use analytics_show_kpis)",
14
+ )
15
+ })
16
+ })
@@ -0,0 +1,88 @@
1
+ import { useCallback } from "react"
2
+ import { useHostBridge } from "./host-bridge.js"
3
+
4
+ export interface HostActions {
5
+ /**
6
+ * Open an external URL in a new browser tab via the host bridge
7
+ * (`bridge.openLink` / `window.openai.openExternal`). Works inside the
8
+ * sandboxed iframe where raw `window.open` is blocked. Falls back to
9
+ * `window.open` only when no MCP host bridge is available (dev preview).
10
+ */
11
+ openLink: (url: string) => void
12
+ /**
13
+ * Ask the host to render another tool's widget by sending a follow-up
14
+ * user message into the conversation (`bridge.sendMessage` /
15
+ * `window.openai.sendFollowUpMessage`). The agent interprets the prompt
16
+ * and invokes the matching tool, whose widget then appears as the next
17
+ * chat turn — the proper MCP-Apps pattern for in-widget navigation.
18
+ *
19
+ * The prompt should be natural language and include enough hints
20
+ * (preferably the tool name in parentheses) for the agent to pick the
21
+ * right tool unambiguously.
22
+ */
23
+ showWidget: (prompt: string) => void
24
+ /**
25
+ * Hand an open-ended, judgment task to the agent in the conversation
26
+ * (`sendFollowUpMessage`) — root-cause analysis, "explain this", migration
27
+ * planning, ticket drafting. Same bridge as {@link HostActions.showWidget}
28
+ * but a distinct affordance: this crosses the UI→chat boundary deliberately,
29
+ * for things the deterministic UI cannot do. The prompt should be
30
+ * self-contained — inline the relevant IDs/keys rather than relying on
31
+ * ambient ModelContext alone.
32
+ */
33
+ askAi: (prompt: string) => void
34
+ }
35
+
36
+ /**
37
+ * Builds the natural-language intent string passed to {@link HostActions.showWidget}.
38
+ * The host agent reads the trailing `(use <toolName>)` hint to pick the right
39
+ * tool unambiguously. Centralised so every in-widget navigation handoff phrases
40
+ * the hint identically (and a tool rename only has to flow through the imported
41
+ * `*_SHOW_*` tool-name constant).
42
+ *
43
+ * @example host.showWidget(buildShowWidgetIntent(SHOW_PROCESS_DETAIL, `Show the process detail for \`${key}\``))
44
+ */
45
+ export function buildShowWidgetIntent(toolName: string, description: string): string {
46
+ return `${description} (use ${toolName})`
47
+ }
48
+
49
+ /**
50
+ * Bridge-aware host actions for widgets. A thin, intent-named facade over
51
+ * {@link useHostBridge}: `openLink` → `bridge.openExternal`, and both
52
+ * `showWidget` and `askAi` → `bridge.sendFollowup` (kept as two affordances
53
+ * because they cross the UI→chat boundary for different reasons — see above).
54
+ *
55
+ * Relationship to {@link useHostBridge}: this hook resolves the active bridge
56
+ * through `useHostBridge()`, so it honours an explicit `HostBridgeProvider`
57
+ * (ChatGPT / standalone) when one is present, and otherwise falls back to the
58
+ * default `mcp-use` bridge — exactly the behaviour widgets had before the bridge
59
+ * abstraction existed, so no existing widget or test breaks. Prefer
60
+ * `useHostBridge` directly in new portable widgets; `useHostActions` stays for
61
+ * its named affordances and backward compatibility.
62
+ */
63
+ export function useHostActions(): HostActions {
64
+ const bridge = useHostBridge()
65
+
66
+ const openLink = useCallback(
67
+ (url: string) => {
68
+ bridge.openExternal(url)
69
+ },
70
+ [bridge],
71
+ )
72
+
73
+ const showWidget = useCallback(
74
+ (prompt: string) => {
75
+ bridge.sendFollowup(prompt)
76
+ },
77
+ [bridge],
78
+ )
79
+
80
+ const askAi = useCallback(
81
+ (prompt: string) => {
82
+ bridge.sendFollowup(prompt)
83
+ },
84
+ [bridge],
85
+ )
86
+
87
+ return { openLink, showWidget, askAi }
88
+ }
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { FixtureCallToolRegistry, buildFixtureWidgetProps } from "./widget-fixture.js"
3
+
4
+ describe("FixtureCallToolRegistry", () => {
5
+ it("returns a static value entry wrapped under structuredContent", async () => {
6
+ const reg = new FixtureCallToolRegistry()
7
+ reg.register("get_thing", { id: "a", name: "Thing A" })
8
+
9
+ const res = await reg.call("get_thing", {})
10
+
11
+ expect(res.structuredContent).toEqual({ id: "a", name: "Thing A" })
12
+ // Mirrored into a text block so mcp-use's text-first decoder also sees it.
13
+ expect(res.content?.[0]?.text).toContain("Thing A")
14
+ })
15
+
16
+ it("passes a value entry through untouched when it already looks like a tool response", async () => {
17
+ const reg = new FixtureCallToolRegistry({
18
+ already_shaped: { structuredContent: { ok: true }, isError: false },
19
+ })
20
+
21
+ const res = await reg.call("already_shaped")
22
+
23
+ expect(res).toEqual({ structuredContent: { ok: true }, isError: false })
24
+ })
25
+
26
+ it("invokes a handler entry with the call args and wraps the return value", async () => {
27
+ const reg = new FixtureCallToolRegistry()
28
+ reg.register("search", (args: Record<string, unknown>) => ({
29
+ query: args.q,
30
+ hits: [String(args.q)],
31
+ }))
32
+
33
+ const res = await reg.call("search", { q: "leasing" })
34
+
35
+ expect(res.structuredContent).toEqual({ query: "leasing", hits: ["leasing"] })
36
+ })
37
+
38
+ it("awaits async handler entries", async () => {
39
+ const reg = new FixtureCallToolRegistry()
40
+ reg.register("slow", async (args: Record<string, unknown>) => {
41
+ await Promise.resolve()
42
+ return { echoed: args.value }
43
+ })
44
+
45
+ const res = await reg.call("slow", { value: 42 })
46
+
47
+ expect(res.structuredContent).toEqual({ echoed: 42 })
48
+ })
49
+
50
+ it("wraps a string entry into both structuredContent and a text block", async () => {
51
+ const reg = new FixtureCallToolRegistry({ greet: "hello" })
52
+
53
+ const res = await reg.call("greet")
54
+
55
+ expect(res.structuredContent).toBe("hello")
56
+ expect(res.content?.[0]?.text).toBe("hello")
57
+ })
58
+
59
+ it("rejects unknown tools with a descriptive, name-listing error", async () => {
60
+ const reg = new FixtureCallToolRegistry({ known: { ok: true } })
61
+
62
+ await expect(reg.call("missing")).rejects.toThrow(/No fixture registered for tool "missing"/)
63
+ await expect(reg.call("missing")).rejects.toThrow(/known/)
64
+ })
65
+
66
+ it("lists registered names and reports membership", () => {
67
+ const reg = new FixtureCallToolRegistry({ a: 1, b: 2 })
68
+ reg.register("c", 3)
69
+
70
+ expect(reg.has("a")).toBe(true)
71
+ expect(reg.has("z")).toBe(false)
72
+ expect(reg.names().sort()).toEqual(["a", "b", "c"])
73
+ })
74
+
75
+ it("overwrites an existing fixture on re-register", async () => {
76
+ const reg = new FixtureCallToolRegistry({ t: { v: 1 } })
77
+ reg.register("t", { v: 2 })
78
+
79
+ const res = await reg.call("t")
80
+
81
+ expect(res.structuredContent).toEqual({ v: 2 })
82
+ })
83
+ })
84
+
85
+ describe("buildFixtureWidgetProps", () => {
86
+ it("uses the fixture data as the keys map and an empty pipeline by default", () => {
87
+ const data = { "customers:customer": { name: "Acme" } }
88
+
89
+ const props = buildFixtureWidgetProps(data)
90
+
91
+ expect(props.keys).toBe(data)
92
+ expect(props.context.keys).toBe(data)
93
+ expect(props.context.steps).toEqual({})
94
+ expect(props.context.errors).toEqual([])
95
+ })
96
+
97
+ it("seeds a pipeline step keyed by dataType for adaptDataWidget components", () => {
98
+ const data = { value: 7 }
99
+
100
+ const props = buildFixtureWidgetProps(data, "demo:metric")
101
+
102
+ expect(props.context.steps.fixture?._dataType).toBe("demo:metric")
103
+ expect(props.context.steps.fixture?.data).toBe(data)
104
+ // The adapter scans steps by _dataType, so a matching step must exist.
105
+ const match = Object.values(props.context.steps).find((s) => s._dataType === "demo:metric")
106
+ expect(match?.data).toBe(data)
107
+ })
108
+ })