🚀. Socket Launch Week Day 2:Introducing Manifest Alerts.Learn more
Sign In

@ai-sdk/react

Package Overview
Dependencies
Maintainers
3
Versions
978
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ai-sdk/react - npm Package Compare versions

Comparing version
4.0.0-beta.116
to
4.0.0-beta.181
+164
src/mcp-apps/app-frame.tsx
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 };

@@ -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": {

@@ -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