AI News HubLIVE
In-site rewrite4 min read

Add MCP Apps to Your AI SDK Application

This guide explains how to build an MCP Apps host using @ai-sdk/mcp and @ai-sdk/react, including filtering model-visible tools, reading ui:// resources, and rendering interactive tool UIs in a sandboxed iframe.

SourceHacker News AIAuthor: flashbrew

Add MCP Apps to your AI SDK application

Build an MCP Apps host with the AI SDK using @ai-sdk/mcp and @ai-sdk/react to filter model-visible tools, read ui:// resources, and render interactive tool UIs in a sandboxed iframe with experimental_MCPAppRenderer.

Knowledge Base/AI SDK

6 min read

Last updated June 25, 2026

MCP Apps let a Model Context Protocol (MCP) tool return an interactive UI instead of plain text. The model still calls ordinary MCP tools, but a tool can point to a ui:// resource that holds HTML, and your app renders that HTML in a sandboxed iframe. To build the host side, the AI SDK provides @ai-sdk/mcp helpers for advertising MCP Apps support, filtering which tools the model sees, and reading ui:// resources, plus @ai-sdk/react components for rendering the iframe and bridging its messages. Your chat can then display a dashboard, form, or other interactive view generated by a tool, while the untrusted HTML remains isolated.

Overview

In this guide, you'll learn how to:

Connect to an MCP server with MCP Apps capabilities

Pass only model-visible tools to the model with splitMCPAppTools

Read a tool's ui:// resource with readMCPAppResource

Proxy app-initiated tool calls safely from the iframe

Render the app UI in your React chat with experimental_MCPAppRenderer

Prerequisites

Before you begin, make sure you have:

The ai package with @ai-sdk/mcp and @ai-sdk/react

The MCP TypeScript SDK (@modelcontextprotocol/sdk) and a provider package, such as @ai-sdk/openai

An MCP server that exposes MCP Apps tools (tools that point to ui:// resources)

A React app that uses useChat (the examples below use the Next.js App Router)

How an MCP Apps host works

An MCP Apps host connects to an MCP server, decides which tools the model can see, and renders any app UI that a tool points to. At runtime, the host follows these steps:

Connect to the MCP server with MCP Apps client capabilities.

List the server's tools and split them by MCP Apps visibility.

Pass only the model-visible tools to streamText or generateText.

Read a tool's ui:// resource when its tool part includes MCP App metadata.

Render the HTML resource in a sandboxed iframe.

Proxy allowed iframe requests, such as app-visible tool calls, back to the MCP server.

The rest of this guide builds each step.

Steps

Connect to the MCP server with MCP Apps support

Create the MCP client with mcpAppClientCapabilities so the host advertises that it can render text/html;profile=mcp-app resources.

app/api/chat/mcp-client.ts

import { createMCPClient, mcpAppClientCapabilities } from '@ai-sdk/mcp';

import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

export function createMCPAppsClient(origin: string) {

return createMCPClient({

transport: new StreamableHTTPClientTransport(new URL('/mcp', origin)),

clientName: 'my-mcp-apps-host',

capabilities: mcpAppClientCapabilities,

});

}

Advertise these capabilities only if your host can safely fetch and render MCP App resources.

Expose only model-visible tools

MCP Apps tools can declare _meta.ui.visibility. Pass tools marked "model" to the model, and keep tools marked only "app" for iframe requests so the model never sees them. Split the tool list with splitMCPAppTools and pass modelVisible to streamText.

app/api/chat/route.ts

import { splitMCPAppTools } from '@ai-sdk/mcp';

import {

convertToModelMessages,

createUIMessageStreamResponse,

streamText,

toUIMessageStream,

} from 'ai';

import { createMCPAppsClient } from './mcp-client';

import { openai } from '@ai-sdk/openai';

export async function POST(req: Request) {

const requestUrl = new URL(req.url);

const client = await createMCPAppsClient(requestUrl.origin);

const { messages } = await req.json();

try {

const definitions = await client.listTools();

const { modelVisible } = splitMCPAppTools(definitions);

const tools = client.toolsFromDefinitions(modelVisible);

const result = streamText({

model: openai('gpt-5.2'),

tools,

messages: await convertToModelMessages(messages),

onEnd: async () => {

await client.close();

},

});

return createUIMessageStreamResponse({

stream: toUIMessageStream({ stream: result.stream }),

});

} catch (error) {

await client.close();

throw error;

}

}

When the model calls an app-backed tool, the MCP client keeps the app metadata in the tool UI, which the React renderer uses to determine whether a tool part has an MCP App.

Read the app's UI resource

Read a tool's ui:// resource with readMCPAppResource before you send it to the browser host.

app/api/mcp-app-host/read-resource/route.ts

import { readMCPAppResource } from '@ai-sdk/mcp';

import { createMCPAppsClient } from '../../chat/mcp-client';

export async function POST(req: Request) {

const requestUrl = new URL(req.url);

const { uri } = await req.json();

const client = await createMCPAppsClient(requestUrl.origin);

try {

return Response.json(await readMCPAppResource({ client, uri }));

} finally {

await client.close();

}

}

readMCPAppResource checks that the resource uses a ui:// URI, requires the MCP Apps MIME type, decodes text or base64 content, and returns the HTML along with rendering metadata such as its content security policy and permissions.

Proxy app-visible tool calls

The iframe can't reach your MCP server directly. It sends JSON-RPC messages to your host, and your host decides which ones to allow. For an app-initiated tool call, confirm that the requested tool is app-visible before calling the MCP server.

app/api/mcp-app-host/call-tool/route.ts

import { splitMCPAppTools } from '@ai-sdk/mcp';

import { createMCPAppsClient } from '../../chat/mcp-client';

export async function POST(req: Request) {

const requestUrl = new URL(req.url);

const { name, arguments: toolArguments } = await req.json();

const client = await createMCPAppsClient(requestUrl.origin);

try {

const { appVisible } = splitMCPAppTools(await client.listTools());

const isAllowed = appVisible.tools.some(tool => tool.name === name);

if (!isAllowed) {

return Response.json(

{ error: 'Tool is not app-visible' },

{ status: 403 },

);

}

return Response.json(

await client.callTool({

name,

arguments: toolArguments ?? {},

}),

);

} finally {

await client.close();

}

}

In production, add any policy and user-approval checks your app needs before forwarding an iframe request.

Render the app in your React chat

In your React chat UI, render normal message parts as usual and pass tool parts to experimental_MCPAppRenderer.

experimental_MCPAppRenderer is experimental and may change in a future release.

app/page.tsx

'use client';

import {

experimental_MCPAppRenderer as MCPAppRenderer,

useChat,

type MCPAppBridgeHandlers,

type MCPAppMetadata,

type MCPAppResource,

type MCPAppSandboxConfig,

} from '@ai-sdk/react';

import { DefaultChatTransport, isToolUIPart } from 'ai';

const sandbox = {

url: '/mcp-app-sandbox',

className: 'h-80 w-full rounded-lg border',

style: { border: 0 },

} satisfies MCPAppSandboxConfig;

async function loadResource(app: MCPAppMetadata): Promise {

const response = await fetch('/api/mcp-app-host/read-resource', {

method: 'POST',

body: JSON.stringify({ uri: app.resourceUri }),

});

if (!response.ok) {

throw new Error('Failed to load MCP App resource');

}

return response.json();

}

const handlers: MCPAppBridgeHandlers = {

callTool: params =>

fetch('/api/mcp-app-host/call-tool', {

method: 'POST',

body: JSON.stringify(params),

}).then(response => response.json()),

openLink: ({ url }) => {

window.open(url, '_blank', 'noopener,noreferrer');

return {};

},

};

export default function Chat() {

const { messages, sendMessage } = useChat({

transport: new DefaultChatTransport({ api: '/api/chat' }),

});

return (

{messages.map(message =>

message.parts.map((part, index) => {

if (part.type === 'text') {

return

{part.text}

;

}

if (isToolUIPart(part)) {

return (

);

}

return null;

}),

)}

);

}

experimental_MCPAppRenderer renders nothing for ordinary tools. For an app-backed tool, it loads the resource, creates the sandbox bridge, sends tool input and result notifications to the iframe, and forwards supported app requests through your handlers.

Best practices

Treat MCP App HTML as untrusted. Render it in a sandboxed iframe, ideally through a sandbox proxy route on a separate origin.

Never pass app-only tools to the model. Use splitMCPAppTools and expose only the modelVisible tools.

Validate every iframe request on the server before you call client.callTool.

Cache resources by resourceUri so repeated tool calls don't refetch identical HTML.

Keep each tool's content and structuredContent useful on their own, so text-only hosts still work without the UI.

Close short-lived MCP clients when the response or host request finishes.

Next steps

Read the MCP Apps helpers reference for the host-side functions.

See the MCP App Renderer reference for the React component's props.

Learn more about setting up the underlying MCP tools.

Was this helpful?

Read related documentation

AI SDK

Explore more AI SDK guides

Pass state to AI SDK tools and agents with context

Add skills to your AI SDK agents

Generate videos with AI SDK

Add MCP Apps to Your AI SDK Application | AI News Hub