@ai-sdk/react
Advanced tools
| import { useEffect, useMemo, useRef } from 'react'; | ||
| import { MCPAppBridge } from './bridge'; | ||
| import { | ||
| MCP_APP_DEFAULT_INNER_SANDBOX, | ||
| MCP_APP_DEFAULT_OUTER_SANDBOX, | ||
| getMCPAppAllowAttribute, | ||
| getMCPAppCSP, | ||
| } from './sandbox'; | ||
| import type { MCPAppFrameProps } from './types'; | ||
| import { normalizeMCPAppToolResult } from './utils'; | ||
| function sendToolState({ | ||
| bridge, | ||
| input, | ||
| output, | ||
| }: { | ||
| bridge?: MCPAppBridge; | ||
| input: unknown; | ||
| output: unknown; | ||
| }) { | ||
| if (bridge == null) { | ||
| return; | ||
| } | ||
| if (input !== undefined) { | ||
| bridge.sendToolInput(input); | ||
| } | ||
| if (output !== undefined) { | ||
| bridge.sendToolResult(normalizeMCPAppToolResult(output)); | ||
| } | ||
| } | ||
| export function MCPAppFrame({ | ||
| app, | ||
| resource, | ||
| input, | ||
| output, | ||
| sandbox, | ||
| handlers, | ||
| hostInfo, | ||
| hostContext, | ||
| }: MCPAppFrameProps) { | ||
| const iframeRef = useRef<HTMLIFrameElement>(null); | ||
| const bridgeRef = useRef<MCPAppBridge | undefined>(undefined); | ||
| const inputRef = useRef(input); | ||
| const outputRef = useRef(output); | ||
| const hostContextRef = useRef(hostContext); | ||
| const initializedRef = useRef(false); | ||
| inputRef.current = input; | ||
| outputRef.current = output; | ||
| hostContextRef.current = hostContext; | ||
| const targetOrigin = sandbox.targetOrigin ?? '*'; | ||
| const sandboxUrl = String(sandbox.url); | ||
| const resourceCSP = getMCPAppCSP(resource.meta?.csp); | ||
| const resourceAllow = getMCPAppAllowAttribute(resource.meta?.permissions); | ||
| const innerSandbox = sandbox.innerSandbox ?? MCP_APP_DEFAULT_INNER_SANDBOX; | ||
| const bridgeHandlers = useMemo( | ||
| () => ({ | ||
| ...handlers, | ||
| onInitialized: () => { | ||
| initializedRef.current = true; | ||
| handlers?.onInitialized?.(); | ||
| sendToolState({ | ||
| bridge: bridgeRef.current, | ||
| input: inputRef.current, | ||
| output: outputRef.current, | ||
| }); | ||
| }, | ||
| }), | ||
| [handlers], | ||
| ); | ||
| const bridgeHandlersRef = useRef(bridgeHandlers); | ||
| bridgeHandlersRef.current = bridgeHandlers; | ||
| useEffect(() => { | ||
| const iframe = iframeRef.current; | ||
| const targetWindow = iframe?.contentWindow; | ||
| if (targetWindow == null) { | ||
| return; | ||
| } | ||
| initializedRef.current = false; | ||
| const bridge = new MCPAppBridge({ | ||
| targetWindow, | ||
| targetOrigin, | ||
| handlers: bridgeHandlersRef.current, | ||
| hostInfo, | ||
| hostContext: hostContextRef.current, | ||
| }); | ||
| bridgeRef.current = bridge; | ||
| const onMessage = (event: MessageEvent) => { | ||
| if ( | ||
| event.source === targetWindow && | ||
| event.data?.jsonrpc === '2.0' && | ||
| event.data.method === 'ui/notifications/sandbox-proxy-ready' | ||
| ) { | ||
| bridge.sendSandboxResourceReady({ | ||
| html: resource.html, | ||
| csp: resourceCSP, | ||
| sandbox: innerSandbox, | ||
| allow: resourceAllow, | ||
| }); | ||
| return; | ||
| } | ||
| bridge.handleMessage(event); | ||
| }; | ||
| window.addEventListener('message', onMessage); | ||
| return () => { | ||
| initializedRef.current = false; | ||
| window.removeEventListener('message', onMessage); | ||
| void bridge.teardownResource().catch(() => {}); | ||
| bridge.close(); | ||
| bridgeRef.current = undefined; | ||
| }; | ||
| }, [ | ||
| hostInfo, | ||
| innerSandbox, | ||
| resource.html, | ||
| resourceAllow, | ||
| resourceCSP, | ||
| sandboxUrl, | ||
| targetOrigin, | ||
| ]); | ||
| useEffect(() => { | ||
| bridgeRef.current?.setHandlers(bridgeHandlers); | ||
| }, [bridgeHandlers]); | ||
| useEffect(() => { | ||
| if (hostContext != null) { | ||
| bridgeRef.current?.setHostContext(hostContext); | ||
| } | ||
| }, [hostContext]); | ||
| useEffect(() => { | ||
| if (initializedRef.current && input !== undefined) { | ||
| bridgeRef.current?.sendToolInput(input); | ||
| } | ||
| }, [input]); | ||
| useEffect(() => { | ||
| if (initializedRef.current && output !== undefined) { | ||
| bridgeRef.current?.sendToolResult(normalizeMCPAppToolResult(output)); | ||
| } | ||
| }, [output]); | ||
| return ( | ||
| <iframe | ||
| ref={iframeRef} | ||
| title="MCP App" | ||
| aria-label={sandbox.title ?? app.resourceUri} | ||
| src={sandboxUrl} | ||
| className={sandbox.className} | ||
| style={sandbox.style} | ||
| sandbox={sandbox.outerSandbox ?? MCP_APP_DEFAULT_OUTER_SANDBOX} | ||
| /> | ||
| ); | ||
| } |
| import type { MCPAppResource } from '@ai-sdk/mcp'; | ||
| import { useEffect, useState } from 'react'; | ||
| import { MCPAppFrame } from './app-frame'; | ||
| import type { MCPAppMetadata, MCPAppRendererProps } from './types'; | ||
| import { getMCPAppFromToolPart } from './utils'; | ||
| type LoadedResourceState = { | ||
| resourceUri: string; | ||
| resource?: MCPAppResource; | ||
| error?: Error; | ||
| }; | ||
| function getToolPartOutput(part: MCPAppRendererProps['part']): unknown { | ||
| return part.state === 'output-available' ? part.output : undefined; | ||
| } | ||
| function getToolPartInput(part: MCPAppRendererProps['part']): unknown { | ||
| return part.state === 'input-available' || part.state === 'output-available' | ||
| ? part.input | ||
| : undefined; | ||
| } | ||
| export function MCPAppRenderer({ | ||
| part, | ||
| sandbox, | ||
| resource: resourceProp, | ||
| loadResource, | ||
| handlers, | ||
| hostInfo, | ||
| hostContext, | ||
| fallback = null, | ||
| }: MCPAppRendererProps) { | ||
| const app = getMCPAppFromToolPart(part); | ||
| const [cachedApp, setCachedApp] = useState<MCPAppMetadata>(); | ||
| const [loadedResource, setLoadedResource] = useState<LoadedResourceState>(); | ||
| useEffect(() => { | ||
| if (app != null) { | ||
| setCachedApp(previous => | ||
| previous?.resourceUri === app.resourceUri ? previous : app, | ||
| ); | ||
| } | ||
| }, [app?.resourceUri]); | ||
| const appForRender = app ?? cachedApp; | ||
| useEffect(() => { | ||
| if (appForRender == null || resourceProp != null || loadResource == null) { | ||
| return; | ||
| } | ||
| let cancelled = false; | ||
| const resourceUri = appForRender.resourceUri; | ||
| loadResource(appForRender) | ||
| .then(resource => { | ||
| if (!cancelled) { | ||
| setLoadedResource({ resourceUri, resource }); | ||
| } | ||
| }) | ||
| .catch(error => { | ||
| if (!cancelled) { | ||
| setLoadedResource({ | ||
| resourceUri, | ||
| error: error instanceof Error ? error : new Error(String(error)), | ||
| }); | ||
| } | ||
| }); | ||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [appForRender?.resourceUri, loadResource, resourceProp]); | ||
| const loadedResourceForApp = | ||
| loadedResource?.resourceUri === appForRender?.resourceUri | ||
| ? loadedResource | ||
| : undefined; | ||
| const resource = resourceProp ?? loadedResourceForApp?.resource; | ||
| const error = resourceProp == null ? loadedResourceForApp?.error : undefined; | ||
| if (appForRender == null || error != null || resource == null) { | ||
| return fallback; | ||
| } | ||
| return ( | ||
| <MCPAppFrame | ||
| app={appForRender} | ||
| resource={resource} | ||
| input={getToolPartInput(part)} | ||
| output={getToolPartOutput(part)} | ||
| sandbox={sandbox} | ||
| handlers={handlers} | ||
| hostInfo={hostInfo} | ||
| hostContext={hostContext} | ||
| /> | ||
| ); | ||
| } |
| import { isJSONObject } from '@ai-sdk/provider'; | ||
| import type { | ||
| MCPAppBridgeHandlers, | ||
| MCPAppHostContext, | ||
| MCPAppJsonRpcMessage, | ||
| MCPAppJsonRpcNotification, | ||
| MCPAppJsonRpcRequest, | ||
| MCPAppJsonRpcResponse, | ||
| MCPAppToolCallParams, | ||
| } from './types'; | ||
| const MCP_APP_PROTOCOL_VERSION = '2026-01-26'; | ||
| /** | ||
| * Checks whether an iframe message looks like a JSON-RPC 2.0 message. | ||
| */ | ||
| function isJsonRpcMessage(value: unknown): value is MCPAppJsonRpcMessage { | ||
| return ( | ||
| value != null && | ||
| typeof value === 'object' && | ||
| !Array.isArray(value) && | ||
| 'jsonrpc' in value && | ||
| value.jsonrpc === '2.0' | ||
| ); | ||
| } | ||
| /** | ||
| * Checks whether a JSON-RPC message expects a response. | ||
| */ | ||
| function isRequest( | ||
| message: MCPAppJsonRpcMessage, | ||
| ): message is MCPAppJsonRpcRequest { | ||
| return 'method' in message && 'id' in message; | ||
| } | ||
| /** | ||
| * Checks whether a JSON-RPC message is a fire-and-forget notification. | ||
| */ | ||
| function isNotification( | ||
| message: MCPAppJsonRpcMessage, | ||
| ): message is MCPAppJsonRpcNotification { | ||
| return 'method' in message && !('id' in message); | ||
| } | ||
| /** | ||
| * Normalizes unknown thrown values into an `Error`. | ||
| */ | ||
| function toError(error: unknown): Error { | ||
| return error instanceof Error ? error : new Error(String(error)); | ||
| } | ||
| /** | ||
| * Validates the params for app-initiated `tools/call` requests. | ||
| */ | ||
| function assertToolCallParams(params: unknown): MCPAppToolCallParams { | ||
| if (!isJSONObject(params) || typeof params.name !== 'string') { | ||
| throw new Error('Invalid tools/call params'); | ||
| } | ||
| return { | ||
| name: params.name, | ||
| arguments: isJSONObject(params.arguments) ? params.arguments : undefined, | ||
| }; | ||
| } | ||
| /** | ||
| * Host-side JSON-RPC bridge for one MCP App iframe. | ||
| * | ||
| * It handles the MCP Apps initialization handshake, sends tool input/result | ||
| * notifications to the iframe, and proxies allowed iframe requests through | ||
| * host-provided callbacks. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const bridge = new MCPAppBridge({ | ||
| * targetWindow: iframe.contentWindow!, | ||
| * handlers: { | ||
| * allowedTools: ['refreshDashboardData'], | ||
| * callTool: params => client.callTool(params), | ||
| * }, | ||
| * }); | ||
| * | ||
| * window.addEventListener('message', event => bridge.handleMessage(event)); | ||
| * bridge.sendToolInput({ topic: 'usage' }); | ||
| * ``` | ||
| */ | ||
| export class MCPAppBridge { | ||
| private initialized = false; | ||
| private pendingNotifications: MCPAppJsonRpcNotification[] = []; | ||
| private nextRequestId = 0; | ||
| private pendingResponses = new Map< | ||
| string | number, | ||
| { | ||
| resolve: (value: unknown) => void; | ||
| reject: (error: Error) => void; | ||
| } | ||
| >(); | ||
| constructor({ | ||
| targetWindow, | ||
| targetOrigin = '*', | ||
| handlers = {}, | ||
| hostInfo = { name: 'ai-sdk-react', version: '1.0.0' }, | ||
| hostContext = { displayMode: 'inline' }, | ||
| }: { | ||
| targetWindow: Window; | ||
| targetOrigin?: string; | ||
| handlers?: MCPAppBridgeHandlers; | ||
| hostInfo?: { name: string; version: string }; | ||
| hostContext?: MCPAppHostContext; | ||
| }) { | ||
| this.targetWindow = targetWindow; | ||
| this.targetOrigin = targetOrigin; | ||
| this.handlers = handlers; | ||
| this.hostInfo = hostInfo; | ||
| this.hostContext = hostContext; | ||
| } | ||
| private targetWindow: Window; | ||
| private targetOrigin: string; | ||
| private handlers: MCPAppBridgeHandlers; | ||
| private hostInfo: { name: string; version: string }; | ||
| private hostContext: MCPAppHostContext; | ||
| /** | ||
| * Replaces the callbacks used to serve iframe requests. | ||
| */ | ||
| setHandlers(handlers: MCPAppBridgeHandlers): void { | ||
| this.handlers = handlers; | ||
| } | ||
| /** | ||
| * Updates host context and notifies the iframe after initialization. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * bridge.setHostContext({ theme: 'dark', displayMode: 'inline' }); | ||
| * ``` | ||
| */ | ||
| setHostContext(hostContext: MCPAppHostContext): void { | ||
| this.hostContext = hostContext; | ||
| this.sendNotification({ | ||
| method: 'ui/notifications/host-context-changed', | ||
| params: hostContext, | ||
| }); | ||
| } | ||
| /** | ||
| * Processes one `message` event from the sandbox proxy iframe. | ||
| */ | ||
| handleMessage(event: MessageEvent): void { | ||
| if (event.source !== this.targetWindow || !isJsonRpcMessage(event.data)) { | ||
| return; | ||
| } | ||
| const message = event.data; | ||
| if ('result' in message || 'error' in message) { | ||
| this.handleResponse(message); | ||
| return; | ||
| } | ||
| if (isRequest(message)) { | ||
| void this.handleRequest(message); | ||
| return; | ||
| } | ||
| if (isNotification(message)) { | ||
| this.handleNotification(message); | ||
| } | ||
| } | ||
| /** | ||
| * Sends app HTML and sandbox settings to the sandbox proxy. | ||
| */ | ||
| sendSandboxResourceReady(params: unknown): void { | ||
| this.post({ | ||
| jsonrpc: '2.0', | ||
| method: 'ui/notifications/sandbox-resource-ready', | ||
| params, | ||
| }); | ||
| } | ||
| /** | ||
| * Sends final tool arguments to the MCP App. | ||
| */ | ||
| sendToolInput(input: unknown): void { | ||
| this.sendNotification({ | ||
| method: 'ui/notifications/tool-input', | ||
| params: { arguments: input }, | ||
| }); | ||
| } | ||
| /** | ||
| * Sends a completed MCP tool result to the MCP App. | ||
| */ | ||
| sendToolResult(result: unknown): void { | ||
| this.sendNotification({ | ||
| method: 'ui/notifications/tool-result', | ||
| params: result, | ||
| }); | ||
| } | ||
| /** | ||
| * Notifies the MCP App that the related tool call was cancelled. | ||
| */ | ||
| sendToolCancelled(reason?: string): void { | ||
| this.sendNotification({ | ||
| method: 'ui/notifications/tool-cancelled', | ||
| params: reason != null ? { reason } : {}, | ||
| }); | ||
| } | ||
| /** | ||
| * Requests graceful teardown before the host removes the iframe. | ||
| */ | ||
| teardownResource(): Promise<unknown> { | ||
| return this.request('ui/resource-teardown', {}); | ||
| } | ||
| /** | ||
| * Rejects pending bridge requests and clears queued notifications. | ||
| */ | ||
| close(): void { | ||
| for (const pending of this.pendingResponses.values()) { | ||
| pending.reject(new Error('MCP App bridge closed')); | ||
| } | ||
| this.pendingResponses.clear(); | ||
| this.pendingNotifications = []; | ||
| } | ||
| /** | ||
| * Resolves or rejects a host-initiated request when the iframe responds. | ||
| */ | ||
| private handleResponse(response: MCPAppJsonRpcResponse): void { | ||
| const pending = this.pendingResponses.get(response.id); | ||
| if (pending == null) { | ||
| return; | ||
| } | ||
| this.pendingResponses.delete(response.id); | ||
| if (response.error != null) { | ||
| pending.reject(new Error(response.error.message)); | ||
| } else { | ||
| pending.resolve(response.result); | ||
| } | ||
| } | ||
| /** | ||
| * Runs a handler for an iframe request and posts the JSON-RPC response. | ||
| */ | ||
| private async handleRequest(request: MCPAppJsonRpcRequest): Promise<void> { | ||
| try { | ||
| const result = await this.getRequestResult(request); | ||
| this.post({ jsonrpc: '2.0', id: request.id, result }); | ||
| } catch (error) { | ||
| const normalizedError = toError(error); | ||
| this.handlers.onError?.(normalizedError); | ||
| this.post({ | ||
| jsonrpc: '2.0', | ||
| id: request.id, | ||
| error: { code: -32603, message: normalizedError.message }, | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Maps supported iframe request methods to host callbacks. | ||
| */ | ||
| private async getRequestResult( | ||
| request: MCPAppJsonRpcRequest, | ||
| ): Promise<unknown> { | ||
| switch (request.method) { | ||
| case 'ui/initialize': | ||
| return { | ||
| protocolVersion: MCP_APP_PROTOCOL_VERSION, | ||
| hostCapabilities: { | ||
| ...(this.handlers.callTool != null ? { serverTools: {} } : {}), | ||
| ...(this.handlers.readResource != null | ||
| ? { serverResources: {} } | ||
| : {}), | ||
| ...(this.handlers.onLog != null ? { logging: {} } : {}), | ||
| }, | ||
| hostInfo: this.hostInfo, | ||
| hostContext: this.hostContext, | ||
| }; | ||
| case 'tools/call': { | ||
| if (this.handlers.callTool == null) { | ||
| throw new Error('No tools/call handler configured'); | ||
| } | ||
| const params = assertToolCallParams(request.params); | ||
| // Deny-by-default: the (untrusted) MCP App may only invoke tools the | ||
| // host has explicitly allow-listed. Omitting `allowedTools` exposes no | ||
| // tools, rather than forwarding every requested tool to `callTool`. | ||
| if ( | ||
| this.handlers.allowedTools == null || | ||
| !this.handlers.allowedTools.includes(params.name) | ||
| ) { | ||
| throw new Error(`Tool is not app-visible: ${params.name}`); | ||
| } | ||
| return this.handlers.callTool(params); | ||
| } | ||
| case 'resources/read': | ||
| if (this.handlers.readResource == null) { | ||
| throw new Error('No resources/read handler configured'); | ||
| } | ||
| return this.handlers.readResource(request.params as { uri: string }); | ||
| case 'resources/list': | ||
| if (this.handlers.listResources == null) { | ||
| throw new Error('No resources/list handler configured'); | ||
| } | ||
| return this.handlers.listResources(request.params); | ||
| case 'ui/open-link': | ||
| if (this.handlers.openLink == null) { | ||
| throw new Error('No ui/open-link handler configured'); | ||
| } | ||
| return this.handlers.openLink(request.params as { url: string }); | ||
| case 'ui/message': | ||
| return this.handlers.sendMessage?.(request.params) ?? {}; | ||
| case 'ui/update-model-context': | ||
| return this.handlers.updateModelContext?.(request.params) ?? {}; | ||
| case 'ui/request-display-mode': | ||
| return ( | ||
| this.handlers.requestDisplayMode?.( | ||
| request.params as { mode: 'inline' | 'fullscreen' | 'pip' }, | ||
| ) ?? { mode: this.hostContext.displayMode ?? 'inline' } | ||
| ); | ||
| default: | ||
| throw new Error(`Unsupported MCP App method: ${request.method}`); | ||
| } | ||
| } | ||
| /** | ||
| * Handles iframe lifecycle and telemetry notifications. | ||
| */ | ||
| private handleNotification(notification: MCPAppJsonRpcNotification): void { | ||
| switch (notification.method) { | ||
| case 'ui/notifications/initialized': | ||
| this.initialized = true; | ||
| this.flushNotifications(); | ||
| this.handlers.onInitialized?.(); | ||
| break; | ||
| case 'ui/notifications/size-changed': | ||
| this.handlers.onSizeChange?.( | ||
| notification.params as { width?: number; height?: number }, | ||
| ); | ||
| break; | ||
| case 'ui/notifications/request-teardown': | ||
| this.handlers.onRequestTeardown?.(notification.params); | ||
| break; | ||
| case 'notifications/message': | ||
| this.handlers.onLog?.(notification.params); | ||
| break; | ||
| } | ||
| } | ||
| /** | ||
| * Sends a host-to-iframe notification, queueing it until app initialization. | ||
| */ | ||
| private sendNotification( | ||
| notification: Omit<MCPAppJsonRpcNotification, 'jsonrpc'>, | ||
| ) { | ||
| const message = { jsonrpc: '2.0' as const, ...notification }; | ||
| if (!this.initialized && !notification.method.includes('sandbox')) { | ||
| this.pendingNotifications.push(message); | ||
| return; | ||
| } | ||
| this.post(message); | ||
| } | ||
| /** | ||
| * Sends notifications that were queued before `ui/notifications/initialized`. | ||
| */ | ||
| private flushNotifications(): void { | ||
| const notifications = this.pendingNotifications; | ||
| this.pendingNotifications = []; | ||
| for (const notification of notifications) { | ||
| this.post(notification); | ||
| } | ||
| } | ||
| /** | ||
| * Sends a host-initiated JSON-RPC request to the iframe. | ||
| */ | ||
| private request(method: string, params: unknown): Promise<unknown> { | ||
| const id = this.nextRequestId++; | ||
| this.post({ jsonrpc: '2.0', id, method, params }); | ||
| return new Promise((resolve, reject) => { | ||
| this.pendingResponses.set(id, { resolve, reject }); | ||
| }); | ||
| } | ||
| /** | ||
| * Posts a JSON-RPC message to the sandbox proxy iframe. | ||
| */ | ||
| private post(message: MCPAppJsonRpcMessage): void { | ||
| this.targetWindow.postMessage(message, this.targetOrigin); | ||
| } | ||
| } |
| export { MCPAppRenderer as experimental_MCPAppRenderer } from './app-renderer'; | ||
| export type { | ||
| MCPAppBridgeHandlers, | ||
| MCPAppMetadata, | ||
| MCPAppRendererProps, | ||
| MCPAppResource, | ||
| MCPAppSandboxConfig, | ||
| } from './types'; |
| import type { MCPAppResourceCSP } from '@ai-sdk/mcp'; | ||
| /** | ||
| * Default sandbox permissions for the outer sandbox proxy iframe. | ||
| */ | ||
| export const MCP_APP_DEFAULT_OUTER_SANDBOX = | ||
| 'allow-scripts allow-same-origin allow-forms'; | ||
| /** | ||
| * Default sandbox permissions for the inner iframe that runs app HTML. | ||
| */ | ||
| export const MCP_APP_DEFAULT_INNER_SANDBOX = 'allow-scripts allow-forms'; | ||
| /** | ||
| * Converts MCP App CSP metadata into a Content-Security-Policy string. | ||
| * | ||
| * The returned value is meant to be passed to a sandbox proxy, which can apply | ||
| * it to the inner iframe document. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const csp = getMCPAppCSP({ | ||
| * connectDomains: ['https://api.example.com'], | ||
| * resourceDomains: ['https://cdn.example.com'], | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export function getMCPAppCSP(csp?: MCPAppResourceCSP): string | undefined { | ||
| if (csp == null) { | ||
| return undefined; | ||
| } | ||
| const connectSrc = ["'self'", ...(csp.connectDomains ?? [])]; | ||
| const imgSrc = ["'self'", 'data:', ...(csp.resourceDomains ?? [])]; | ||
| const frameSrc = ["'self'", ...(csp.frameDomains ?? [])]; | ||
| return [ | ||
| "default-src 'none'", | ||
| "script-src 'unsafe-inline'", | ||
| "style-src 'unsafe-inline'", | ||
| `connect-src ${connectSrc.join(' ')}`, | ||
| `img-src ${imgSrc.join(' ')}`, | ||
| `font-src ${imgSrc.join(' ')}`, | ||
| `frame-src ${frameSrc.join(' ')}`, | ||
| ].join('; '); | ||
| } | ||
| /** | ||
| * Converts MCP App permission metadata into an iframe `allow` attribute. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const allow = getMCPAppAllowAttribute({ | ||
| * microphone: {}, | ||
| * clipboardWrite: {}, | ||
| * }); | ||
| * // "microphone; clipboard-write" | ||
| * ``` | ||
| */ | ||
| export function getMCPAppAllowAttribute( | ||
| permissions?: Record<string, unknown>, | ||
| ): string | undefined { | ||
| if (permissions == null) { | ||
| return undefined; | ||
| } | ||
| const allow = []; | ||
| if (permissions.camera) allow.push('camera'); | ||
| if (permissions.microphone) allow.push('microphone'); | ||
| if (permissions.geolocation) allow.push('geolocation'); | ||
| if (permissions.clipboardWrite) allow.push('clipboard-write'); | ||
| return allow.length > 0 ? allow.join('; ') : undefined; | ||
| } |
| import type { MCPAppResource } from '@ai-sdk/mcp'; | ||
| import type { DynamicToolUIPart, ToolUIPart, UITools } from 'ai'; | ||
| import type { CSSProperties, ReactNode } from 'react'; | ||
| export type { MCPAppResource }; | ||
| export type MCPAppDisplayMode = 'inline' | 'fullscreen' | 'pip'; | ||
| export type MCPAppMetadata = { | ||
| resourceUri: string; | ||
| mimeType: MCPAppResource['mimeType']; | ||
| visibility?: Array<'model' | 'app'>; | ||
| }; | ||
| export type MCPAppHostContext = { | ||
| theme?: 'light' | 'dark'; | ||
| displayMode?: MCPAppDisplayMode; | ||
| availableDisplayModes?: MCPAppDisplayMode[]; | ||
| [key: string]: unknown; | ||
| }; | ||
| export type MCPAppToolCallParams = { | ||
| name: string; | ||
| arguments?: Record<string, unknown>; | ||
| }; | ||
| export type MCPAppBridgeHandlers = { | ||
| /** | ||
| * Tools the MCP App is allowed to invoke via `tools/call`. Deny-by-default: | ||
| * if omitted, the (untrusted) app cannot call any tool. List only the tools | ||
| * the app is meant to see. | ||
| */ | ||
| allowedTools?: string[]; | ||
| callTool?: (params: MCPAppToolCallParams) => Promise<unknown> | unknown; | ||
| readResource?: (params: { uri: string }) => Promise<unknown> | unknown; | ||
| listResources?: (params?: unknown) => Promise<unknown> | unknown; | ||
| openLink?: (params: { url: string }) => Promise<unknown> | unknown; | ||
| sendMessage?: (params: unknown) => Promise<unknown> | unknown; | ||
| updateModelContext?: (params: unknown) => Promise<unknown> | unknown; | ||
| requestDisplayMode?: (params: { | ||
| mode: MCPAppDisplayMode; | ||
| }) => Promise<{ mode: MCPAppDisplayMode }> | { mode: MCPAppDisplayMode }; | ||
| onSizeChange?: (params: { width?: number; height?: number }) => void; | ||
| onInitialized?: () => void; | ||
| onRequestTeardown?: (params: unknown) => void; | ||
| onLog?: (params: unknown) => void; | ||
| onError?: (error: Error) => void; | ||
| }; | ||
| export type MCPAppSandboxConfig = { | ||
| url: string | URL; | ||
| title?: string; | ||
| className?: string; | ||
| style?: CSSProperties; | ||
| targetOrigin?: string; | ||
| outerSandbox?: string; | ||
| innerSandbox?: string; | ||
| }; | ||
| export type MCPAppFrameProps = { | ||
| app: MCPAppMetadata; | ||
| resource: MCPAppResource; | ||
| input?: unknown; | ||
| output?: unknown; | ||
| sandbox: MCPAppSandboxConfig; | ||
| handlers?: MCPAppBridgeHandlers; | ||
| hostInfo?: { name: string; version: string }; | ||
| hostContext?: MCPAppHostContext; | ||
| }; | ||
| export type MCPAppRendererProps = { | ||
| part: ToolUIPart<UITools> | DynamicToolUIPart; | ||
| sandbox: MCPAppSandboxConfig; | ||
| resource?: MCPAppResource; | ||
| loadResource?: (app: MCPAppMetadata) => Promise<MCPAppResource>; | ||
| handlers?: MCPAppBridgeHandlers; | ||
| hostInfo?: { name: string; version: string }; | ||
| hostContext?: MCPAppHostContext; | ||
| fallback?: ReactNode; | ||
| }; | ||
| export type MCPAppJsonRpcRequest = { | ||
| jsonrpc: '2.0'; | ||
| id: string | number; | ||
| method: string; | ||
| params?: unknown; | ||
| }; | ||
| export type MCPAppJsonRpcNotification = { | ||
| jsonrpc: '2.0'; | ||
| method: string; | ||
| params?: unknown; | ||
| }; | ||
| export type MCPAppJsonRpcResponse = { | ||
| jsonrpc: '2.0'; | ||
| id: string | number; | ||
| result?: unknown; | ||
| error?: { code: number; message: string; data?: unknown }; | ||
| }; | ||
| export type MCPAppJsonRpcMessage = | ||
| | MCPAppJsonRpcRequest | ||
| | MCPAppJsonRpcNotification | ||
| | MCPAppJsonRpcResponse; |
| import { isJSONObject } from '@ai-sdk/provider'; | ||
| import type { MCPAppMetadata, MCPAppRendererProps } from './types'; | ||
| /** | ||
| * Extracts MCP App metadata from an AI SDK tool UI part. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const app = getMCPAppFromToolPart({ | ||
| * type: 'dynamic-tool', | ||
| * toolName: 'showDashboard', | ||
| * toolCallId: 'call-1', | ||
| * state: 'input-available', | ||
| * input: { topic: 'usage' }, | ||
| * toolMetadata: { | ||
| * mcp: { | ||
| * app: { | ||
| * resourceUri: 'ui://example/dashboard', | ||
| * mimeType: 'text/html;profile=mcp-app', | ||
| * }, | ||
| * }, | ||
| * }, | ||
| * }); | ||
| * // { resourceUri: 'ui://example/dashboard', mimeType: 'text/html;profile=mcp-app' } | ||
| * ``` | ||
| */ | ||
| export function getMCPAppFromToolPart( | ||
| part: MCPAppRendererProps['part'], | ||
| ): MCPAppMetadata | undefined { | ||
| const mcpMetadata = part.toolMetadata?.mcp; | ||
| const rawAppMetadata = isJSONObject(mcpMetadata) | ||
| ? mcpMetadata.app | ||
| : undefined; | ||
| const appMetadata = isJSONObject(rawAppMetadata) ? rawAppMetadata : undefined; | ||
| if ( | ||
| appMetadata == null || | ||
| appMetadata.mimeType !== 'text/html;profile=mcp-app' || | ||
| typeof appMetadata.resourceUri !== 'string' || | ||
| !appMetadata.resourceUri.startsWith('ui://') || | ||
| (appMetadata.visibility != null && | ||
| (!Array.isArray(appMetadata.visibility) || | ||
| appMetadata.visibility.some( | ||
| value => value !== 'model' && value !== 'app', | ||
| ))) | ||
| ) { | ||
| return undefined; | ||
| } | ||
| return appMetadata as MCPAppMetadata; | ||
| } | ||
| /** | ||
| * Converts an AI SDK tool output into the MCP Apps tool-result shape. | ||
| * | ||
| * MCP Apps expect tool results to have `content` and optional | ||
| * `structuredContent`. MCP tools already return that shape, but typed AI SDK | ||
| * tools may return only structured data. This helper wraps structured-only | ||
| * outputs so the iframe can receive a valid tool result notification. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * normalizeMCPAppToolResult({ cards: [] }); | ||
| * // { content: [], structuredContent: { cards: [] } } | ||
| * ``` | ||
| */ | ||
| export function normalizeMCPAppToolResult(output: unknown): { | ||
| content: unknown[]; | ||
| structuredContent?: unknown; | ||
| isError?: boolean; | ||
| } { | ||
| if (output != null && typeof output === 'object' && 'content' in output) { | ||
| return output as { | ||
| content: unknown[]; | ||
| structuredContent?: unknown; | ||
| isError?: boolean; | ||
| }; | ||
| } | ||
| return { | ||
| content: [], | ||
| structuredContent: output, | ||
| }; | ||
| } |
| import { | ||
| Experimental_AbstractRealtimeSession as AbstractRealtimeSession, | ||
| type Experimental_RealtimeServerEvent as RealtimeServerEvent, | ||
| type Experimental_RealtimeSessionOptions as RealtimeSessionOptions, | ||
| type Experimental_RealtimeState as RealtimeState, | ||
| type Experimental_RealtimeStatus as RealtimeStatus, | ||
| type UIMessage, | ||
| } from 'ai'; | ||
| import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'; | ||
| type UseRealtimeOptions = RealtimeSessionOptions; | ||
| type RealtimeStateKey = keyof RealtimeState; | ||
| type RealtimeStoreKey = { | ||
| model: RealtimeSessionOptions['model']; | ||
| token: RealtimeSessionOptions['api']['token']; | ||
| sessionConfig: RealtimeSessionOptions['sessionConfig']; | ||
| sampleRate: RealtimeSessionOptions['sampleRate']; | ||
| maxEvents: RealtimeSessionOptions['maxEvents']; | ||
| }; | ||
| function getRealtimeStoreKey(options: UseRealtimeOptions): RealtimeStoreKey { | ||
| return { | ||
| model: options.model, | ||
| token: options.api.token, | ||
| sessionConfig: options.sessionConfig, | ||
| sampleRate: options.sampleRate, | ||
| maxEvents: options.maxEvents, | ||
| }; | ||
| } | ||
| function shouldCreateRealtimeStore( | ||
| currentKey: RealtimeStoreKey, | ||
| nextOptions: UseRealtimeOptions, | ||
| ): boolean { | ||
| return ( | ||
| currentKey.model !== nextOptions.model || | ||
| currentKey.token !== nextOptions.api.token || | ||
| currentKey.sessionConfig !== nextOptions.sessionConfig || | ||
| currentKey.sampleRate !== nextOptions.sampleRate || | ||
| currentKey.maxEvents !== nextOptions.maxEvents | ||
| ); | ||
| } | ||
| class RealtimeStore extends AbstractRealtimeSession { | ||
| protected state: RealtimeState = { | ||
| status: 'disconnected', | ||
| messages: [], | ||
| events: [], | ||
| isCapturing: false, | ||
| isPlaying: false, | ||
| }; | ||
| private callbacks: { [K in RealtimeStateKey]: Set<() => void> } = { | ||
| status: new Set(), | ||
| messages: new Set(), | ||
| events: new Set(), | ||
| isCapturing: new Set(), | ||
| isPlaying: new Set(), | ||
| }; | ||
| get status(): RealtimeStatus { | ||
| return this.state.status; | ||
| } | ||
| get messages(): UIMessage[] { | ||
| return this.state.messages; | ||
| } | ||
| get events(): RealtimeServerEvent[] { | ||
| return this.state.events; | ||
| } | ||
| get isCapturing(): boolean { | ||
| return this.state.isCapturing; | ||
| } | ||
| get isPlaying(): boolean { | ||
| return this.state.isPlaying; | ||
| } | ||
| subscribe(key: RealtimeStateKey, onChange: () => void): () => void { | ||
| this.callbacks[key].add(onChange); | ||
| return () => { | ||
| this.callbacks[key].delete(onChange); | ||
| }; | ||
| } | ||
| protected setState<K extends RealtimeStateKey>( | ||
| key: K, | ||
| value: RealtimeState[K], | ||
| ): void { | ||
| this.state = { ...this.state, [key]: value }; | ||
| this.callbacks[key].forEach(callback => callback()); | ||
| } | ||
| protected pushMessage(message: UIMessage): void { | ||
| this.state = { | ||
| ...this.state, | ||
| messages: [...this.state.messages, message], | ||
| }; | ||
| this.callbacks.messages.forEach(callback => callback()); | ||
| } | ||
| protected updateMessages( | ||
| updater: (messages: UIMessage[]) => UIMessage[], | ||
| ): void { | ||
| this.state = { | ||
| ...this.state, | ||
| messages: updater(this.state.messages), | ||
| }; | ||
| this.callbacks.messages.forEach(callback => callback()); | ||
| } | ||
| protected pushEvent(event: RealtimeServerEvent): void { | ||
| const nextEvents = [...this.state.events, event]; | ||
| this.state = { | ||
| ...this.state, | ||
| events: | ||
| nextEvents.length > this.maxEvents | ||
| ? nextEvents.slice(-this.maxEvents) | ||
| : nextEvents, | ||
| }; | ||
| this.callbacks.events.forEach(callback => callback()); | ||
| } | ||
| } | ||
| type UseRealtimeReturn = { | ||
| status: RealtimeStatus; | ||
| messages: UIMessage[]; | ||
| events: RealtimeServerEvent[]; | ||
| isCapturing: boolean; | ||
| isPlaying: boolean; | ||
| connect: () => Promise<void>; | ||
| disconnect: () => void; | ||
| addToolOutput: (callId: string, result: unknown) => void; | ||
| sendEvent: RealtimeStore['sendEvent']; | ||
| sendTextMessage: (text: string) => void; | ||
| sendAudio: (base64Audio: string) => void; | ||
| commitAudio: () => void; | ||
| clearAudioBuffer: () => void; | ||
| requestResponse: (options?: { modalities?: string[] }) => void; | ||
| cancelResponse: () => void; | ||
| startAudioCapture: (stream: MediaStream) => void; | ||
| stopAudioCapture: () => void; | ||
| stopPlayback: () => void; | ||
| }; | ||
| function useRealtime(options: UseRealtimeOptions): UseRealtimeReturn { | ||
| const callbacksRef = useRef({ | ||
| onToolCall: options.onToolCall, | ||
| onEvent: options.onEvent, | ||
| onError: options.onError, | ||
| }); | ||
| callbacksRef.current = { | ||
| onToolCall: options.onToolCall, | ||
| onEvent: options.onEvent, | ||
| onError: options.onError, | ||
| }; | ||
| const realtimeRef = useRef<{ | ||
| store: RealtimeStore; | ||
| key: RealtimeStoreKey; | ||
| } | null>(null); | ||
| let realtimeEntry = realtimeRef.current; | ||
| if ( | ||
| realtimeEntry == null || | ||
| shouldCreateRealtimeStore(realtimeEntry.key, options) | ||
| ) { | ||
| realtimeEntry = { | ||
| store: new RealtimeStore({ | ||
| ...options, | ||
| onToolCall: (...args) => callbacksRef.current.onToolCall?.(...args), | ||
| onEvent: (...args) => callbacksRef.current.onEvent?.(...args), | ||
| onError: (...args) => callbacksRef.current.onError?.(...args), | ||
| }), | ||
| key: getRealtimeStoreKey(options), | ||
| }; | ||
| realtimeRef.current = realtimeEntry; | ||
| } else { | ||
| realtimeEntry.key = getRealtimeStoreKey(options); | ||
| } | ||
| const rt = realtimeEntry.store; | ||
| const status = useSyncExternalStore( | ||
| useCallback(cb => rt.subscribe('status', cb), [rt]), | ||
| () => rt.status, | ||
| () => rt.status, | ||
| ); | ||
| const messages = useSyncExternalStore( | ||
| useCallback(cb => rt.subscribe('messages', cb), [rt]), | ||
| () => rt.messages, | ||
| () => rt.messages, | ||
| ); | ||
| const events = useSyncExternalStore( | ||
| useCallback(cb => rt.subscribe('events', cb), [rt]), | ||
| () => rt.events, | ||
| () => rt.events, | ||
| ); | ||
| const isCapturing = useSyncExternalStore( | ||
| useCallback(cb => rt.subscribe('isCapturing', cb), [rt]), | ||
| () => rt.isCapturing, | ||
| () => rt.isCapturing, | ||
| ); | ||
| const isPlaying = useSyncExternalStore( | ||
| useCallback(cb => rt.subscribe('isPlaying', cb), [rt]), | ||
| () => rt.isPlaying, | ||
| () => rt.isPlaying, | ||
| ); | ||
| useEffect(() => { | ||
| return () => rt.dispose(); | ||
| }, [rt]); | ||
| return { | ||
| status, | ||
| messages, | ||
| events, | ||
| isCapturing, | ||
| isPlaying, | ||
| connect: rt.connect.bind(rt), | ||
| disconnect: rt.disconnect.bind(rt), | ||
| addToolOutput: rt.addToolOutput.bind(rt), | ||
| sendEvent: rt.sendEvent.bind(rt), | ||
| sendTextMessage: rt.sendTextMessage.bind(rt), | ||
| sendAudio: rt.sendAudio.bind(rt), | ||
| commitAudio: rt.commitAudio.bind(rt), | ||
| clearAudioBuffer: rt.clearAudioBuffer.bind(rt), | ||
| requestResponse: rt.requestResponse.bind(rt), | ||
| cancelResponse: rt.cancelResponse.bind(rt), | ||
| startAudioCapture: rt.startAudioCapture.bind(rt), | ||
| stopAudioCapture: rt.stopAudioCapture.bind(rt), | ||
| stopPlayback: rt.stopPlayback.bind(rt), | ||
| }; | ||
| } | ||
| export const experimental_useRealtime = useRealtime; | ||
| export type { | ||
| RealtimeStatus as Experimental_RealtimeStatus, | ||
| UseRealtimeOptions as Experimental_UseRealtimeOptions, | ||
| UseRealtimeReturn as Experimental_UseRealtimeReturn, | ||
| }; |
+123
-3
@@ -1,4 +0,9 @@ | ||
| import { UIMessage, AbstractChat, ChatInit, CompletionRequestOptions, UseCompletionOptions, DeepPartial } from 'ai'; | ||
| export { CreateUIMessage, UIMessage, UseCompletionOptions } from 'ai'; | ||
| import { UIMessage, AbstractChat, ChatInit, CompletionRequestOptions, UseCompletionOptions, DeepPartial, Experimental_RealtimeSessionOptions, Experimental_RealtimeStatus, Experimental_RealtimeServerEvent, Experimental_AbstractRealtimeSession, Experimental_RealtimeState, ToolUIPart, UITools, DynamicToolUIPart } from 'ai'; | ||
| export { CreateUIMessage, Experimental_RealtimeStatus, UIMessage, UseCompletionOptions } from 'ai'; | ||
| import { FlexibleSchema, FetchFunction, Resolvable, InferSchema } from '@ai-sdk/provider-utils'; | ||
| import * as react_jsx_runtime from 'react/jsx-runtime'; | ||
| import * as react from 'react'; | ||
| import { CSSProperties, ReactNode } from 'react'; | ||
| import { MCPAppResource } from '@ai-sdk/mcp'; | ||
| export { MCPAppResource } from '@ai-sdk/mcp'; | ||
@@ -178,2 +183,117 @@ declare class Chat<UI_MESSAGE extends UIMessage> extends AbstractChat<UI_MESSAGE> { | ||
| export { Chat, Experimental_UseObjectHelpers, Experimental_UseObjectOptions, UseChatHelpers, UseChatOptions, UseCompletionHelpers, experimental_useObject, useChat, useCompletion }; | ||
| type UseRealtimeOptions = Experimental_RealtimeSessionOptions; | ||
| type RealtimeStateKey = keyof Experimental_RealtimeState; | ||
| declare class RealtimeStore extends Experimental_AbstractRealtimeSession { | ||
| protected state: Experimental_RealtimeState; | ||
| private callbacks; | ||
| get status(): Experimental_RealtimeStatus; | ||
| get messages(): UIMessage[]; | ||
| get events(): Experimental_RealtimeServerEvent[]; | ||
| get isCapturing(): boolean; | ||
| get isPlaying(): boolean; | ||
| subscribe(key: RealtimeStateKey, onChange: () => void): () => void; | ||
| protected setState<K extends RealtimeStateKey>(key: K, value: Experimental_RealtimeState[K]): void; | ||
| protected pushMessage(message: UIMessage): void; | ||
| protected updateMessages(updater: (messages: UIMessage[]) => UIMessage[]): void; | ||
| protected pushEvent(event: Experimental_RealtimeServerEvent): void; | ||
| } | ||
| type UseRealtimeReturn = { | ||
| status: Experimental_RealtimeStatus; | ||
| messages: UIMessage[]; | ||
| events: Experimental_RealtimeServerEvent[]; | ||
| isCapturing: boolean; | ||
| isPlaying: boolean; | ||
| connect: () => Promise<void>; | ||
| disconnect: () => void; | ||
| addToolOutput: (callId: string, result: unknown) => void; | ||
| sendEvent: RealtimeStore['sendEvent']; | ||
| sendTextMessage: (text: string) => void; | ||
| sendAudio: (base64Audio: string) => void; | ||
| commitAudio: () => void; | ||
| clearAudioBuffer: () => void; | ||
| requestResponse: (options?: { | ||
| modalities?: string[]; | ||
| }) => void; | ||
| cancelResponse: () => void; | ||
| startAudioCapture: (stream: MediaStream) => void; | ||
| stopAudioCapture: () => void; | ||
| stopPlayback: () => void; | ||
| }; | ||
| declare function useRealtime(options: UseRealtimeOptions): UseRealtimeReturn; | ||
| declare const experimental_useRealtime: typeof useRealtime; | ||
| type MCPAppDisplayMode = 'inline' | 'fullscreen' | 'pip'; | ||
| type MCPAppMetadata = { | ||
| resourceUri: string; | ||
| mimeType: MCPAppResource['mimeType']; | ||
| visibility?: Array<'model' | 'app'>; | ||
| }; | ||
| type MCPAppHostContext = { | ||
| theme?: 'light' | 'dark'; | ||
| displayMode?: MCPAppDisplayMode; | ||
| availableDisplayModes?: MCPAppDisplayMode[]; | ||
| [key: string]: unknown; | ||
| }; | ||
| type MCPAppToolCallParams = { | ||
| name: string; | ||
| arguments?: Record<string, unknown>; | ||
| }; | ||
| type MCPAppBridgeHandlers = { | ||
| /** | ||
| * Tools the MCP App is allowed to invoke via `tools/call`. Deny-by-default: | ||
| * if omitted, the (untrusted) app cannot call any tool. List only the tools | ||
| * the app is meant to see. | ||
| */ | ||
| allowedTools?: string[]; | ||
| callTool?: (params: MCPAppToolCallParams) => Promise<unknown> | unknown; | ||
| readResource?: (params: { | ||
| uri: string; | ||
| }) => Promise<unknown> | unknown; | ||
| listResources?: (params?: unknown) => Promise<unknown> | unknown; | ||
| openLink?: (params: { | ||
| url: string; | ||
| }) => Promise<unknown> | unknown; | ||
| sendMessage?: (params: unknown) => Promise<unknown> | unknown; | ||
| updateModelContext?: (params: unknown) => Promise<unknown> | unknown; | ||
| requestDisplayMode?: (params: { | ||
| mode: MCPAppDisplayMode; | ||
| }) => Promise<{ | ||
| mode: MCPAppDisplayMode; | ||
| }> | { | ||
| mode: MCPAppDisplayMode; | ||
| }; | ||
| onSizeChange?: (params: { | ||
| width?: number; | ||
| height?: number; | ||
| }) => void; | ||
| onInitialized?: () => void; | ||
| onRequestTeardown?: (params: unknown) => void; | ||
| onLog?: (params: unknown) => void; | ||
| onError?: (error: Error) => void; | ||
| }; | ||
| type MCPAppSandboxConfig = { | ||
| url: string | URL; | ||
| title?: string; | ||
| className?: string; | ||
| style?: CSSProperties; | ||
| targetOrigin?: string; | ||
| outerSandbox?: string; | ||
| innerSandbox?: string; | ||
| }; | ||
| type MCPAppRendererProps = { | ||
| part: ToolUIPart<UITools> | DynamicToolUIPart; | ||
| sandbox: MCPAppSandboxConfig; | ||
| resource?: MCPAppResource; | ||
| loadResource?: (app: MCPAppMetadata) => Promise<MCPAppResource>; | ||
| handlers?: MCPAppBridgeHandlers; | ||
| hostInfo?: { | ||
| name: string; | ||
| version: string; | ||
| }; | ||
| hostContext?: MCPAppHostContext; | ||
| fallback?: ReactNode; | ||
| }; | ||
| declare function MCPAppRenderer({ part, sandbox, resource: resourceProp, loadResource, handlers, hostInfo, hostContext, fallback, }: MCPAppRendererProps): string | number | boolean | Iterable<react.ReactNode> | react_jsx_runtime.JSX.Element | null; | ||
| export { Chat, Experimental_UseObjectHelpers, Experimental_UseObjectOptions, UseRealtimeOptions as Experimental_UseRealtimeOptions, UseRealtimeReturn as Experimental_UseRealtimeReturn, MCPAppBridgeHandlers, MCPAppMetadata, MCPAppRendererProps, MCPAppSandboxConfig, UseChatHelpers, UseChatOptions, UseCompletionHelpers, MCPAppRenderer as experimental_MCPAppRenderer, experimental_useObject, experimental_useRealtime, useChat, useCompletion }; |
+742
-0
@@ -506,5 +506,747 @@ var __accessCheck = (obj, member, msg) => { | ||
| var experimental_useObject = useObject; | ||
| // src/use-realtime.ts | ||
| import { | ||
| Experimental_AbstractRealtimeSession as AbstractRealtimeSession | ||
| } from "ai"; | ||
| import { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef4, useSyncExternalStore as useSyncExternalStore2 } from "react"; | ||
| function getRealtimeStoreKey(options) { | ||
| return { | ||
| model: options.model, | ||
| token: options.api.token, | ||
| sessionConfig: options.sessionConfig, | ||
| sampleRate: options.sampleRate, | ||
| maxEvents: options.maxEvents | ||
| }; | ||
| } | ||
| function shouldCreateRealtimeStore(currentKey, nextOptions) { | ||
| return currentKey.model !== nextOptions.model || currentKey.token !== nextOptions.api.token || currentKey.sessionConfig !== nextOptions.sessionConfig || currentKey.sampleRate !== nextOptions.sampleRate || currentKey.maxEvents !== nextOptions.maxEvents; | ||
| } | ||
| var RealtimeStore = class extends AbstractRealtimeSession { | ||
| constructor() { | ||
| super(...arguments); | ||
| this.state = { | ||
| status: "disconnected", | ||
| messages: [], | ||
| events: [], | ||
| isCapturing: false, | ||
| isPlaying: false | ||
| }; | ||
| this.callbacks = { | ||
| status: /* @__PURE__ */ new Set(), | ||
| messages: /* @__PURE__ */ new Set(), | ||
| events: /* @__PURE__ */ new Set(), | ||
| isCapturing: /* @__PURE__ */ new Set(), | ||
| isPlaying: /* @__PURE__ */ new Set() | ||
| }; | ||
| } | ||
| get status() { | ||
| return this.state.status; | ||
| } | ||
| get messages() { | ||
| return this.state.messages; | ||
| } | ||
| get events() { | ||
| return this.state.events; | ||
| } | ||
| get isCapturing() { | ||
| return this.state.isCapturing; | ||
| } | ||
| get isPlaying() { | ||
| return this.state.isPlaying; | ||
| } | ||
| subscribe(key, onChange) { | ||
| this.callbacks[key].add(onChange); | ||
| return () => { | ||
| this.callbacks[key].delete(onChange); | ||
| }; | ||
| } | ||
| setState(key, value) { | ||
| this.state = { ...this.state, [key]: value }; | ||
| this.callbacks[key].forEach((callback) => callback()); | ||
| } | ||
| pushMessage(message) { | ||
| this.state = { | ||
| ...this.state, | ||
| messages: [...this.state.messages, message] | ||
| }; | ||
| this.callbacks.messages.forEach((callback) => callback()); | ||
| } | ||
| updateMessages(updater) { | ||
| this.state = { | ||
| ...this.state, | ||
| messages: updater(this.state.messages) | ||
| }; | ||
| this.callbacks.messages.forEach((callback) => callback()); | ||
| } | ||
| pushEvent(event) { | ||
| const nextEvents = [...this.state.events, event]; | ||
| this.state = { | ||
| ...this.state, | ||
| events: nextEvents.length > this.maxEvents ? nextEvents.slice(-this.maxEvents) : nextEvents | ||
| }; | ||
| this.callbacks.events.forEach((callback) => callback()); | ||
| } | ||
| }; | ||
| function useRealtime(options) { | ||
| const callbacksRef = useRef4({ | ||
| onToolCall: options.onToolCall, | ||
| onEvent: options.onEvent, | ||
| onError: options.onError | ||
| }); | ||
| callbacksRef.current = { | ||
| onToolCall: options.onToolCall, | ||
| onEvent: options.onEvent, | ||
| onError: options.onError | ||
| }; | ||
| const realtimeRef = useRef4(null); | ||
| let realtimeEntry = realtimeRef.current; | ||
| if (realtimeEntry == null || shouldCreateRealtimeStore(realtimeEntry.key, options)) { | ||
| realtimeEntry = { | ||
| store: new RealtimeStore({ | ||
| ...options, | ||
| onToolCall: (...args) => { | ||
| var _a, _b; | ||
| return (_b = (_a = callbacksRef.current).onToolCall) == null ? void 0 : _b.call(_a, ...args); | ||
| }, | ||
| onEvent: (...args) => { | ||
| var _a, _b; | ||
| return (_b = (_a = callbacksRef.current).onEvent) == null ? void 0 : _b.call(_a, ...args); | ||
| }, | ||
| onError: (...args) => { | ||
| var _a, _b; | ||
| return (_b = (_a = callbacksRef.current).onError) == null ? void 0 : _b.call(_a, ...args); | ||
| } | ||
| }), | ||
| key: getRealtimeStoreKey(options) | ||
| }; | ||
| realtimeRef.current = realtimeEntry; | ||
| } else { | ||
| realtimeEntry.key = getRealtimeStoreKey(options); | ||
| } | ||
| const rt = realtimeEntry.store; | ||
| const status = useSyncExternalStore2( | ||
| useCallback4((cb) => rt.subscribe("status", cb), [rt]), | ||
| () => rt.status, | ||
| () => rt.status | ||
| ); | ||
| const messages = useSyncExternalStore2( | ||
| useCallback4((cb) => rt.subscribe("messages", cb), [rt]), | ||
| () => rt.messages, | ||
| () => rt.messages | ||
| ); | ||
| const events = useSyncExternalStore2( | ||
| useCallback4((cb) => rt.subscribe("events", cb), [rt]), | ||
| () => rt.events, | ||
| () => rt.events | ||
| ); | ||
| const isCapturing = useSyncExternalStore2( | ||
| useCallback4((cb) => rt.subscribe("isCapturing", cb), [rt]), | ||
| () => rt.isCapturing, | ||
| () => rt.isCapturing | ||
| ); | ||
| const isPlaying = useSyncExternalStore2( | ||
| useCallback4((cb) => rt.subscribe("isPlaying", cb), [rt]), | ||
| () => rt.isPlaying, | ||
| () => rt.isPlaying | ||
| ); | ||
| useEffect3(() => { | ||
| return () => rt.dispose(); | ||
| }, [rt]); | ||
| return { | ||
| status, | ||
| messages, | ||
| events, | ||
| isCapturing, | ||
| isPlaying, | ||
| connect: rt.connect.bind(rt), | ||
| disconnect: rt.disconnect.bind(rt), | ||
| addToolOutput: rt.addToolOutput.bind(rt), | ||
| sendEvent: rt.sendEvent.bind(rt), | ||
| sendTextMessage: rt.sendTextMessage.bind(rt), | ||
| sendAudio: rt.sendAudio.bind(rt), | ||
| commitAudio: rt.commitAudio.bind(rt), | ||
| clearAudioBuffer: rt.clearAudioBuffer.bind(rt), | ||
| requestResponse: rt.requestResponse.bind(rt), | ||
| cancelResponse: rt.cancelResponse.bind(rt), | ||
| startAudioCapture: rt.startAudioCapture.bind(rt), | ||
| stopAudioCapture: rt.stopAudioCapture.bind(rt), | ||
| stopPlayback: rt.stopPlayback.bind(rt) | ||
| }; | ||
| } | ||
| var experimental_useRealtime = useRealtime; | ||
| // src/mcp-apps/app-renderer.tsx | ||
| import { useEffect as useEffect5, useState as useState3 } from "react"; | ||
| // src/mcp-apps/app-frame.tsx | ||
| import { useEffect as useEffect4, useMemo, useRef as useRef5 } from "react"; | ||
| // src/mcp-apps/bridge.ts | ||
| import { isJSONObject } from "@ai-sdk/provider"; | ||
| var MCP_APP_PROTOCOL_VERSION = "2026-01-26"; | ||
| function isJsonRpcMessage(value) { | ||
| return value != null && typeof value === "object" && !Array.isArray(value) && "jsonrpc" in value && value.jsonrpc === "2.0"; | ||
| } | ||
| function isRequest(message) { | ||
| return "method" in message && "id" in message; | ||
| } | ||
| function isNotification(message) { | ||
| return "method" in message && !("id" in message); | ||
| } | ||
| function toError(error) { | ||
| return error instanceof Error ? error : new Error(String(error)); | ||
| } | ||
| function assertToolCallParams(params) { | ||
| if (!isJSONObject(params) || typeof params.name !== "string") { | ||
| throw new Error("Invalid tools/call params"); | ||
| } | ||
| return { | ||
| name: params.name, | ||
| arguments: isJSONObject(params.arguments) ? params.arguments : void 0 | ||
| }; | ||
| } | ||
| var MCPAppBridge = class { | ||
| constructor({ | ||
| targetWindow, | ||
| targetOrigin = "*", | ||
| handlers = {}, | ||
| hostInfo = { name: "ai-sdk-react", version: "1.0.0" }, | ||
| hostContext = { displayMode: "inline" } | ||
| }) { | ||
| this.initialized = false; | ||
| this.pendingNotifications = []; | ||
| this.nextRequestId = 0; | ||
| this.pendingResponses = /* @__PURE__ */ new Map(); | ||
| this.targetWindow = targetWindow; | ||
| this.targetOrigin = targetOrigin; | ||
| this.handlers = handlers; | ||
| this.hostInfo = hostInfo; | ||
| this.hostContext = hostContext; | ||
| } | ||
| /** | ||
| * Replaces the callbacks used to serve iframe requests. | ||
| */ | ||
| setHandlers(handlers) { | ||
| this.handlers = handlers; | ||
| } | ||
| /** | ||
| * Updates host context and notifies the iframe after initialization. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * bridge.setHostContext({ theme: 'dark', displayMode: 'inline' }); | ||
| * ``` | ||
| */ | ||
| setHostContext(hostContext) { | ||
| this.hostContext = hostContext; | ||
| this.sendNotification({ | ||
| method: "ui/notifications/host-context-changed", | ||
| params: hostContext | ||
| }); | ||
| } | ||
| /** | ||
| * Processes one `message` event from the sandbox proxy iframe. | ||
| */ | ||
| handleMessage(event) { | ||
| if (event.source !== this.targetWindow || !isJsonRpcMessage(event.data)) { | ||
| return; | ||
| } | ||
| const message = event.data; | ||
| if ("result" in message || "error" in message) { | ||
| this.handleResponse(message); | ||
| return; | ||
| } | ||
| if (isRequest(message)) { | ||
| void this.handleRequest(message); | ||
| return; | ||
| } | ||
| if (isNotification(message)) { | ||
| this.handleNotification(message); | ||
| } | ||
| } | ||
| /** | ||
| * Sends app HTML and sandbox settings to the sandbox proxy. | ||
| */ | ||
| sendSandboxResourceReady(params) { | ||
| this.post({ | ||
| jsonrpc: "2.0", | ||
| method: "ui/notifications/sandbox-resource-ready", | ||
| params | ||
| }); | ||
| } | ||
| /** | ||
| * Sends final tool arguments to the MCP App. | ||
| */ | ||
| sendToolInput(input) { | ||
| this.sendNotification({ | ||
| method: "ui/notifications/tool-input", | ||
| params: { arguments: input } | ||
| }); | ||
| } | ||
| /** | ||
| * Sends a completed MCP tool result to the MCP App. | ||
| */ | ||
| sendToolResult(result) { | ||
| this.sendNotification({ | ||
| method: "ui/notifications/tool-result", | ||
| params: result | ||
| }); | ||
| } | ||
| /** | ||
| * Notifies the MCP App that the related tool call was cancelled. | ||
| */ | ||
| sendToolCancelled(reason) { | ||
| this.sendNotification({ | ||
| method: "ui/notifications/tool-cancelled", | ||
| params: reason != null ? { reason } : {} | ||
| }); | ||
| } | ||
| /** | ||
| * Requests graceful teardown before the host removes the iframe. | ||
| */ | ||
| teardownResource() { | ||
| return this.request("ui/resource-teardown", {}); | ||
| } | ||
| /** | ||
| * Rejects pending bridge requests and clears queued notifications. | ||
| */ | ||
| close() { | ||
| for (const pending of this.pendingResponses.values()) { | ||
| pending.reject(new Error("MCP App bridge closed")); | ||
| } | ||
| this.pendingResponses.clear(); | ||
| this.pendingNotifications = []; | ||
| } | ||
| /** | ||
| * Resolves or rejects a host-initiated request when the iframe responds. | ||
| */ | ||
| handleResponse(response) { | ||
| const pending = this.pendingResponses.get(response.id); | ||
| if (pending == null) { | ||
| return; | ||
| } | ||
| this.pendingResponses.delete(response.id); | ||
| if (response.error != null) { | ||
| pending.reject(new Error(response.error.message)); | ||
| } else { | ||
| pending.resolve(response.result); | ||
| } | ||
| } | ||
| /** | ||
| * Runs a handler for an iframe request and posts the JSON-RPC response. | ||
| */ | ||
| async handleRequest(request) { | ||
| var _a, _b; | ||
| try { | ||
| const result = await this.getRequestResult(request); | ||
| this.post({ jsonrpc: "2.0", id: request.id, result }); | ||
| } catch (error) { | ||
| const normalizedError = toError(error); | ||
| (_b = (_a = this.handlers).onError) == null ? void 0 : _b.call(_a, normalizedError); | ||
| this.post({ | ||
| jsonrpc: "2.0", | ||
| id: request.id, | ||
| error: { code: -32603, message: normalizedError.message } | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Maps supported iframe request methods to host callbacks. | ||
| */ | ||
| async getRequestResult(request) { | ||
| var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j; | ||
| switch (request.method) { | ||
| case "ui/initialize": | ||
| return { | ||
| protocolVersion: MCP_APP_PROTOCOL_VERSION, | ||
| hostCapabilities: { | ||
| ...this.handlers.callTool != null ? { serverTools: {} } : {}, | ||
| ...this.handlers.readResource != null ? { serverResources: {} } : {}, | ||
| ...this.handlers.onLog != null ? { logging: {} } : {} | ||
| }, | ||
| hostInfo: this.hostInfo, | ||
| hostContext: this.hostContext | ||
| }; | ||
| case "tools/call": { | ||
| if (this.handlers.callTool == null) { | ||
| throw new Error("No tools/call handler configured"); | ||
| } | ||
| const params = assertToolCallParams(request.params); | ||
| if (this.handlers.allowedTools == null || !this.handlers.allowedTools.includes(params.name)) { | ||
| throw new Error(`Tool is not app-visible: ${params.name}`); | ||
| } | ||
| return this.handlers.callTool(params); | ||
| } | ||
| case "resources/read": | ||
| if (this.handlers.readResource == null) { | ||
| throw new Error("No resources/read handler configured"); | ||
| } | ||
| return this.handlers.readResource(request.params); | ||
| case "resources/list": | ||
| if (this.handlers.listResources == null) { | ||
| throw new Error("No resources/list handler configured"); | ||
| } | ||
| return this.handlers.listResources(request.params); | ||
| case "ui/open-link": | ||
| if (this.handlers.openLink == null) { | ||
| throw new Error("No ui/open-link handler configured"); | ||
| } | ||
| return this.handlers.openLink(request.params); | ||
| case "ui/message": | ||
| return (_c = (_b = (_a = this.handlers).sendMessage) == null ? void 0 : _b.call(_a, request.params)) != null ? _c : {}; | ||
| case "ui/update-model-context": | ||
| return (_f = (_e = (_d = this.handlers).updateModelContext) == null ? void 0 : _e.call(_d, request.params)) != null ? _f : {}; | ||
| case "ui/request-display-mode": | ||
| return (_j = (_h = (_g = this.handlers).requestDisplayMode) == null ? void 0 : _h.call( | ||
| _g, | ||
| request.params | ||
| )) != null ? _j : { mode: (_i = this.hostContext.displayMode) != null ? _i : "inline" }; | ||
| default: | ||
| throw new Error(`Unsupported MCP App method: ${request.method}`); | ||
| } | ||
| } | ||
| /** | ||
| * Handles iframe lifecycle and telemetry notifications. | ||
| */ | ||
| handleNotification(notification) { | ||
| var _a, _b, _c, _d, _e, _f, _g, _h; | ||
| switch (notification.method) { | ||
| case "ui/notifications/initialized": | ||
| this.initialized = true; | ||
| this.flushNotifications(); | ||
| (_b = (_a = this.handlers).onInitialized) == null ? void 0 : _b.call(_a); | ||
| break; | ||
| case "ui/notifications/size-changed": | ||
| (_d = (_c = this.handlers).onSizeChange) == null ? void 0 : _d.call( | ||
| _c, | ||
| notification.params | ||
| ); | ||
| break; | ||
| case "ui/notifications/request-teardown": | ||
| (_f = (_e = this.handlers).onRequestTeardown) == null ? void 0 : _f.call(_e, notification.params); | ||
| break; | ||
| case "notifications/message": | ||
| (_h = (_g = this.handlers).onLog) == null ? void 0 : _h.call(_g, notification.params); | ||
| break; | ||
| } | ||
| } | ||
| /** | ||
| * Sends a host-to-iframe notification, queueing it until app initialization. | ||
| */ | ||
| sendNotification(notification) { | ||
| const message = { jsonrpc: "2.0", ...notification }; | ||
| if (!this.initialized && !notification.method.includes("sandbox")) { | ||
| this.pendingNotifications.push(message); | ||
| return; | ||
| } | ||
| this.post(message); | ||
| } | ||
| /** | ||
| * Sends notifications that were queued before `ui/notifications/initialized`. | ||
| */ | ||
| flushNotifications() { | ||
| const notifications = this.pendingNotifications; | ||
| this.pendingNotifications = []; | ||
| for (const notification of notifications) { | ||
| this.post(notification); | ||
| } | ||
| } | ||
| /** | ||
| * Sends a host-initiated JSON-RPC request to the iframe. | ||
| */ | ||
| request(method, params) { | ||
| const id = this.nextRequestId++; | ||
| this.post({ jsonrpc: "2.0", id, method, params }); | ||
| return new Promise((resolve2, reject) => { | ||
| this.pendingResponses.set(id, { resolve: resolve2, reject }); | ||
| }); | ||
| } | ||
| /** | ||
| * Posts a JSON-RPC message to the sandbox proxy iframe. | ||
| */ | ||
| post(message) { | ||
| this.targetWindow.postMessage(message, this.targetOrigin); | ||
| } | ||
| }; | ||
| // src/mcp-apps/sandbox.ts | ||
| var MCP_APP_DEFAULT_OUTER_SANDBOX = "allow-scripts allow-same-origin allow-forms"; | ||
| var MCP_APP_DEFAULT_INNER_SANDBOX = "allow-scripts allow-forms"; | ||
| function getMCPAppCSP(csp) { | ||
| var _a, _b, _c; | ||
| if (csp == null) { | ||
| return void 0; | ||
| } | ||
| const connectSrc = ["'self'", ...(_a = csp.connectDomains) != null ? _a : []]; | ||
| const imgSrc = ["'self'", "data:", ...(_b = csp.resourceDomains) != null ? _b : []]; | ||
| const frameSrc = ["'self'", ...(_c = csp.frameDomains) != null ? _c : []]; | ||
| return [ | ||
| "default-src 'none'", | ||
| "script-src 'unsafe-inline'", | ||
| "style-src 'unsafe-inline'", | ||
| `connect-src ${connectSrc.join(" ")}`, | ||
| `img-src ${imgSrc.join(" ")}`, | ||
| `font-src ${imgSrc.join(" ")}`, | ||
| `frame-src ${frameSrc.join(" ")}` | ||
| ].join("; "); | ||
| } | ||
| function getMCPAppAllowAttribute(permissions) { | ||
| if (permissions == null) { | ||
| return void 0; | ||
| } | ||
| const allow = []; | ||
| if (permissions.camera) | ||
| allow.push("camera"); | ||
| if (permissions.microphone) | ||
| allow.push("microphone"); | ||
| if (permissions.geolocation) | ||
| allow.push("geolocation"); | ||
| if (permissions.clipboardWrite) | ||
| allow.push("clipboard-write"); | ||
| return allow.length > 0 ? allow.join("; ") : void 0; | ||
| } | ||
| // src/mcp-apps/utils.ts | ||
| import { isJSONObject as isJSONObject2 } from "@ai-sdk/provider"; | ||
| function getMCPAppFromToolPart(part) { | ||
| var _a; | ||
| const mcpMetadata = (_a = part.toolMetadata) == null ? void 0 : _a.mcp; | ||
| const rawAppMetadata = isJSONObject2(mcpMetadata) ? mcpMetadata.app : void 0; | ||
| const appMetadata = isJSONObject2(rawAppMetadata) ? rawAppMetadata : void 0; | ||
| if (appMetadata == null || appMetadata.mimeType !== "text/html;profile=mcp-app" || typeof appMetadata.resourceUri !== "string" || !appMetadata.resourceUri.startsWith("ui://") || appMetadata.visibility != null && (!Array.isArray(appMetadata.visibility) || appMetadata.visibility.some( | ||
| (value) => value !== "model" && value !== "app" | ||
| ))) { | ||
| return void 0; | ||
| } | ||
| return appMetadata; | ||
| } | ||
| function normalizeMCPAppToolResult(output) { | ||
| if (output != null && typeof output === "object" && "content" in output) { | ||
| return output; | ||
| } | ||
| return { | ||
| content: [], | ||
| structuredContent: output | ||
| }; | ||
| } | ||
| // src/mcp-apps/app-frame.tsx | ||
| import { jsx } from "react/jsx-runtime"; | ||
| function sendToolState({ | ||
| bridge, | ||
| input, | ||
| output | ||
| }) { | ||
| if (bridge == null) { | ||
| return; | ||
| } | ||
| if (input !== void 0) { | ||
| bridge.sendToolInput(input); | ||
| } | ||
| if (output !== void 0) { | ||
| bridge.sendToolResult(normalizeMCPAppToolResult(output)); | ||
| } | ||
| } | ||
| function MCPAppFrame({ | ||
| app, | ||
| resource, | ||
| input, | ||
| output, | ||
| sandbox, | ||
| handlers, | ||
| hostInfo, | ||
| hostContext | ||
| }) { | ||
| var _a, _b, _c, _d, _e, _f; | ||
| const iframeRef = useRef5(null); | ||
| const bridgeRef = useRef5(void 0); | ||
| const inputRef = useRef5(input); | ||
| const outputRef = useRef5(output); | ||
| const hostContextRef = useRef5(hostContext); | ||
| const initializedRef = useRef5(false); | ||
| inputRef.current = input; | ||
| outputRef.current = output; | ||
| hostContextRef.current = hostContext; | ||
| const targetOrigin = (_a = sandbox.targetOrigin) != null ? _a : "*"; | ||
| const sandboxUrl = String(sandbox.url); | ||
| const resourceCSP = getMCPAppCSP((_b = resource.meta) == null ? void 0 : _b.csp); | ||
| const resourceAllow = getMCPAppAllowAttribute((_c = resource.meta) == null ? void 0 : _c.permissions); | ||
| const innerSandbox = (_d = sandbox.innerSandbox) != null ? _d : MCP_APP_DEFAULT_INNER_SANDBOX; | ||
| const bridgeHandlers = useMemo( | ||
| () => ({ | ||
| ...handlers, | ||
| onInitialized: () => { | ||
| var _a2; | ||
| initializedRef.current = true; | ||
| (_a2 = handlers == null ? void 0 : handlers.onInitialized) == null ? void 0 : _a2.call(handlers); | ||
| sendToolState({ | ||
| bridge: bridgeRef.current, | ||
| input: inputRef.current, | ||
| output: outputRef.current | ||
| }); | ||
| } | ||
| }), | ||
| [handlers] | ||
| ); | ||
| const bridgeHandlersRef = useRef5(bridgeHandlers); | ||
| bridgeHandlersRef.current = bridgeHandlers; | ||
| useEffect4(() => { | ||
| const iframe = iframeRef.current; | ||
| const targetWindow = iframe == null ? void 0 : iframe.contentWindow; | ||
| if (targetWindow == null) { | ||
| return; | ||
| } | ||
| initializedRef.current = false; | ||
| const bridge = new MCPAppBridge({ | ||
| targetWindow, | ||
| targetOrigin, | ||
| handlers: bridgeHandlersRef.current, | ||
| hostInfo, | ||
| hostContext: hostContextRef.current | ||
| }); | ||
| bridgeRef.current = bridge; | ||
| const onMessage = (event) => { | ||
| var _a2; | ||
| if (event.source === targetWindow && ((_a2 = event.data) == null ? void 0 : _a2.jsonrpc) === "2.0" && event.data.method === "ui/notifications/sandbox-proxy-ready") { | ||
| bridge.sendSandboxResourceReady({ | ||
| html: resource.html, | ||
| csp: resourceCSP, | ||
| sandbox: innerSandbox, | ||
| allow: resourceAllow | ||
| }); | ||
| return; | ||
| } | ||
| bridge.handleMessage(event); | ||
| }; | ||
| window.addEventListener("message", onMessage); | ||
| return () => { | ||
| initializedRef.current = false; | ||
| window.removeEventListener("message", onMessage); | ||
| void bridge.teardownResource().catch(() => { | ||
| }); | ||
| bridge.close(); | ||
| bridgeRef.current = void 0; | ||
| }; | ||
| }, [ | ||
| hostInfo, | ||
| innerSandbox, | ||
| resource.html, | ||
| resourceAllow, | ||
| resourceCSP, | ||
| sandboxUrl, | ||
| targetOrigin | ||
| ]); | ||
| useEffect4(() => { | ||
| var _a2; | ||
| (_a2 = bridgeRef.current) == null ? void 0 : _a2.setHandlers(bridgeHandlers); | ||
| }, [bridgeHandlers]); | ||
| useEffect4(() => { | ||
| var _a2; | ||
| if (hostContext != null) { | ||
| (_a2 = bridgeRef.current) == null ? void 0 : _a2.setHostContext(hostContext); | ||
| } | ||
| }, [hostContext]); | ||
| useEffect4(() => { | ||
| var _a2; | ||
| if (initializedRef.current && input !== void 0) { | ||
| (_a2 = bridgeRef.current) == null ? void 0 : _a2.sendToolInput(input); | ||
| } | ||
| }, [input]); | ||
| useEffect4(() => { | ||
| var _a2; | ||
| if (initializedRef.current && output !== void 0) { | ||
| (_a2 = bridgeRef.current) == null ? void 0 : _a2.sendToolResult(normalizeMCPAppToolResult(output)); | ||
| } | ||
| }, [output]); | ||
| return /* @__PURE__ */ jsx( | ||
| "iframe", | ||
| { | ||
| ref: iframeRef, | ||
| title: "MCP App", | ||
| "aria-label": (_e = sandbox.title) != null ? _e : app.resourceUri, | ||
| src: sandboxUrl, | ||
| className: sandbox.className, | ||
| style: sandbox.style, | ||
| sandbox: (_f = sandbox.outerSandbox) != null ? _f : MCP_APP_DEFAULT_OUTER_SANDBOX | ||
| } | ||
| ); | ||
| } | ||
| // src/mcp-apps/app-renderer.tsx | ||
| import { jsx as jsx2 } from "react/jsx-runtime"; | ||
| function getToolPartOutput(part) { | ||
| return part.state === "output-available" ? part.output : void 0; | ||
| } | ||
| function getToolPartInput(part) { | ||
| return part.state === "input-available" || part.state === "output-available" ? part.input : void 0; | ||
| } | ||
| function MCPAppRenderer({ | ||
| part, | ||
| sandbox, | ||
| resource: resourceProp, | ||
| loadResource, | ||
| handlers, | ||
| hostInfo, | ||
| hostContext, | ||
| fallback = null | ||
| }) { | ||
| const app = getMCPAppFromToolPart(part); | ||
| const [cachedApp, setCachedApp] = useState3(); | ||
| const [loadedResource, setLoadedResource] = useState3(); | ||
| useEffect5(() => { | ||
| if (app != null) { | ||
| setCachedApp( | ||
| (previous) => (previous == null ? void 0 : previous.resourceUri) === app.resourceUri ? previous : app | ||
| ); | ||
| } | ||
| }, [app == null ? void 0 : app.resourceUri]); | ||
| const appForRender = app != null ? app : cachedApp; | ||
| useEffect5(() => { | ||
| if (appForRender == null || resourceProp != null || loadResource == null) { | ||
| return; | ||
| } | ||
| let cancelled = false; | ||
| const resourceUri = appForRender.resourceUri; | ||
| loadResource(appForRender).then((resource2) => { | ||
| if (!cancelled) { | ||
| setLoadedResource({ resourceUri, resource: resource2 }); | ||
| } | ||
| }).catch((error2) => { | ||
| if (!cancelled) { | ||
| setLoadedResource({ | ||
| resourceUri, | ||
| error: error2 instanceof Error ? error2 : new Error(String(error2)) | ||
| }); | ||
| } | ||
| }); | ||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [appForRender == null ? void 0 : appForRender.resourceUri, loadResource, resourceProp]); | ||
| const loadedResourceForApp = (loadedResource == null ? void 0 : loadedResource.resourceUri) === (appForRender == null ? void 0 : appForRender.resourceUri) ? loadedResource : void 0; | ||
| const resource = resourceProp != null ? resourceProp : loadedResourceForApp == null ? void 0 : loadedResourceForApp.resource; | ||
| const error = resourceProp == null ? loadedResourceForApp == null ? void 0 : loadedResourceForApp.error : void 0; | ||
| if (appForRender == null || error != null || resource == null) { | ||
| return fallback; | ||
| } | ||
| return /* @__PURE__ */ jsx2( | ||
| MCPAppFrame, | ||
| { | ||
| app: appForRender, | ||
| resource, | ||
| input: getToolPartInput(part), | ||
| output: getToolPartOutput(part), | ||
| sandbox, | ||
| handlers, | ||
| hostInfo, | ||
| hostContext | ||
| } | ||
| ); | ||
| } | ||
| export { | ||
| Chat, | ||
| MCPAppRenderer as experimental_MCPAppRenderer, | ||
| experimental_useObject, | ||
| experimental_useRealtime, | ||
| useChat, | ||
@@ -511,0 +1253,0 @@ useCompletion |
+17
-15
| { | ||
| "name": "@ai-sdk/react", | ||
| "version": "4.0.0-beta.116", | ||
| "version": "4.0.0-beta.181", | ||
| "type": "module", | ||
@@ -29,22 +29,24 @@ "license": "Apache-2.0", | ||
| "dependencies": { | ||
| "swr": "^2.2.5", | ||
| "swr": "^2.4.1", | ||
| "throttleit": "2.1.0", | ||
| "@ai-sdk/provider-utils": "5.0.0-beta.30", | ||
| "ai": "7.0.0-beta.116" | ||
| "@ai-sdk/mcp": "2.0.0-beta.66", | ||
| "ai": "7.0.0-beta.177", | ||
| "@ai-sdk/provider": "4.0.0-beta.19", | ||
| "@ai-sdk/provider-utils": "5.0.0-beta.49" | ||
| }, | ||
| "devDependencies": { | ||
| "@testing-library/jest-dom": "^6.6.3", | ||
| "@testing-library/react": "^16.0.1", | ||
| "@testing-library/user-event": "^14.5.2", | ||
| "@types/node": "20.17.24", | ||
| "@types/react": "^18", | ||
| "@types/react-dom": "^18", | ||
| "@vitejs/plugin-react": "^4.3.4", | ||
| "jsdom": "^24.0.0", | ||
| "@testing-library/jest-dom": "^6.9.1", | ||
| "@testing-library/react": "^16.3.2", | ||
| "@testing-library/user-event": "^14.6.1", | ||
| "@types/node": "22.19.19", | ||
| "@types/react": "^18.3.28", | ||
| "@types/react-dom": "^18.3.7", | ||
| "@vitejs/plugin-react": "^4.7.0", | ||
| "jsdom": "^24.1.3", | ||
| "msw": "2.6.4", | ||
| "react-dom": "^18 || ^19", | ||
| "react-dom": "^19.2.6", | ||
| "tsup": "^7.2.0", | ||
| "typescript": "5.8.3", | ||
| "zod": "3.25.76", | ||
| "@ai-sdk/test-server": "2.0.0-beta.3", | ||
| "@ai-sdk/test-server": "2.0.0-beta.7", | ||
| "@vercel/ai-tsconfig": "0.0.0" | ||
@@ -56,3 +58,3 @@ }, | ||
| "engines": { | ||
| "node": ">=18" | ||
| "node": ">=22" | ||
| }, | ||
@@ -59,0 +61,0 @@ "publishConfig": { |
+2
-0
@@ -5,1 +5,3 @@ export * from './use-chat'; | ||
| export * from './use-object'; | ||
| export * from './use-realtime'; | ||
| export * from './mcp-apps'; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
267219
79.39%24
50%3332
135.98%7
40%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
Updated
Updated