@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.
- package/README.md +36 -0
- package/dist/app/adapt-data-widget.d.ts +33 -0
- package/dist/app/adapt-data-widget.d.ts.map +1 -0
- package/dist/app/adapt-data-widget.js +35 -0
- package/dist/app/adapt-data-widget.js.map +1 -0
- package/dist/app/builder/CatalogueSheet.d.ts +13 -0
- package/dist/app/builder/CatalogueSheet.d.ts.map +1 -0
- package/dist/app/builder/CatalogueSheet.js +33 -0
- package/dist/app/builder/CatalogueSheet.js.map +1 -0
- package/dist/app/builder/PipelineStrip.d.ts +22 -0
- package/dist/app/builder/PipelineStrip.d.ts.map +1 -0
- package/dist/app/builder/PipelineStrip.js +31 -0
- package/dist/app/builder/PipelineStrip.js.map +1 -0
- package/dist/app/builder/PreviewPane.d.ts +13 -0
- package/dist/app/builder/PreviewPane.d.ts.map +1 -0
- package/dist/app/builder/PreviewPane.js +21 -0
- package/dist/app/builder/PreviewPane.js.map +1 -0
- package/dist/app/builder/SaveDialog.d.ts +14 -0
- package/dist/app/builder/SaveDialog.d.ts.map +1 -0
- package/dist/app/builder/SaveDialog.js +14 -0
- package/dist/app/builder/SaveDialog.js.map +1 -0
- package/dist/app/builder/Toolbar.d.ts +17 -0
- package/dist/app/builder/Toolbar.d.ts.map +1 -0
- package/dist/app/builder/Toolbar.js +15 -0
- package/dist/app/builder/Toolbar.js.map +1 -0
- package/dist/app/builder/Workspace.d.ts +28 -0
- package/dist/app/builder/Workspace.d.ts.map +1 -0
- package/dist/app/builder/Workspace.js +117 -0
- package/dist/app/builder/Workspace.js.map +1 -0
- package/dist/app/builder/builder-model.d.ts +86 -0
- package/dist/app/builder/builder-model.d.ts.map +1 -0
- package/dist/app/builder/builder-model.js +174 -0
- package/dist/app/builder/builder-model.js.map +1 -0
- package/dist/app/builder/builder-reducer.d.ts +71 -0
- package/dist/app/builder/builder-reducer.d.ts.map +1 -0
- package/dist/app/builder/builder-reducer.js +201 -0
- package/dist/app/builder/builder-reducer.js.map +1 -0
- package/dist/app/builder/labels.d.ts +78 -0
- package/dist/app/builder/labels.d.ts.map +1 -0
- package/dist/app/builder/labels.js +71 -0
- package/dist/app/builder/labels.js.map +1 -0
- package/dist/app/builder/status-chip.d.ts +10 -0
- package/dist/app/builder/status-chip.d.ts.map +1 -0
- package/dist/app/builder/status-chip.js +16 -0
- package/dist/app/builder/status-chip.js.map +1 -0
- package/dist/app/host-bridge.d.ts +211 -0
- package/dist/app/host-bridge.d.ts.map +1 -0
- package/dist/app/host-bridge.js +247 -0
- package/dist/app/host-bridge.js.map +1 -0
- package/dist/app/index.d.ts +16 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +18 -0
- package/dist/app/index.js.map +1 -0
- package/dist/app/layout-builder.d.ts +48 -0
- package/dist/app/layout-builder.d.ts.map +1 -0
- package/dist/app/layout-builder.js +418 -0
- package/dist/app/layout-builder.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +43 -0
- package/dist/app/mcp-app-view.d.ts.map +1 -0
- package/dist/app/mcp-app-view.js +169 -0
- package/dist/app/mcp-app-view.js.map +1 -0
- package/dist/app/mcp-toolkit-app.d.ts +18 -0
- package/dist/app/mcp-toolkit-app.d.ts.map +1 -0
- package/dist/app/mcp-toolkit-app.js +22 -0
- package/dist/app/mcp-toolkit-app.js.map +1 -0
- package/dist/app/use-host-actions.d.ts +57 -0
- package/dist/app/use-host-actions.d.ts.map +1 -0
- package/dist/app/use-host-actions.js +42 -0
- package/dist/app/use-host-actions.js.map +1 -0
- package/dist/app/widget-fixture.d.ts +180 -0
- package/dist/app/widget-fixture.d.ts.map +1 -0
- package/dist/app/widget-fixture.js +273 -0
- package/dist/app/widget-fixture.js.map +1 -0
- package/dist/app/widget-props-sheet.d.ts +20 -0
- package/dist/app/widget-props-sheet.d.ts.map +1 -0
- package/dist/app/widget-props-sheet.js +87 -0
- package/dist/app/widget-props-sheet.js.map +1 -0
- package/dist/app/widget-renderer.d.ts +87 -0
- package/dist/app/widget-renderer.d.ts.map +1 -0
- package/dist/app/widget-renderer.js +105 -0
- package/dist/app/widget-renderer.js.map +1 -0
- package/dist/components/DrillButton.d.ts +22 -0
- package/dist/components/DrillButton.d.ts.map +1 -0
- package/dist/components/DrillButton.js +13 -0
- package/dist/components/DrillButton.js.map +1 -0
- package/dist/components/FilterBar.d.ts +28 -0
- package/dist/components/FilterBar.d.ts.map +1 -0
- package/dist/components/FilterBar.js +12 -0
- package/dist/components/FilterBar.js.map +1 -0
- package/dist/components/GridLayout.d.ts +26 -0
- package/dist/components/GridLayout.d.ts.map +1 -0
- package/dist/components/GridLayout.js +31 -0
- package/dist/components/GridLayout.js.map +1 -0
- package/dist/components/GroupCard.d.ts +21 -0
- package/dist/components/GroupCard.d.ts.map +1 -0
- package/dist/components/GroupCard.js +24 -0
- package/dist/components/GroupCard.js.map +1 -0
- package/dist/components/KpiGrid.d.ts +43 -0
- package/dist/components/KpiGrid.d.ts.map +1 -0
- package/dist/components/KpiGrid.js +35 -0
- package/dist/components/KpiGrid.js.map +1 -0
- package/dist/components/ListFooter.d.ts +22 -0
- package/dist/components/ListFooter.d.ts.map +1 -0
- package/dist/components/ListFooter.js +13 -0
- package/dist/components/ListFooter.js.map +1 -0
- package/dist/components/Pills.d.ts +25 -0
- package/dist/components/Pills.d.ts.map +1 -0
- package/dist/components/Pills.js +17 -0
- package/dist/components/Pills.js.map +1 -0
- package/dist/components/SectionHeading.d.ts +16 -0
- package/dist/components/SectionHeading.d.ts.map +1 -0
- package/dist/components/SectionHeading.js +10 -0
- package/dist/components/SectionHeading.js.map +1 -0
- package/dist/components/WidgetHeader.d.ts +21 -0
- package/dist/components/WidgetHeader.d.ts.map +1 -0
- package/dist/components/WidgetHeader.js +11 -0
- package/dist/components/WidgetHeader.js.map +1 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-debounced-value.d.ts +7 -0
- package/dist/hooks/use-debounced-value.d.ts.map +1 -0
- package/dist/hooks/use-debounced-value.js +15 -0
- package/dist/hooks/use-debounced-value.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +6 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +19 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/use-tool-query.d.ts +21 -0
- package/dist/hooks/use-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-tool-query.js +47 -0
- package/dist/hooks/use-tool-query.js.map +1 -0
- package/dist/hooks/use-tool-result-recovery.d.ts +54 -0
- package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -0
- package/dist/hooks/use-tool-result-recovery.js +93 -0
- package/dist/hooks/use-tool-result-recovery.js.map +1 -0
- package/dist/hooks/use-view-data.d.ts +19 -0
- package/dist/hooks/use-view-data.d.ts.map +1 -0
- package/dist/hooks/use-view-data.js +18 -0
- package/dist/hooks/use-view-data.js.map +1 -0
- package/dist/hooks/use-view-tool-query.d.ts +18 -0
- package/dist/hooks/use-view-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-view-tool-query.js +29 -0
- package/dist/hooks/use-view-tool-query.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/parse-tool-result.d.ts +39 -0
- package/dist/lib/parse-tool-result.d.ts.map +1 -0
- package/dist/lib/parse-tool-result.js +66 -0
- package/dist/lib/parse-tool-result.js.map +1 -0
- package/dist/lib/tone-utils.d.ts +18 -0
- package/dist/lib/tone-utils.d.ts.map +1 -0
- package/dist/lib/tone-utils.js +25 -0
- package/dist/lib/tone-utils.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/locale/index.d.ts +3 -0
- package/dist/locale/index.d.ts.map +1 -0
- package/dist/locale/index.js +7 -0
- package/dist/locale/index.js.map +1 -0
- package/dist/locale/locale-provider.d.ts +50 -0
- package/dist/locale/locale-provider.d.ts.map +1 -0
- package/dist/locale/locale-provider.js +47 -0
- package/dist/locale/locale-provider.js.map +1 -0
- package/dist/primitives/alert.d.ts +10 -0
- package/dist/primitives/alert.d.ts.map +1 -0
- package/dist/primitives/alert.js +25 -0
- package/dist/primitives/alert.js.map +1 -0
- package/dist/primitives/badge.d.ts +8 -0
- package/dist/primitives/badge.d.ts.map +1 -0
- package/dist/primitives/badge.js +34 -0
- package/dist/primitives/badge.js.map +1 -0
- package/dist/primitives/button.d.ts +11 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +36 -0
- package/dist/primitives/button.js.map +1 -0
- package/dist/primitives/card.d.ts +12 -0
- package/dist/primitives/card.d.ts.map +1 -0
- package/dist/primitives/card.js +25 -0
- package/dist/primitives/card.js.map +1 -0
- package/dist/primitives/dialog.d.ts +18 -0
- package/dist/primitives/dialog.d.ts.map +1 -0
- package/dist/primitives/dialog.js +38 -0
- package/dist/primitives/dialog.js.map +1 -0
- package/dist/primitives/input.d.ts +4 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +7 -0
- package/dist/primitives/input.js.map +1 -0
- package/dist/primitives/scroll-area.d.ts +6 -0
- package/dist/primitives/scroll-area.d.ts.map +1 -0
- package/dist/primitives/scroll-area.js +11 -0
- package/dist/primitives/scroll-area.js.map +1 -0
- package/dist/primitives/select.d.ts +16 -0
- package/dist/primitives/select.d.ts.map +1 -0
- package/dist/primitives/select.js +38 -0
- package/dist/primitives/select.js.map +1 -0
- package/dist/primitives/separator.d.ts +4 -0
- package/dist/primitives/separator.d.ts.map +1 -0
- package/dist/primitives/separator.js +9 -0
- package/dist/primitives/separator.js.map +1 -0
- package/dist/primitives/sheet.d.ts +15 -0
- package/dist/primitives/sheet.d.ts.map +1 -0
- package/dist/primitives/sheet.js +41 -0
- package/dist/primitives/sheet.js.map +1 -0
- package/dist/primitives/skeleton.d.ts +3 -0
- package/dist/primitives/skeleton.d.ts.map +1 -0
- package/dist/primitives/skeleton.js +7 -0
- package/dist/primitives/skeleton.js.map +1 -0
- package/dist/primitives/switch.d.ts +5 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/primitives/switch.js +8 -0
- package/dist/primitives/switch.js.map +1 -0
- package/dist/primitives/table.d.ts +11 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +28 -0
- package/dist/primitives/table.js.map +1 -0
- package/dist/primitives/tabs.d.ts +12 -0
- package/dist/primitives/tabs.d.ts.map +1 -0
- package/dist/primitives/tabs.js +29 -0
- package/dist/primitives/tabs.js.map +1 -0
- package/dist/providers/query-provider.d.ts +11 -0
- package/dist/providers/query-provider.d.ts.map +1 -0
- package/dist/providers/query-provider.js +20 -0
- package/dist/providers/query-provider.js.map +1 -0
- package/dist/theme/create-theme.d.ts +130 -0
- package/dist/theme/create-theme.d.ts.map +1 -0
- package/dist/theme/create-theme.js +65 -0
- package/dist/theme/create-theme.js.map +1 -0
- package/dist/theme/index.d.ts +7 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +7 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/presets.d.ts +25 -0
- package/dist/theme/presets.d.ts.map +1 -0
- package/dist/theme/presets.js +68 -0
- package/dist/theme/presets.js.map +1 -0
- package/dist/theme/theme-provider.d.ts +69 -0
- package/dist/theme/theme-provider.d.ts.map +1 -0
- package/dist/theme/theme-provider.js +79 -0
- package/dist/theme/theme-provider.js.map +1 -0
- package/package.json +71 -0
- package/src/app/adapt-data-widget.tsx +51 -0
- package/src/app/builder/CatalogueSheet.tsx +230 -0
- package/src/app/builder/PipelineStrip.tsx +260 -0
- package/src/app/builder/PreviewPane.tsx +72 -0
- package/src/app/builder/SaveDialog.tsx +95 -0
- package/src/app/builder/Toolbar.tsx +95 -0
- package/src/app/builder/Workspace.tsx +483 -0
- package/src/app/builder/builder-model.test.ts +351 -0
- package/src/app/builder/builder-model.ts +213 -0
- package/src/app/builder/builder-reducer.test.ts +441 -0
- package/src/app/builder/builder-reducer.ts +289 -0
- package/src/app/builder/labels.ts +139 -0
- package/src/app/builder/status-chip.tsx +37 -0
- package/src/app/host-bridge.test.ts +206 -0
- package/src/app/host-bridge.tsx +371 -0
- package/src/app/index.ts +51 -0
- package/src/app/layout-builder.tsx +872 -0
- package/src/app/mcp-app-view.tsx +367 -0
- package/src/app/mcp-toolkit-app.tsx +25 -0
- package/src/app/use-host-actions.test.ts +16 -0
- package/src/app/use-host-actions.tsx +88 -0
- package/src/app/widget-fixture.test.ts +108 -0
- package/src/app/widget-fixture.tsx +441 -0
- package/src/app/widget-props-sheet.tsx +285 -0
- package/src/app/widget-renderer.test.ts +42 -0
- package/src/app/widget-renderer.tsx +200 -0
- package/src/components/DrillButton.tsx +37 -0
- package/src/components/FilterBar.tsx +68 -0
- package/src/components/GridLayout.tsx +49 -0
- package/src/components/GroupCard.tsx +52 -0
- package/src/components/KpiGrid.tsx +134 -0
- package/src/components/ListFooter.tsx +48 -0
- package/src/components/Pills.tsx +45 -0
- package/src/components/SectionHeading.tsx +26 -0
- package/src/components/WidgetHeader.tsx +51 -0
- package/src/globals.css +168 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/use-debounced-value.ts +15 -0
- package/src/hooks/use-mobile.ts +24 -0
- package/src/hooks/use-tool-query.ts +63 -0
- package/src/hooks/use-tool-result-recovery.test.ts +65 -0
- package/src/hooks/use-tool-result-recovery.ts +144 -0
- package/src/hooks/use-view-data.ts +31 -0
- package/src/hooks/use-view-tool-query.ts +38 -0
- package/src/index.ts +134 -0
- package/src/lib/parse-tool-result.test.ts +139 -0
- package/src/lib/parse-tool-result.ts +89 -0
- package/src/lib/tone-utils.test.ts +35 -0
- package/src/lib/tone-utils.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/locale/index.ts +7 -0
- package/src/locale/locale-provider.tsx +81 -0
- package/src/primitives/alert.tsx +60 -0
- package/src/primitives/badge.tsx +49 -0
- package/src/primitives/button.tsx +62 -0
- package/src/primitives/card.tsx +92 -0
- package/src/primitives/dialog.tsx +144 -0
- package/src/primitives/input.tsx +21 -0
- package/src/primitives/scroll-area.tsx +54 -0
- package/src/primitives/select.tsx +173 -0
- package/src/primitives/separator.tsx +21 -0
- package/src/primitives/sheet.tsx +134 -0
- package/src/primitives/skeleton.tsx +13 -0
- package/src/primitives/switch.tsx +24 -0
- package/src/primitives/table.tsx +87 -0
- package/src/primitives/tabs.tsx +79 -0
- package/src/providers/query-provider.tsx +38 -0
- package/src/theme/create-theme.test.ts +108 -0
- package/src/theme/create-theme.ts +158 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/presets.ts +82 -0
- package/src/theme/theme-provider.tsx +152 -0
- package/src/ui-catalog.test.ts +112 -0
- 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
|
+
}
|