Guide for developers using Mastra to add interactive HTML UIs to MCP tools via the MCP Apps extension, enabling rich UI rendering in Mastra Studio.
Insight
The MCP Apps extension (from @modelcontextprotocol/ext-apps) enables MCP tools to serve interactive HTML UIs via ui:// resources. Tools define app resources on MCPServer using the appResources config with ui:// URIs mapped to inline HTML or file paths. Each tool is linked to its app resource via the _meta.ui.resourceUri property on the tool definition. The App class from @modelcontextprotocol/ext-apps provides three core guest-side APIs: callServerTool() calls MCP server tools from within the iframe for interactive computation; sendMessage() injects user messages into the agent chat to trigger new model turns; and ontoolinput is a callback that fires when the host delivers tool input data to the iframe, allowing pre-population of form fields. Communication between host and iframe uses a JSON-RPC postMessage protocol. The communication flow is: agent calls tool → tool returns brief content text plus structuredContent data → host renders iframe with app HTML → user interacts with UI → UI calls callServerTool() for computation → UI calls sendMessage() to inject results into chat. For remote MCP servers, use MCPClient.listTools() to get tools and toMCPServerProxies() to register the server so Studio can resolve app resources. Sandbox security uses @mcp-ui/client to render MCP App iframes through a sandbox proxy with allow-scripts, allow-forms, and allow-popups permissions. The iframe does not access the parent page’s DOM, cookies, or storage.
Hành động
To add an interactive UI to an MCP tool: 1) Create a tool with createTool() and ensure its execute function returns both content (brief text summary for the model) and structuredContent (data payload for the UI). 2) On your MCPServer, define appResources with a ui:// URI mapped to HTML (inline or file path). 3) Link the tool to its app via calculatorTool._meta = { ui: { resourceUri: ‘ui://calculator/main’ } }. 4) Register the MCP server at the Mastra level in the mcpServers config so Studio can resolve app resources. 5) For remote servers, use new MCPClient() with server config, pass await mcpClient.listTools() to agent tools, and spread …mcpClient.toMCPServerProxies() in Mastra mcpServers. In the app HTML, import App from the ESM CDN, register ontoolinput callback for hydration, implement button handlers that call app.callServerTool(), and call await app.connect() after registering handlers. To prevent UI flicker with default form values, start body with opacity: 0 and add the ‘ready’ class after ontoolinput hydrates or as a 150ms fallback timeout.
Kết quả
Mastra Studio renders the app HTML in a sandboxed iframe alongside the tool form or inline in agent chat. Users interact with the UI to call server tools and inject results back into the conversation, while the model sees only the brief text content from the tool response.
Điều kiện áp dụng
Requires @modelcontextprotocol/ext-apps package for guest-side App class and @mcp-ui/client for host-side iframe rendering. MCP Apps extension must be implemented by the MCP server.
Nội dung gốc (Original)
MCP Apps
The MCP Apps extension allows MCP tools to serve interactive HTML UIs via ui:// resources. When a tool has an associated app resource, Mastra Studio renders it in a sandboxed iframe alongside the tool form or inline in agent chat.
When to use MCP Apps
Use MCP Apps when a tool result is better presented as an interactive UI rather than plain text. For example:
A calculator that renders input fields and buttons for computation
A color picker that displays swatches and hex values
A form builder that captures structured user input
A data visualizer that renders charts
Quickstart
Define app resources on your MCPServer by providing a ui:// URI mapped to inline HTML or an HTML file path.
Agents consume tools — they do not need to know about MCP servers. Pass tools to the agent’s tools config, and register the MCP server at the Mastra level so Studio can resolve app resources.
import { Agent } from '@mastra/core/agent'import { calculatorTool } from '../mcp/tools'export const myAgent = new Agent({ id: 'my-agent', name: 'My Agent', instructions: 'You have access to interactive UI tools.', model: 'openai/gpt-5-mini', tools: { calculatorTool },})
Register the MCP server at the Mastra level. Studio scans registered MCP servers to map tools to their app resources.
import { Mastra } from '@mastra/core/mastra'import { myAgent } from './agents'import { myAppServer } from './mcp/server'export const mastra = new Mastra({ agents: { myAgent }, mcpServers: { myAppServer },})
For remote MCP servers, use MCPClient.listTools() to get tools and toMCPServerProxies() to register the server:
import { MCPClient } from '@mastra/mcp'const mcpClient = new MCPClient({ servers: { remoteApp: { url: new URL('https://remote-mcp-server.example.com/mcp') }, },})const myAgent = new Agent({ id: 'my-agent', name: 'My Agent', model: 'openai/gpt-5-mini', tools: await mcpClient.listTools(),})export const mastra = new Mastra({ agents: { myAgent }, mcpServers: { ...mcpClient.toMCPServerProxies() },})
When tools come from MCPClient.listTools(), each tool’s _meta.ui is automatically stamped with a serverId so Studio can resolve its app resources without scanning all servers.
How MCP Apps work
MCP Apps follow a specific communication pattern between the host (Mastra Studio) and the iframe:
The tool executes and returns a brief summary in content (visible to the model) and detailed data in structuredContent (visible to the UI only).
The host renders the app HTML in a sandboxed iframe.
The iframe communicates with the host via a JSON-RPC postMessage protocol.
The app can call server tools using callServerTool() and inject messages into the chat using sendMessage().
Agent calls tool → Tool returns brief content + structuredContent → Host renders iframe with app HTML → User interacts with UI → UI calls callServerTool() for computation → UI calls sendMessage() to inject result into chat
Tool result format
Tools with app resources should return two fields:
content: A brief text summary for the model. Keep this short so the agent does not parrot the full result.
structuredContent: The data payload that hydrates the UI. The model does not see this field.
Injects a user message into the agent chat, triggering a new model turn. Use this for sharing results or requesting follow-up actions.
await app.sendMessage({ role: 'user', content: [{ type: 'text', text: 'The result of 42 + 8 is 50' }],})
app.ontoolinput
A callback that fires when the host delivers tool input data to the iframe, allowing pre-population of form fields. The params.arguments object contains the tool call arguments.
Preventing UI flicker: If your app has default form values, the user may briefly see them before ontoolinput hydrates the correct values. To prevent this, start the body hidden and reveal it after hydration:
<style> body { opacity: 0; transition: opacity 0.15s; } body.ready { opacity: 1; }</style><script type="module"> import { App } from 'https://cdn.jsdelivr.net/npm/@modelcontextprotocol/ext-apps/+esm' const app = new App({ name: 'MyApp', version: '1.0.0' }) app.ontoolinput = params => { // Hydrate form fields from params.arguments document.body.classList.add('ready') } await app.connect() // Fallback: reveal after connection if no tool input arrives setTimeout(() => document.body.classList.add('ready'), 150)</script>
app.connect()
Establishes the connection to the host. Call this after registering all event handlers.
await app.connect()
Note: See the App class API reference for the full list of methods, callbacks, and lifecycle hooks.
Using external MCP servers with apps
External (non-Mastra) MCP servers that implement the MCP Apps extension work with Mastra via MCPClient. Use listTools() for agent tools and toMCPServerProxies() to register them in Studio.
import { Mastra } from '@mastra/core/mastra'import { MCPClient } from '@mastra/mcp'import { Agent } from '@mastra/core/agent'const mcpClient = new MCPClient({ servers: { 'external-server': { command: 'node', args: ['path/to/external-server.js'], }, },})const myAgent = new Agent({ id: 'my-agent', name: 'My Agent', model: 'openai/gpt-5-mini', tools: await mcpClient.listTools(),})export const mastra = new Mastra({ agents: { myAgent }, mcpServers: { ...mcpClient.toMCPServerProxies(), },})
Note: Visit MCPClient reference for more details on proxying external servers.
Sandbox security
Mastra Studio uses @mcp-ui/client to render MCP App iframes through a sandbox proxy. The proxy loads app HTML via postMessage rather than srcDoc, providing additional isolation.
App iframes are sandboxed with the following permissions:
allow-scripts: Enables JavaScript execution
allow-forms: Allows form submission
allow-popups: Permits window.open() and link targets
The iframe does not have access to the parent page’s DOM, cookies, or storage. All communication happens through the JSON-RPC postMessage protocol managed by @mcp-ui/client’s AppRenderer on the host side and @modelcontextprotocol/ext-apps’s App class on the guest side.