@nitrostack/widgets 1.0.3 → 1.0.5
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 +40 -62
- package/dist/hooks/use-merged-tool-output.d.ts +8 -0
- package/dist/hooks/use-merged-tool-output.d.ts.map +1 -0
- package/dist/hooks/use-merged-tool-output.js +26 -0
- package/dist/sdk.d.ts +10 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +51 -1
- package/dist/tool-output-normalize.d.ts +31 -0
- package/dist/tool-output-normalize.d.ts.map +1 -0
- package/dist/tool-output-normalize.js +89 -0
- package/dist/withToolData.d.ts.map +1 -1
- package/dist/withToolData.js +51 -8
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,89 +1,67 @@
|
|
|
1
|
-
# @nitrostack/widgets
|
|
1
|
+
# @nitrostack/widgets
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React SDK for building interactive widget UIs that render with NitroStack MCP
|
|
4
|
+
tool outputs.
|
|
4
5
|
|
|
5
|
-
[](https://www.npmjs.com/package/@nitrostack/widgets)
|
|
7
|
+
[](https://www.npmjs.com/package/@nitrostack/widgets)
|
|
8
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## ✨ Key Features
|
|
14
|
-
|
|
15
|
-
- **⚛️ React Powered**: Build interactive UI logic using the React ecosystem you already know.
|
|
16
|
-
- **📊 Rich Visualization**: Render charts, maps, and complex data tables instead of raw JSON.
|
|
17
|
-
- **🎮 Interactive Elements**: Add buttons, forms, and triggers that call back to your MCP server.
|
|
18
|
-
- **🔌 seamless Integration**: Attaches directly to tool results using the `@Widget()` decorator.
|
|
19
|
-
- **🧪 Visual Preview**: Instantly preview and debug your widgets using NitroStudio.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## 📦 Installation
|
|
10
|
+
## Installation
|
|
24
11
|
|
|
25
12
|
```bash
|
|
26
13
|
npm install @nitrostack/widgets react react-dom
|
|
27
14
|
```
|
|
28
15
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
## 🚀 Quick Start
|
|
32
|
-
|
|
33
|
-
Define a widget in your NitroStack server:
|
|
16
|
+
## What You Get
|
|
34
17
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
id: 'user-card',
|
|
40
|
-
title: 'User Profile',
|
|
41
|
-
path: '/widgets/user-profile'
|
|
42
|
-
})
|
|
43
|
-
async getUser() {
|
|
44
|
-
return { name: 'Alice', role: 'Admin' };
|
|
45
|
-
}
|
|
46
|
-
```
|
|
18
|
+
- `useWidgetSDK()` for data and host interaction
|
|
19
|
+
- Theme/display helpers for adaptive UI behavior
|
|
20
|
+
- State helpers for interactive widget flows
|
|
21
|
+
- Compatibility with NitroStudio widget previews
|
|
47
22
|
|
|
48
|
-
|
|
23
|
+
## Quick Example
|
|
49
24
|
|
|
50
25
|
```tsx
|
|
51
|
-
|
|
52
|
-
import { useWidgetData } from '@nitrostack/widgets';
|
|
26
|
+
'use client';
|
|
53
27
|
|
|
54
|
-
|
|
55
|
-
const { data, loading } = useWidgetData<UserData>();
|
|
28
|
+
import { useWidgetSDK } from '@nitrostack/widgets';
|
|
56
29
|
|
|
57
|
-
|
|
30
|
+
export default function ProductCard() {
|
|
31
|
+
const { isReady, getToolOutput } = useWidgetSDK();
|
|
32
|
+
const data = getToolOutput<{ name: string; price: number }>();
|
|
33
|
+
|
|
34
|
+
if (!isReady || !data) return <div>Loading...</div>;
|
|
58
35
|
|
|
59
36
|
return (
|
|
60
|
-
<div
|
|
37
|
+
<div>
|
|
61
38
|
<h3>{data.name}</h3>
|
|
62
|
-
<p
|
|
39
|
+
<p>${data.price}</p>
|
|
63
40
|
</div>
|
|
64
41
|
);
|
|
65
|
-
}
|
|
42
|
+
}
|
|
66
43
|
```
|
|
67
44
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
## 🎨 NitroStudio
|
|
71
|
-
|
|
72
|
-
NitroStudio is the best way to develop widgets. It automatically runs your server and providing a hot-reloading preview environment where you can see your widgets respond to real tool data in real-time.
|
|
73
|
-
|
|
74
|
-

|
|
45
|
+
## NitroStudio
|
|
75
46
|
|
|
76
|
-
|
|
47
|
+
NitroStudio is the fastest way to test widget output and interaction behavior in
|
|
48
|
+
real MCP workflows.
|
|
77
49
|
|
|
78
|
-
|
|
50
|
+
- Download: <https://nitrostack.ai/studio>
|
|
51
|
+
- Widgets guide: <https://docs.nitrostack.ai/sdk/typescript/ui/widgets>
|
|
79
52
|
|
|
80
|
-
##
|
|
53
|
+
## Links
|
|
81
54
|
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
55
|
+
- Widgets docs: <https://docs.nitrostack.ai/sdk/typescript/ui/widgets>
|
|
56
|
+
- Full docs: <https://docs.nitrostack.ai>
|
|
57
|
+
- Source: <https://github.com/nitrocloudofficial/nitrostack>
|
|
58
|
+
- npm: <https://www.npmjs.com/package/@nitrostack/widgets>
|
|
59
|
+
- Blog: <https://blog.nitrostack.ai>
|
|
86
60
|
|
|
87
|
-
|
|
61
|
+
## Community
|
|
88
62
|
|
|
89
|
-
|
|
63
|
+
- Discord: <https://discord.gg/uVWey6UhuD>
|
|
64
|
+
- X: <https://x.com/nitrostackai>
|
|
65
|
+
- YouTube: <https://www.youtube.com/@nitrostackai>
|
|
66
|
+
- LinkedIn: <https://linkedin.com/company/nitrostack-ai/>
|
|
67
|
+
- GitHub: <https://github.com/nitrostackai>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NitroStack CLI bundles pass tool output as a `data` prop (see bootstrap in `build.ts`).
|
|
3
|
+
* Hosts may also expose the same payload via `window.openai` / `__MCP_APP_CONTEXT__`.
|
|
4
|
+
* Prefer SDK output when present; otherwise use bootstrap `data` so widgets render in MCPJam
|
|
5
|
+
* and similar clients before the bridge is ready.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useMergedToolOutput<T = unknown>(propData?: T | null): T | null;
|
|
8
|
+
//# sourceMappingURL=use-merged-tool-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-merged-tool-output.d.ts","sourceRoot":"","sources":["../../src/hooks/use-merged-tool-output.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAY9E"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useWidgetSDK } from './useWidgetSDK.js';
|
|
2
|
+
import { normalizeToolOutputForWidget } from '../sdk.js';
|
|
3
|
+
function hasNonEmptyRecord(v) {
|
|
4
|
+
return (v != null &&
|
|
5
|
+
typeof v === 'object' &&
|
|
6
|
+
!Array.isArray(v) &&
|
|
7
|
+
Object.keys(v).length > 0);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* NitroStack CLI bundles pass tool output as a `data` prop (see bootstrap in `build.ts`).
|
|
11
|
+
* Hosts may also expose the same payload via `window.openai` / `__MCP_APP_CONTEXT__`.
|
|
12
|
+
* Prefer SDK output when present; otherwise use bootstrap `data` so widgets render in MCPJam
|
|
13
|
+
* and similar clients before the bridge is ready.
|
|
14
|
+
*/
|
|
15
|
+
export function useMergedToolOutput(propData) {
|
|
16
|
+
const { getToolOutput } = useWidgetSDK();
|
|
17
|
+
const sdk = getToolOutput();
|
|
18
|
+
const prop = propData != null ? normalizeToolOutputForWidget(propData) : null;
|
|
19
|
+
if (hasNonEmptyRecord(sdk)) {
|
|
20
|
+
return sdk;
|
|
21
|
+
}
|
|
22
|
+
if (hasNonEmptyRecord(prop)) {
|
|
23
|
+
return prop;
|
|
24
|
+
}
|
|
25
|
+
return (sdk ?? prop ?? null);
|
|
26
|
+
}
|
package/dist/sdk.d.ts
CHANGED
|
@@ -84,8 +84,18 @@ export declare class WidgetSDK {
|
|
|
84
84
|
getToolInput<T = unknown>(): T | null;
|
|
85
85
|
/**
|
|
86
86
|
* Get tool output data
|
|
87
|
+
* Handles both OpenAI (window.openai.toolOutput) and MCP Apps
|
|
88
|
+
* (unwrapping structuredContent or raw content arrays).
|
|
87
89
|
*/
|
|
88
90
|
getToolOutput<T = unknown>(): T | null;
|
|
91
|
+
/**
|
|
92
|
+
* Robustly extract data from tool output.
|
|
93
|
+
* Supports:
|
|
94
|
+
* - OpenAI structuredContent wrapper
|
|
95
|
+
* - Standard MCP content array (text/json)
|
|
96
|
+
* - Standard MCP contents array (fallback)
|
|
97
|
+
*/
|
|
98
|
+
private extractToolData;
|
|
89
99
|
/**
|
|
90
100
|
* Get tool response metadata
|
|
91
101
|
*/
|
package/dist/sdk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEhE;;;GAGG;AACH,qBAAa,SAAS;IAClB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA0B;IAEjD,OAAO;IAIP;;OAEG;IACH,MAAM,CAAC,WAAW,IAAI,SAAS;IAO/B;;;OAGG;IACH,OAAO,IAAI,OAAO;IAKlB;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;;OAGG;IACG,YAAY,CAAC,OAAO,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBjD;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7D;;OAEG;IACH,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAO1C;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAS3F;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOxC;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAOpC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAOjC;;OAEG;IACG,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,WAAW,CAAA;KAAE,CAAC;IAO3E;;OAEG;IACH,YAAY,IAAI,IAAI;IASpB;;OAEG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAO/B;;OAEG;IACG,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASxD;;OAEG;IACH,YAAY,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,IAAI;IAKrC
|
|
1
|
+
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEhE;;;GAGG;AACH,qBAAa,SAAS;IAClB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA0B;IAEjD,OAAO;IAIP;;OAEG;IACH,MAAM,CAAC,WAAW,IAAI,SAAS;IAO/B;;;OAGG;IACH,OAAO,IAAI,OAAO;IAKlB;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;;OAGG;IACG,YAAY,CAAC,OAAO,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBjD;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7D;;OAEG;IACH,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAO1C;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAS3F;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOxC;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAOpC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAOjC;;OAEG;IACG,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,WAAW,CAAA;KAAE,CAAC;IAO3E;;OAEG;IACH,YAAY,IAAI,IAAI;IASpB;;OAEG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAO/B;;OAEG;IACG,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASxD;;OAEG;IACH,YAAY,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,IAAI;IAKrC;;;;OAIG;IACH,aAAa,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,IAAI;IAStC;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAwCvB;;OAEG;IACH,uBAAuB,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,IAAI;IAKhD;;OAEG;IACH,QAAQ,IAAI,OAAO,GAAG,MAAM;IAK5B;;OAEG;IACH,YAAY,IAAI,MAAM;IAKtB;;OAEG;IACH,cAAc,IAAI,WAAW;IAK7B;;OAEG;IACH,YAAY;IAKZ;;OAEG;IACH,SAAS,IAAI,MAAM;IAKnB;;OAEG;IACH,WAAW;IAKX;;OAEG;IACH,SAAS,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,IAAI;IAIlC;;OAEG;IACH,UAAU,IAAI,OAAO;CAGxB;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAExC"}
|
package/dist/sdk.js
CHANGED
|
@@ -172,11 +172,61 @@ export class WidgetSDK {
|
|
|
172
172
|
}
|
|
173
173
|
/**
|
|
174
174
|
* Get tool output data
|
|
175
|
+
* Handles both OpenAI (window.openai.toolOutput) and MCP Apps
|
|
176
|
+
* (unwrapping structuredContent or raw content arrays).
|
|
175
177
|
*/
|
|
176
178
|
getToolOutput() {
|
|
177
179
|
if (!this.isReady())
|
|
178
180
|
return null;
|
|
179
|
-
|
|
181
|
+
const rawOutput = window.openai.toolOutput;
|
|
182
|
+
if (!rawOutput)
|
|
183
|
+
return null;
|
|
184
|
+
return this.extractToolData(rawOutput);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Robustly extract data from tool output.
|
|
188
|
+
* Supports:
|
|
189
|
+
* - OpenAI structuredContent wrapper
|
|
190
|
+
* - Standard MCP content array (text/json)
|
|
191
|
+
* - Standard MCP contents array (fallback)
|
|
192
|
+
*/
|
|
193
|
+
extractToolData(output) {
|
|
194
|
+
if (!output)
|
|
195
|
+
return null;
|
|
196
|
+
// 1. Check for structuredContent (OpenAI / NitroStack default)
|
|
197
|
+
if (output.structuredContent) {
|
|
198
|
+
return output.structuredContent;
|
|
199
|
+
}
|
|
200
|
+
// 2. Check for standard MCP content array
|
|
201
|
+
const contents = output.content || output.contents;
|
|
202
|
+
if (Array.isArray(contents)) {
|
|
203
|
+
// Look for JSON content first
|
|
204
|
+
const jsonContent = contents.find(c => c && typeof c === 'object' && (c.mimeType === 'application/json' || c.type === 'json'));
|
|
205
|
+
if (jsonContent) {
|
|
206
|
+
try {
|
|
207
|
+
return (typeof jsonContent.text === 'string' ? JSON.parse(jsonContent.text) : jsonContent.text);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// fall through
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Look for text content that might be JSON
|
|
214
|
+
const textContent = contents.find(c => c && typeof c === 'object' && (c.mimeType === 'text/plain' || c.type === 'text'));
|
|
215
|
+
if (textContent && typeof textContent.text === 'string') {
|
|
216
|
+
try {
|
|
217
|
+
// Only try parsing if it looks like JSON
|
|
218
|
+
if (textContent.text.trim().startsWith('{') || textContent.text.trim().startsWith('[')) {
|
|
219
|
+
return JSON.parse(textContent.text);
|
|
220
|
+
}
|
|
221
|
+
return textContent.text;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return textContent.text;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// 3. Fallback to raw output if it doesn't match standard patterns
|
|
229
|
+
return output;
|
|
180
230
|
}
|
|
181
231
|
/**
|
|
182
232
|
* Get tool response metadata
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aligns with NitroStack server `appType`: only `mcp-app` needs unwrapping of MCP
|
|
3
|
+
* `tools/call` results. ChatGPT (`openai` / default) must leave `toolOutput` untouched.
|
|
4
|
+
*/
|
|
5
|
+
export type NitroStackWidgetAppType = 'openai' | 'mcp-app';
|
|
6
|
+
/**
|
|
7
|
+
* MCP Apps mode for widgets. Uses **only** `nitrostackAppType` so we never collide with
|
|
8
|
+
* host fields named `appType` (e.g. display or product metadata on the same inject object).
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveWidgetAppType(data: Record<string, unknown>): NitroStackWidgetAppType;
|
|
11
|
+
/**
|
|
12
|
+
* Many hosts set `toolOutput` to the MCP tool result `{ content, structuredContent }` while
|
|
13
|
+
* widgets read the flat JSON in `structuredContent`. If the root also carries real widget
|
|
14
|
+
* fields (e.g. `shops` alongside `content`), keep the root — that is the ChatGPT dual-shape.
|
|
15
|
+
*/
|
|
16
|
+
export declare function coalesceOpenAiToolOutput<T>(toolOutput: T | null | undefined): T | null | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Nitro hosts disagree on shape: `data`, `openai`, `globals`, or `payload` may carry globals.
|
|
19
|
+
* Later keys win: `data` > `payload` > `globals` > `openai`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseNitroInjectOpenaiPayload(message: unknown): Record<string, unknown> | null;
|
|
22
|
+
/**
|
|
23
|
+
* MCP `tools/call` results use `content` blocks plus optional `structuredContent`.
|
|
24
|
+
* ChatGPT Apps SDK typically exposes the structured JSON as `toolOutput` directly.
|
|
25
|
+
*
|
|
26
|
+
* @param appType - When omitted or `openai`, returns `toolOutput` unchanged (required for ChatGPT).
|
|
27
|
+
*/
|
|
28
|
+
export declare function normalizeToolOutputForWidget<T>(toolOutput: T | null | undefined, options?: {
|
|
29
|
+
appType?: NitroStackWidgetAppType;
|
|
30
|
+
}): T | null | undefined;
|
|
31
|
+
//# sourceMappingURL=tool-output-normalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-output-normalize.d.ts","sourceRoot":"","sources":["../src/tool-output-normalize.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,uBAAuB,CAE3F;AA6BD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,CAAC,GAAG,IAAI,GAAG,SAAS,CAclG;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAc9F;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,EAC5C,UAAU,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,uBAAuB,CAAA;CAAE,GAC9C,CAAC,GAAG,IAAI,GAAG,SAAS,CAatB"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Apps mode for widgets. Uses **only** `nitrostackAppType` so we never collide with
|
|
3
|
+
* host fields named `appType` (e.g. display or product metadata on the same inject object).
|
|
4
|
+
*/
|
|
5
|
+
export function resolveWidgetAppType(data) {
|
|
6
|
+
return data.nitrostackAppType === 'mcp-app' ? 'mcp-app' : 'openai';
|
|
7
|
+
}
|
|
8
|
+
/** Keys that belong to MCP `tools/call` / ChatGPT tool-result envelopes, not widget payloads */
|
|
9
|
+
const MCP_TOOL_OUTPUT_ENVELOPE_KEYS = new Set([
|
|
10
|
+
'content',
|
|
11
|
+
'structuredContent',
|
|
12
|
+
'isError',
|
|
13
|
+
'_meta',
|
|
14
|
+
]);
|
|
15
|
+
/** JSON-RPC top-level keys when the client forwards the whole `tools/call` response */
|
|
16
|
+
const JSONRPC_TOP_KEYS = new Set(['result', 'id', 'jsonrpc']);
|
|
17
|
+
/**
|
|
18
|
+
* Some MCP clients pass the full JSON-RPC object `{ jsonrpc, id, result: { content, structuredContent } }`
|
|
19
|
+
* as `toolOutput`. Strip that shell when there are no other sibling payload fields.
|
|
20
|
+
*/
|
|
21
|
+
function peelJsonRpcToolResult(o) {
|
|
22
|
+
const keys = Object.keys(o);
|
|
23
|
+
const extra = keys.filter((k) => !JSONRPC_TOP_KEYS.has(k));
|
|
24
|
+
if (extra.length > 0) {
|
|
25
|
+
return o;
|
|
26
|
+
}
|
|
27
|
+
if (o.result != null && typeof o.result === 'object' && !Array.isArray(o.result)) {
|
|
28
|
+
return o.result;
|
|
29
|
+
}
|
|
30
|
+
return o;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Many hosts set `toolOutput` to the MCP tool result `{ content, structuredContent }` while
|
|
34
|
+
* widgets read the flat JSON in `structuredContent`. If the root also carries real widget
|
|
35
|
+
* fields (e.g. `shops` alongside `content`), keep the root — that is the ChatGPT dual-shape.
|
|
36
|
+
*/
|
|
37
|
+
export function coalesceOpenAiToolOutput(toolOutput) {
|
|
38
|
+
if (toolOutput == null || typeof toolOutput !== 'object' || Array.isArray(toolOutput)) {
|
|
39
|
+
return toolOutput;
|
|
40
|
+
}
|
|
41
|
+
const o = peelJsonRpcToolResult(toolOutput);
|
|
42
|
+
const sc = o.structuredContent;
|
|
43
|
+
if (sc == null || typeof sc !== 'object' || Array.isArray(sc) || !Array.isArray(o.content)) {
|
|
44
|
+
return o === toolOutput ? toolOutput : o;
|
|
45
|
+
}
|
|
46
|
+
const extraKeys = Object.keys(o).filter((k) => !MCP_TOOL_OUTPUT_ENVELOPE_KEYS.has(k));
|
|
47
|
+
if (extraKeys.length > 0) {
|
|
48
|
+
return o === toolOutput ? toolOutput : o;
|
|
49
|
+
}
|
|
50
|
+
return sc;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Nitro hosts disagree on shape: `data`, `openai`, `globals`, or `payload` may carry globals.
|
|
54
|
+
* Later keys win: `data` > `payload` > `globals` > `openai`.
|
|
55
|
+
*/
|
|
56
|
+
export function parseNitroInjectOpenaiPayload(message) {
|
|
57
|
+
if (!message || typeof message !== 'object') {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const m = message;
|
|
61
|
+
const pick = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
62
|
+
const merged = {
|
|
63
|
+
...pick(m.openai),
|
|
64
|
+
...pick(m.globals),
|
|
65
|
+
...pick(m.payload),
|
|
66
|
+
...pick(m.data),
|
|
67
|
+
};
|
|
68
|
+
return Object.keys(merged).length > 0 ? merged : null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* MCP `tools/call` results use `content` blocks plus optional `structuredContent`.
|
|
72
|
+
* ChatGPT Apps SDK typically exposes the structured JSON as `toolOutput` directly.
|
|
73
|
+
*
|
|
74
|
+
* @param appType - When omitted or `openai`, returns `toolOutput` unchanged (required for ChatGPT).
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeToolOutputForWidget(toolOutput, options) {
|
|
77
|
+
if ((options?.appType ?? 'openai') !== 'mcp-app') {
|
|
78
|
+
return toolOutput;
|
|
79
|
+
}
|
|
80
|
+
if (toolOutput == null || typeof toolOutput !== 'object') {
|
|
81
|
+
return toolOutput;
|
|
82
|
+
}
|
|
83
|
+
const o = peelJsonRpcToolResult(toolOutput);
|
|
84
|
+
const sc = o.structuredContent;
|
|
85
|
+
if (sc != null && typeof sc === 'object' && !Array.isArray(sc) && Array.isArray(o.content)) {
|
|
86
|
+
return sc;
|
|
87
|
+
}
|
|
88
|
+
return (o === toolOutput ? toolOutput : o);
|
|
89
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withToolData.d.ts","sourceRoot":"","sources":["../src/withToolData.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAEnD;;;;;;;;GAQG;AAEH,MAAM,WAAW,iBAAiB,CAAC,CAAC,GAAG,GAAG;IACxC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,CAAC,GAAG,GAAG,EAClC,gBAAgB,EAAE,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"withToolData.d.ts","sourceRoot":"","sources":["../src/withToolData.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAEnD;;;;;;;;GAQG;AAEH,MAAM,WAAW,iBAAiB,CAAC,CAAC,GAAG,GAAG;IACxC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,CAAC,GAAG,GAAG,EAClC,gBAAgB,EAAE,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,iDA+PnD"}
|
package/dist/withToolData.js
CHANGED
|
@@ -12,18 +12,60 @@ export function withToolData(WrappedComponent) {
|
|
|
12
12
|
useEffect(() => {
|
|
13
13
|
// Mark as mounted to prevent SSR/hydration issues
|
|
14
14
|
setMounted(true);
|
|
15
|
+
// Robustly extract data from tool output
|
|
16
|
+
const extractToolData = (output) => {
|
|
17
|
+
if (!output)
|
|
18
|
+
return null;
|
|
19
|
+
// 1. Check for structuredContent (OpenAI / NitroStack default)
|
|
20
|
+
if (output.structuredContent) {
|
|
21
|
+
return output.structuredContent;
|
|
22
|
+
}
|
|
23
|
+
// 2. Check for standard MCP content array
|
|
24
|
+
const contents = output.content || output.contents;
|
|
25
|
+
if (Array.isArray(contents)) {
|
|
26
|
+
// Look for JSON content first
|
|
27
|
+
const jsonContent = contents.find((c) => c && typeof c === 'object' && (c.mimeType === 'application/json' || c.type === 'json'));
|
|
28
|
+
if (jsonContent) {
|
|
29
|
+
try {
|
|
30
|
+
return (typeof jsonContent.text === 'string' ? JSON.parse(jsonContent.text) : jsonContent.text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// fall through
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Look for text content that might be JSON
|
|
37
|
+
const textContent = contents.find((c) => c && typeof c === 'object' && (c.mimeType === 'text/plain' || c.type === 'text'));
|
|
38
|
+
if (textContent && typeof textContent.text === 'string') {
|
|
39
|
+
try {
|
|
40
|
+
// Only try parsing if it looks like JSON
|
|
41
|
+
if (textContent.text.trim().startsWith('{') || textContent.text.trim().startsWith('[')) {
|
|
42
|
+
return JSON.parse(textContent.text);
|
|
43
|
+
}
|
|
44
|
+
return textContent.text;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return textContent.text;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 3. Fallback to raw output
|
|
52
|
+
return output;
|
|
53
|
+
};
|
|
15
54
|
// Function to check for data
|
|
16
55
|
const checkForData = () => {
|
|
17
56
|
try {
|
|
18
57
|
if (typeof window !== 'undefined') {
|
|
19
58
|
const openai = window.openai;
|
|
20
|
-
if (openai && openai.toolOutput) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
59
|
+
if (openai && (openai.toolOutput || openai.toolInput)) {
|
|
60
|
+
const data = extractToolData(openai.toolOutput);
|
|
61
|
+
if (data) {
|
|
62
|
+
setState({
|
|
63
|
+
data: data,
|
|
64
|
+
loading: false,
|
|
65
|
+
error: null,
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
27
69
|
}
|
|
28
70
|
}
|
|
29
71
|
}
|
|
@@ -47,8 +89,9 @@ export function withToolData(WrappedComponent) {
|
|
|
47
89
|
console.log('[Widget] Received postMessage:', event.data);
|
|
48
90
|
if (event.data && event.data.type === 'toolOutput') {
|
|
49
91
|
console.log('[Widget] Setting data:', event.data.data);
|
|
92
|
+
const data = extractToolData(event.data.data);
|
|
50
93
|
setState({
|
|
51
|
-
data:
|
|
94
|
+
data: data,
|
|
52
95
|
loading: false,
|
|
53
96
|
error: null,
|
|
54
97
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitrostack/widgets",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Widget utilities for NitroStack - Build interactive UI widgets for MCP tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"author": "Nitrostack Inc <hello@nitrostack.ai>",
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"react": "^18.0.0 || ^19.0.0"
|
|
40
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
41
|
+
"@modelcontextprotocol/ext-apps": ">=0.1.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@testing-library/jest-dom": "^6.9.1",
|