# MCP Apps
The [MCP Apps extension](https://github.com/modelcontextprotocol/ext-apps) 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.
```typescript
import { MCPServer } from '@mastra/mcp'
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
const calculatorTool = createTool({
id: 'calculatorWithUI',
description: 'An interactive calculator',
inputSchema: z.object({
num1: z.number(),
num2: z.number(),
operation: z.enum(['add', 'subtract']),
}),
execute: async ({ num1, num2, operation }) => {
const result = operation === 'add' ? num1 + num2 : num1 - num2
return {
content: [{ type: 'text', text: 'An interactive calculator is displayed.' }],
structuredContent: { result },
}
},
})
const server = new MCPServer({
id: 'my-app-server',
name: 'My App Server',
version: '1.0.0',
tools: { calculatorTool },
appResources: {
'ui://calculator/main': {
name: 'Interactive Calculator',
html: `
Calculator
`,
},
},
})
```
Link the tool to its app resource by adding `_meta.ui.resourceUri` to the tool definition:
```typescript
calculatorTool._meta = {
ui: { resourceUri: 'ui://calculator/main' },
}
```
> **Note:** Visit [MCPServer reference](https://mastra.ai/reference/tools/mcp-server) for the full `appResources` configuration.
## Connecting MCP Apps to agents
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.
```typescript
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.
```typescript
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:
```typescript
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:
1. The tool executes and returns a brief summary in `content` (visible to the model) and detailed data in `structuredContent` (visible to the UI only).
2. The host renders the app HTML in a sandboxed iframe.
3. The iframe communicates with the host via a JSON-RPC postMessage protocol.
4. The app can call server tools using `callServerTool()` and inject messages into the chat using `sendMessage()`.
```text
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.
```typescript
execute: async ({ num1, num2, operation }) => {
const result = operation === 'add' ? num1 + num2 : num1 - num2
return {
content: [{ type: 'text', text: 'An interactive calculator is displayed.' }],
structuredContent: { result },
}
}
```
## App API (guest-side)
MCP App HTML uses the standard [`App` class from `@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) to communicate with the host. Import it via ESM CDN or bundle it.
```javascript
import { App } from 'https://cdn.jsdelivr.net/npm/@modelcontextprotocol/ext-apps/+esm'
const app = new App({ name: 'MyApp', version: '1.0.0' })
```
### `app.callServerTool(params)`
Calls an MCP server tool from within the iframe. This is useful for interactive computation without leaving the UI.
```javascript
const result = await app.callServerTool({
name: 'calculatorWithUI',
arguments: { num1: 42, num2: 8, operation: 'add' },
})
```
### `app.sendMessage(params)`
Injects a user message into the agent chat, triggering a new model turn. Use this for sharing results or requesting follow-up actions.
```javascript
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.
```javascript
app.ontoolinput = params => {
document.getElementById('num1').value = params.arguments.num1
}
```
> **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:
>
> ```html
>
>
> ```
### `app.connect()`
Establishes the connection to the host. Call this after registering all event handlers.
```javascript
await app.connect()
```
> **Note:** See the [`App` class API reference](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html) 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.
```typescript
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](https://mastra.ai/reference/tools/mcp-client) for more details on proxying external servers.
## Sandbox security
Mastra Studio uses [`@mcp-ui/client`](https://www.npmjs.com/package/@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.
## Related
- [MCP overview](https://mastra.ai/docs/mcp/overview)
- [MCPServer reference](https://mastra.ai/reference/tools/mcp-server)
- [MCPClient reference](https://mastra.ai/reference/tools/mcp-client)
- [MCP Apps extension spec](https://github.com/modelcontextprotocol/ext-apps)