@ai-sdk/react 4.0.0-canary.125 → 4.0.0-canary.127

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.
@@ -0,0 +1,405 @@
1
+ import { isJSONObject } from '@ai-sdk/provider';
2
+ import type {
3
+ MCPAppBridgeHandlers,
4
+ MCPAppHostContext,
5
+ MCPAppJsonRpcMessage,
6
+ MCPAppJsonRpcNotification,
7
+ MCPAppJsonRpcRequest,
8
+ MCPAppJsonRpcResponse,
9
+ MCPAppToolCallParams,
10
+ } from './types';
11
+
12
+ const MCP_APP_PROTOCOL_VERSION = '2026-01-26';
13
+
14
+ /**
15
+ * Checks whether an iframe message looks like a JSON-RPC 2.0 message.
16
+ */
17
+ function isJsonRpcMessage(value: unknown): value is MCPAppJsonRpcMessage {
18
+ return (
19
+ value != null &&
20
+ typeof value === 'object' &&
21
+ !Array.isArray(value) &&
22
+ 'jsonrpc' in value &&
23
+ value.jsonrpc === '2.0'
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Checks whether a JSON-RPC message expects a response.
29
+ */
30
+ function isRequest(
31
+ message: MCPAppJsonRpcMessage,
32
+ ): message is MCPAppJsonRpcRequest {
33
+ return 'method' in message && 'id' in message;
34
+ }
35
+
36
+ /**
37
+ * Checks whether a JSON-RPC message is a fire-and-forget notification.
38
+ */
39
+ function isNotification(
40
+ message: MCPAppJsonRpcMessage,
41
+ ): message is MCPAppJsonRpcNotification {
42
+ return 'method' in message && !('id' in message);
43
+ }
44
+
45
+ /**
46
+ * Normalizes unknown thrown values into an `Error`.
47
+ */
48
+ function toError(error: unknown): Error {
49
+ return error instanceof Error ? error : new Error(String(error));
50
+ }
51
+
52
+ /**
53
+ * Validates the params for app-initiated `tools/call` requests.
54
+ */
55
+ function assertToolCallParams(params: unknown): MCPAppToolCallParams {
56
+ if (!isJSONObject(params) || typeof params.name !== 'string') {
57
+ throw new Error('Invalid tools/call params');
58
+ }
59
+
60
+ return {
61
+ name: params.name,
62
+ arguments: isJSONObject(params.arguments) ? params.arguments : undefined,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Host-side JSON-RPC bridge for one MCP App iframe.
68
+ *
69
+ * It handles the MCP Apps initialization handshake, sends tool input/result
70
+ * notifications to the iframe, and proxies allowed iframe requests through
71
+ * host-provided callbacks.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const bridge = new MCPAppBridge({
76
+ * targetWindow: iframe.contentWindow!,
77
+ * handlers: {
78
+ * allowedTools: ['refreshDashboardData'],
79
+ * callTool: params => client.callTool(params),
80
+ * },
81
+ * });
82
+ *
83
+ * window.addEventListener('message', event => bridge.handleMessage(event));
84
+ * bridge.sendToolInput({ topic: 'usage' });
85
+ * ```
86
+ */
87
+ export class MCPAppBridge {
88
+ private initialized = false;
89
+ private pendingNotifications: MCPAppJsonRpcNotification[] = [];
90
+ private nextRequestId = 0;
91
+ private pendingResponses = new Map<
92
+ string | number,
93
+ {
94
+ resolve: (value: unknown) => void;
95
+ reject: (error: Error) => void;
96
+ }
97
+ >();
98
+
99
+ constructor({
100
+ targetWindow,
101
+ targetOrigin = '*',
102
+ handlers = {},
103
+ hostInfo = { name: 'ai-sdk-react', version: '1.0.0' },
104
+ hostContext = { displayMode: 'inline' },
105
+ }: {
106
+ targetWindow: Window;
107
+ targetOrigin?: string;
108
+ handlers?: MCPAppBridgeHandlers;
109
+ hostInfo?: { name: string; version: string };
110
+ hostContext?: MCPAppHostContext;
111
+ }) {
112
+ this.targetWindow = targetWindow;
113
+ this.targetOrigin = targetOrigin;
114
+ this.handlers = handlers;
115
+ this.hostInfo = hostInfo;
116
+ this.hostContext = hostContext;
117
+ }
118
+
119
+ private targetWindow: Window;
120
+ private targetOrigin: string;
121
+ private handlers: MCPAppBridgeHandlers;
122
+ private hostInfo: { name: string; version: string };
123
+ private hostContext: MCPAppHostContext;
124
+
125
+ /**
126
+ * Replaces the callbacks used to serve iframe requests.
127
+ */
128
+ setHandlers(handlers: MCPAppBridgeHandlers): void {
129
+ this.handlers = handlers;
130
+ }
131
+
132
+ /**
133
+ * Updates host context and notifies the iframe after initialization.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * bridge.setHostContext({ theme: 'dark', displayMode: 'inline' });
138
+ * ```
139
+ */
140
+ setHostContext(hostContext: MCPAppHostContext): void {
141
+ this.hostContext = hostContext;
142
+ this.sendNotification({
143
+ method: 'ui/notifications/host-context-changed',
144
+ params: hostContext,
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Processes one `message` event from the sandbox proxy iframe.
150
+ */
151
+ handleMessage(event: MessageEvent): void {
152
+ if (event.source !== this.targetWindow || !isJsonRpcMessage(event.data)) {
153
+ return;
154
+ }
155
+
156
+ const message = event.data;
157
+
158
+ if ('result' in message || 'error' in message) {
159
+ this.handleResponse(message);
160
+ return;
161
+ }
162
+
163
+ if (isRequest(message)) {
164
+ void this.handleRequest(message);
165
+ return;
166
+ }
167
+
168
+ if (isNotification(message)) {
169
+ this.handleNotification(message);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Sends app HTML and sandbox settings to the sandbox proxy.
175
+ */
176
+ sendSandboxResourceReady(params: unknown): void {
177
+ this.post({
178
+ jsonrpc: '2.0',
179
+ method: 'ui/notifications/sandbox-resource-ready',
180
+ params,
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Sends final tool arguments to the MCP App.
186
+ */
187
+ sendToolInput(input: unknown): void {
188
+ this.sendNotification({
189
+ method: 'ui/notifications/tool-input',
190
+ params: { arguments: input },
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Sends a completed MCP tool result to the MCP App.
196
+ */
197
+ sendToolResult(result: unknown): void {
198
+ this.sendNotification({
199
+ method: 'ui/notifications/tool-result',
200
+ params: result,
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Notifies the MCP App that the related tool call was cancelled.
206
+ */
207
+ sendToolCancelled(reason?: string): void {
208
+ this.sendNotification({
209
+ method: 'ui/notifications/tool-cancelled',
210
+ params: reason != null ? { reason } : {},
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Requests graceful teardown before the host removes the iframe.
216
+ */
217
+ teardownResource(): Promise<unknown> {
218
+ return this.request('ui/resource-teardown', {});
219
+ }
220
+
221
+ /**
222
+ * Rejects pending bridge requests and clears queued notifications.
223
+ */
224
+ close(): void {
225
+ for (const pending of this.pendingResponses.values()) {
226
+ pending.reject(new Error('MCP App bridge closed'));
227
+ }
228
+ this.pendingResponses.clear();
229
+ this.pendingNotifications = [];
230
+ }
231
+
232
+ /**
233
+ * Resolves or rejects a host-initiated request when the iframe responds.
234
+ */
235
+ private handleResponse(response: MCPAppJsonRpcResponse): void {
236
+ const pending = this.pendingResponses.get(response.id);
237
+ if (pending == null) {
238
+ return;
239
+ }
240
+
241
+ this.pendingResponses.delete(response.id);
242
+
243
+ if (response.error != null) {
244
+ pending.reject(new Error(response.error.message));
245
+ } else {
246
+ pending.resolve(response.result);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Runs a handler for an iframe request and posts the JSON-RPC response.
252
+ */
253
+ private async handleRequest(request: MCPAppJsonRpcRequest): Promise<void> {
254
+ try {
255
+ const result = await this.getRequestResult(request);
256
+ this.post({ jsonrpc: '2.0', id: request.id, result });
257
+ } catch (error) {
258
+ const normalizedError = toError(error);
259
+ this.handlers.onError?.(normalizedError);
260
+ this.post({
261
+ jsonrpc: '2.0',
262
+ id: request.id,
263
+ error: { code: -32603, message: normalizedError.message },
264
+ });
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Maps supported iframe request methods to host callbacks.
270
+ */
271
+ private async getRequestResult(
272
+ request: MCPAppJsonRpcRequest,
273
+ ): Promise<unknown> {
274
+ switch (request.method) {
275
+ case 'ui/initialize':
276
+ return {
277
+ protocolVersion: MCP_APP_PROTOCOL_VERSION,
278
+ hostCapabilities: {
279
+ ...(this.handlers.callTool != null ? { serverTools: {} } : {}),
280
+ ...(this.handlers.readResource != null
281
+ ? { serverResources: {} }
282
+ : {}),
283
+ ...(this.handlers.onLog != null ? { logging: {} } : {}),
284
+ },
285
+ hostInfo: this.hostInfo,
286
+ hostContext: this.hostContext,
287
+ };
288
+
289
+ case 'tools/call': {
290
+ if (this.handlers.callTool == null) {
291
+ throw new Error('No tools/call handler configured');
292
+ }
293
+ const params = assertToolCallParams(request.params);
294
+ if (
295
+ this.handlers.allowedTools != null &&
296
+ !this.handlers.allowedTools.includes(params.name)
297
+ ) {
298
+ throw new Error(`Tool is not app-visible: ${params.name}`);
299
+ }
300
+ return this.handlers.callTool(params);
301
+ }
302
+
303
+ case 'resources/read':
304
+ if (this.handlers.readResource == null) {
305
+ throw new Error('No resources/read handler configured');
306
+ }
307
+ return this.handlers.readResource(request.params as { uri: string });
308
+
309
+ case 'resources/list':
310
+ if (this.handlers.listResources == null) {
311
+ throw new Error('No resources/list handler configured');
312
+ }
313
+ return this.handlers.listResources(request.params);
314
+
315
+ case 'ui/open-link':
316
+ if (this.handlers.openLink == null) {
317
+ throw new Error('No ui/open-link handler configured');
318
+ }
319
+ return this.handlers.openLink(request.params as { url: string });
320
+
321
+ case 'ui/message':
322
+ return this.handlers.sendMessage?.(request.params) ?? {};
323
+
324
+ case 'ui/update-model-context':
325
+ return this.handlers.updateModelContext?.(request.params) ?? {};
326
+
327
+ case 'ui/request-display-mode':
328
+ return (
329
+ this.handlers.requestDisplayMode?.(
330
+ request.params as { mode: 'inline' | 'fullscreen' | 'pip' },
331
+ ) ?? { mode: this.hostContext.displayMode ?? 'inline' }
332
+ );
333
+
334
+ default:
335
+ throw new Error(`Unsupported MCP App method: ${request.method}`);
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Handles iframe lifecycle and telemetry notifications.
341
+ */
342
+ private handleNotification(notification: MCPAppJsonRpcNotification): void {
343
+ switch (notification.method) {
344
+ case 'ui/notifications/initialized':
345
+ this.initialized = true;
346
+ this.flushNotifications();
347
+ this.handlers.onInitialized?.();
348
+ break;
349
+ case 'ui/notifications/size-changed':
350
+ this.handlers.onSizeChange?.(
351
+ notification.params as { width?: number; height?: number },
352
+ );
353
+ break;
354
+ case 'ui/notifications/request-teardown':
355
+ this.handlers.onRequestTeardown?.(notification.params);
356
+ break;
357
+ case 'notifications/message':
358
+ this.handlers.onLog?.(notification.params);
359
+ break;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Sends a host-to-iframe notification, queueing it until app initialization.
365
+ */
366
+ private sendNotification(
367
+ notification: Omit<MCPAppJsonRpcNotification, 'jsonrpc'>,
368
+ ) {
369
+ const message = { jsonrpc: '2.0' as const, ...notification };
370
+ if (!this.initialized && !notification.method.includes('sandbox')) {
371
+ this.pendingNotifications.push(message);
372
+ return;
373
+ }
374
+ this.post(message);
375
+ }
376
+
377
+ /**
378
+ * Sends notifications that were queued before `ui/notifications/initialized`.
379
+ */
380
+ private flushNotifications(): void {
381
+ const notifications = this.pendingNotifications;
382
+ this.pendingNotifications = [];
383
+ for (const notification of notifications) {
384
+ this.post(notification);
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Sends a host-initiated JSON-RPC request to the iframe.
390
+ */
391
+ private request(method: string, params: unknown): Promise<unknown> {
392
+ const id = this.nextRequestId++;
393
+ this.post({ jsonrpc: '2.0', id, method, params });
394
+ return new Promise((resolve, reject) => {
395
+ this.pendingResponses.set(id, { resolve, reject });
396
+ });
397
+ }
398
+
399
+ /**
400
+ * Posts a JSON-RPC message to the sandbox proxy iframe.
401
+ */
402
+ private post(message: MCPAppJsonRpcMessage): void {
403
+ this.targetWindow.postMessage(message, this.targetOrigin);
404
+ }
405
+ }
@@ -0,0 +1,8 @@
1
+ export { MCPAppRenderer as experimental_MCPAppRenderer } from './app-renderer';
2
+ export type {
3
+ MCPAppBridgeHandlers,
4
+ MCPAppMetadata,
5
+ MCPAppRendererProps,
6
+ MCPAppResource,
7
+ MCPAppSandboxConfig,
8
+ } from './types';
@@ -0,0 +1,74 @@
1
+ import type { MCPAppResourceCSP } from '@ai-sdk/mcp';
2
+
3
+ /**
4
+ * Default sandbox permissions for the outer sandbox proxy iframe.
5
+ */
6
+ export const MCP_APP_DEFAULT_OUTER_SANDBOX =
7
+ 'allow-scripts allow-same-origin allow-forms';
8
+
9
+ /**
10
+ * Default sandbox permissions for the inner iframe that runs app HTML.
11
+ */
12
+ export const MCP_APP_DEFAULT_INNER_SANDBOX = 'allow-scripts allow-forms';
13
+
14
+ /**
15
+ * Converts MCP App CSP metadata into a Content-Security-Policy string.
16
+ *
17
+ * The returned value is meant to be passed to a sandbox proxy, which can apply
18
+ * it to the inner iframe document.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const csp = getMCPAppCSP({
23
+ * connectDomains: ['https://api.example.com'],
24
+ * resourceDomains: ['https://cdn.example.com'],
25
+ * });
26
+ * ```
27
+ */
28
+ export function getMCPAppCSP(csp?: MCPAppResourceCSP): string | undefined {
29
+ if (csp == null) {
30
+ return undefined;
31
+ }
32
+
33
+ const connectSrc = ["'self'", ...(csp.connectDomains ?? [])];
34
+ const imgSrc = ["'self'", 'data:', ...(csp.resourceDomains ?? [])];
35
+ const frameSrc = ["'self'", ...(csp.frameDomains ?? [])];
36
+
37
+ return [
38
+ "default-src 'none'",
39
+ "script-src 'unsafe-inline'",
40
+ "style-src 'unsafe-inline'",
41
+ `connect-src ${connectSrc.join(' ')}`,
42
+ `img-src ${imgSrc.join(' ')}`,
43
+ `font-src ${imgSrc.join(' ')}`,
44
+ `frame-src ${frameSrc.join(' ')}`,
45
+ ].join('; ');
46
+ }
47
+
48
+ /**
49
+ * Converts MCP App permission metadata into an iframe `allow` attribute.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const allow = getMCPAppAllowAttribute({
54
+ * microphone: {},
55
+ * clipboardWrite: {},
56
+ * });
57
+ * // "microphone; clipboard-write"
58
+ * ```
59
+ */
60
+ export function getMCPAppAllowAttribute(
61
+ permissions?: Record<string, unknown>,
62
+ ): string | undefined {
63
+ if (permissions == null) {
64
+ return undefined;
65
+ }
66
+
67
+ const allow = [];
68
+ if (permissions.camera) allow.push('camera');
69
+ if (permissions.microphone) allow.push('microphone');
70
+ if (permissions.geolocation) allow.push('geolocation');
71
+ if (permissions.clipboardWrite) allow.push('clipboard-write');
72
+
73
+ return allow.length > 0 ? allow.join('; ') : undefined;
74
+ }
@@ -0,0 +1,100 @@
1
+ import type { MCPAppResource } from '@ai-sdk/mcp';
2
+ import type { DynamicToolUIPart, ToolUIPart, UITools } from 'ai';
3
+ import type { CSSProperties, ReactNode } from 'react';
4
+
5
+ export type { MCPAppResource };
6
+
7
+ export type MCPAppDisplayMode = 'inline' | 'fullscreen' | 'pip';
8
+
9
+ export type MCPAppMetadata = {
10
+ resourceUri: string;
11
+ mimeType: MCPAppResource['mimeType'];
12
+ visibility?: Array<'model' | 'app'>;
13
+ };
14
+
15
+ export type MCPAppHostContext = {
16
+ theme?: 'light' | 'dark';
17
+ displayMode?: MCPAppDisplayMode;
18
+ availableDisplayModes?: MCPAppDisplayMode[];
19
+ [key: string]: unknown;
20
+ };
21
+
22
+ export type MCPAppToolCallParams = {
23
+ name: string;
24
+ arguments?: Record<string, unknown>;
25
+ };
26
+
27
+ export type MCPAppBridgeHandlers = {
28
+ allowedTools?: string[];
29
+ callTool?: (params: MCPAppToolCallParams) => Promise<unknown> | unknown;
30
+ readResource?: (params: { uri: string }) => Promise<unknown> | unknown;
31
+ listResources?: (params?: unknown) => Promise<unknown> | unknown;
32
+ openLink?: (params: { url: string }) => Promise<unknown> | unknown;
33
+ sendMessage?: (params: unknown) => Promise<unknown> | unknown;
34
+ updateModelContext?: (params: unknown) => Promise<unknown> | unknown;
35
+ requestDisplayMode?: (params: {
36
+ mode: MCPAppDisplayMode;
37
+ }) => Promise<{ mode: MCPAppDisplayMode }> | { mode: MCPAppDisplayMode };
38
+ onSizeChange?: (params: { width?: number; height?: number }) => void;
39
+ onInitialized?: () => void;
40
+ onRequestTeardown?: (params: unknown) => void;
41
+ onLog?: (params: unknown) => void;
42
+ onError?: (error: Error) => void;
43
+ };
44
+
45
+ export type MCPAppSandboxConfig = {
46
+ url: string | URL;
47
+ title?: string;
48
+ className?: string;
49
+ style?: CSSProperties;
50
+ targetOrigin?: string;
51
+ outerSandbox?: string;
52
+ innerSandbox?: string;
53
+ };
54
+
55
+ export type MCPAppFrameProps = {
56
+ app: MCPAppMetadata;
57
+ resource: MCPAppResource;
58
+ input?: unknown;
59
+ output?: unknown;
60
+ sandbox: MCPAppSandboxConfig;
61
+ handlers?: MCPAppBridgeHandlers;
62
+ hostInfo?: { name: string; version: string };
63
+ hostContext?: MCPAppHostContext;
64
+ };
65
+
66
+ export type MCPAppRendererProps = {
67
+ part: ToolUIPart<UITools> | DynamicToolUIPart;
68
+ sandbox: MCPAppSandboxConfig;
69
+ resource?: MCPAppResource;
70
+ loadResource?: (app: MCPAppMetadata) => Promise<MCPAppResource>;
71
+ handlers?: MCPAppBridgeHandlers;
72
+ hostInfo?: { name: string; version: string };
73
+ hostContext?: MCPAppHostContext;
74
+ fallback?: ReactNode;
75
+ };
76
+
77
+ export type MCPAppJsonRpcRequest = {
78
+ jsonrpc: '2.0';
79
+ id: string | number;
80
+ method: string;
81
+ params?: unknown;
82
+ };
83
+
84
+ export type MCPAppJsonRpcNotification = {
85
+ jsonrpc: '2.0';
86
+ method: string;
87
+ params?: unknown;
88
+ };
89
+
90
+ export type MCPAppJsonRpcResponse = {
91
+ jsonrpc: '2.0';
92
+ id: string | number;
93
+ result?: unknown;
94
+ error?: { code: number; message: string; data?: unknown };
95
+ };
96
+
97
+ export type MCPAppJsonRpcMessage =
98
+ | MCPAppJsonRpcRequest
99
+ | MCPAppJsonRpcNotification
100
+ | MCPAppJsonRpcResponse;
@@ -0,0 +1,84 @@
1
+ import { isJSONObject } from '@ai-sdk/provider';
2
+ import type { MCPAppMetadata, MCPAppRendererProps } from './types';
3
+
4
+ /**
5
+ * Extracts MCP App metadata from an AI SDK tool UI part.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const app = getMCPAppFromToolPart({
10
+ * type: 'dynamic-tool',
11
+ * toolName: 'showDashboard',
12
+ * toolCallId: 'call-1',
13
+ * state: 'input-available',
14
+ * input: { topic: 'usage' },
15
+ * toolMetadata: {
16
+ * mcp: {
17
+ * app: {
18
+ * resourceUri: 'ui://example/dashboard',
19
+ * mimeType: 'text/html;profile=mcp-app',
20
+ * },
21
+ * },
22
+ * },
23
+ * });
24
+ * // { resourceUri: 'ui://example/dashboard', mimeType: 'text/html;profile=mcp-app' }
25
+ * ```
26
+ */
27
+ export function getMCPAppFromToolPart(
28
+ part: MCPAppRendererProps['part'],
29
+ ): MCPAppMetadata | undefined {
30
+ const mcpMetadata = part.toolMetadata?.mcp;
31
+ const rawAppMetadata = isJSONObject(mcpMetadata)
32
+ ? mcpMetadata.app
33
+ : undefined;
34
+ const appMetadata = isJSONObject(rawAppMetadata) ? rawAppMetadata : undefined;
35
+
36
+ if (
37
+ appMetadata == null ||
38
+ appMetadata.mimeType !== 'text/html;profile=mcp-app' ||
39
+ typeof appMetadata.resourceUri !== 'string' ||
40
+ !appMetadata.resourceUri.startsWith('ui://') ||
41
+ (appMetadata.visibility != null &&
42
+ (!Array.isArray(appMetadata.visibility) ||
43
+ appMetadata.visibility.some(
44
+ value => value !== 'model' && value !== 'app',
45
+ )))
46
+ ) {
47
+ return undefined;
48
+ }
49
+
50
+ return appMetadata as MCPAppMetadata;
51
+ }
52
+
53
+ /**
54
+ * Converts an AI SDK tool output into the MCP Apps tool-result shape.
55
+ *
56
+ * MCP Apps expect tool results to have `content` and optional
57
+ * `structuredContent`. MCP tools already return that shape, but typed AI SDK
58
+ * tools may return only structured data. This helper wraps structured-only
59
+ * outputs so the iframe can receive a valid tool result notification.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * normalizeMCPAppToolResult({ cards: [] });
64
+ * // { content: [], structuredContent: { cards: [] } }
65
+ * ```
66
+ */
67
+ export function normalizeMCPAppToolResult(output: unknown): {
68
+ content: unknown[];
69
+ structuredContent?: unknown;
70
+ isError?: boolean;
71
+ } {
72
+ if (output != null && typeof output === 'object' && 'content' in output) {
73
+ return output as {
74
+ content: unknown[];
75
+ structuredContent?: unknown;
76
+ isError?: boolean;
77
+ };
78
+ }
79
+
80
+ return {
81
+ content: [],
82
+ structuredContent: output,
83
+ };
84
+ }