@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 CHANGED
@@ -1,89 +1,67 @@
1
- # @nitrostack/widgets
1
+ # @nitrostack/widgets
2
2
 
3
- **The visual SDK for NitroStack Build interactive, data-rich UI outputs for your MCP tools.**
3
+ React SDK for building interactive widget UIs that render with NitroStack MCP
4
+ tool outputs.
4
5
 
5
- [![Widgets SDK](https://img.shields.io/badge/NitroStack-Widgets-brightgreen?style=for-the-badge&logo=none)](https://nitrostack.ai)
6
- [![License](https://img.shields.io/badge/License-Apache%202.0-red?style=for-the-badge&logo=none)](https://opensource.org/licenses/Apache-2.0)
7
- [![Documentation](https://img.shields.io/badge/Docs-docs.nitrostack.ai-blue?style=for-the-badge&logo=none)](https://docs.nitrostack.ai)
6
+ [![npm version](https://img.shields.io/npm/v/@nitrostack/widgets?style=flat-square)](https://www.npmjs.com/package/@nitrostack/widgets)
7
+ [![npm downloads](https://img.shields.io/npm/dm/@nitrostack/widgets?style=flat-square)](https://www.npmjs.com/package/@nitrostack/widgets)
8
+ [![license](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
8
9
 
9
- `@nitrostack/widgets` allows you to go beyond plain text tool outputs. Use the power of React to build beautiful, interactive components (Widgets) that render directly in supported MCP clients and NitroStudio.
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
- ```typescript
36
- // On the Server
37
- @Tool({ name: 'get_user_profile' })
38
- @Widget({
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
- Then create the React component:
23
+ ## Quick Example
49
24
 
50
25
  ```tsx
51
- // In your Widgets project
52
- import { useWidgetData } from '@nitrostack/widgets';
26
+ 'use client';
53
27
 
54
- export const UserCard = () => {
55
- const { data, loading } = useWidgetData<UserData>();
28
+ import { useWidgetSDK } from '@nitrostack/widgets';
56
29
 
57
- if (loading) return <div>Loading...</div>;
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 className="p-4 border rounded">
37
+ <div>
61
38
  <h3>{data.name}</h3>
62
- <p>{data.role}</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
- ![NitroStudio](https://raw.githubusercontent.com/nitrocloudofficial/nitrostack/main/typescript/packages/widgets/assets/gif/nitrostudio-main.gif)
45
+ ## NitroStudio
75
46
 
76
- **[Download NitroStudio](https://nitrostack.ai/studio)**
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
- ## 📖 Useful Links
53
+ ## Links
81
54
 
82
- - **[GitHub Repository](https://github.com/nitrocloudofficial/nitrostack)**
83
- - **[Documentation](https://docs.nitrostack.ai)**
84
- - **[Blog & Tutorials](https://blog.nitrostack.ai)**
85
- - **[NPM Package](https://www.npmjs.com/package/@nitrostack/widgets)**
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
- Built with ⚡ by the **NitroStack Team**.
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;;OAEG;IACH,aAAa,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,IAAI;IAKtC;;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"}
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
- return window.openai.toolOutput;
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,iDAkNnD"}
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"}
@@ -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
- setState({
22
- data: openai.toolOutput,
23
- loading: false,
24
- error: null,
25
- });
26
- return true;
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: event.data.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",
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",