# MCP Apps in the Web SDK

> Show the interactive views your own MCP servers return (MCP Apps) inside the Runbear chat widget, what you need to turn them on, what a view can do, and the limits.

Source: https://docs.runbear.io/api/web-sdk/mcp-apps

Last updated: 2026-11-13

> **Note**
>
> **Limited availability.** This feature is being rolled out gradually. Contact [support@runbear.io](mailto:support@runbear.io) to enable it for your organization.

An **MCP App** is an interactive view that an MCP server returns with a tool result, as defined by
the [MCP Apps extension](https://github.com/modelcontextprotocol/ext-apps) of the Model Context
Protocol. When the agent calls a tool that has a view, the widget shows that view under the reply: a
dashboard, a form, a map, whatever the server built. The visitor uses it in place, without another
turn.

Use it when an MCP server you connect already ships views, or when the visitor should explore what a
tool returned rather than read a summary of it.

## What you need

- MCP Apps enabled for your organization.
- An agent on the **Anthropic** runtime. Claude Agent SDK agents don't show views yet, and an agent
  in HIPAA data handling never does.
- A [Custom MCP server](/agents/tools/custom-mcp.md) on **Streamable HTTP** or **SSE**, whose tools
  declare a view. Its authentication must be **Static** or **None**. Views from **OAuth**
  connections, shared or per-user, are not shown yet.
- Web SDK 0.7.0 or later.

## Set up

The widget needs no new option: from 0.7.0 it renders views on its own. Check two things on your
side:

1. If your page sends a Content-Security-Policy that restricts frames, add
   `https://runbear-mcp-app-sandbox.runbear.workers.dev` to `frame-src`. That is the sandbox Runbear
   hosts, where every view runs.
2. In proxy mode, forward the two MCP Apps routes listed under
   [Proxy mode](/api/web-sdk/authentication.md#proxy-mode).

`config.mcpApps.sandboxUrl` points the widget at another sandbox. You only need it to test one of
your own. A sandbox on the same origin as your page is refused, and each view then shows as its
text: `Interactive view: <tool name>`.

## How a view behaves

**Where it shows.** The view appears under the reply as soon as the tool returns, so it can arrive
while the agent is still writing. It starts 320 px tall and grows with its content, up to 2,000 px.

**What it can reach.** The view runs in a sandboxed iframe with no origin of its own, inside a
sandbox page on another domain. It can't read your page, its cookies or the visitor's session, and
it can't use cookies or `localStorage` itself. It receives the tool's input and result, and it can
load from or connect to only the domains its MCP server declares for it.

**Calling tools.** A view can call tools on its own MCP server: a tool whose declared visibility
includes `app`, or a tool that has a view of its own and declares no visibility. A tool declared
visible to the model only can't be called from a view. The call runs on Runbear's servers over the
agent's connection to that server, and the arguments are checked against the tool's input schema. It
doesn't start an agent turn and doesn't reach the model. A tool whose visibility is `app` alone is
hidden from the model.

**Sending a message.** A view can post text as the visitor's next message, exactly as if the visitor
had typed it, so it costs a turn. It can send at most one message a second, and only while the
widget is idle, not while a reply is streaming. Unlike a component's options, which
[go inert once answered](/api/web-sdk/response-components.md#a-component-goes-inert-once-it-is-answered), a view
keeps sending after later messages, so a visitor can keep working in it.

**Telling the model what it shows.** A view can report its current state, for example the filters
the visitor set. The widget keeps the latest report of each view and sends the reports of the four
most recently updated views with the visitor's next message. The model reads it for that turn only, as data from the view, not as instructions. It isn't
stored in the conversation, so after a reload the model sees it again only once the view reports
again.

**Links.** A view can open an `http` or `https` link. It opens in a new tab.

**After a reload.** Views come back with the conversation. The widget loads the view from the MCP
server again and hands it the original tool input and result, so the view's own code is the
server's current version.

**When a view can't load.** If the MCP server can't be reached, needs to be reconnected, returns a
view over the size limit, or the view doesn't start within 15 seconds, the widget shows the
component's text instead. Saving the MCP server connection's settings, such as its URL or its key,
has the same effect on every view shown before the save: a view only ever reaches the connection
exactly as it was when the view was shown.

To stop this widget rendering views, add `"mcp-app"` to
[`config.components.disabled`](/api/web-sdk/configuration.md#componentsdisabled). The agent still calls the tool.

## Where views don't appear

Views appear only in conversations through the Web SDK and the public API. In Slack, Teams and every
other channel, and in scheduled runs, the agent calls the same tool and reads its result, but no view
is produced. A client that doesn't read components sees `Interactive view: <tool name>` at the end of
the reply instead, like any other
[unrecognized component](/api/web-sdk/response-components.md#an-unrecognized-component-renders-prose).

The agent's [response component](/api/response-components.md) settings don't govern views: the
**Component frequency** dial and the component switches never turn them on or off. A view does count
toward the four components a message can carry.

## Limits

| What                                      | Limit                                                                                                         |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Views per message                         | At most 4, counted together with every other response component                                               |
| Tool input and result carried by the view | 65,536 characters of JSON together. A larger result produces no view                                          |
| View HTML                                 | 5 MB                                                                                                          |
| Arguments of a tool call from a view      | 65,536 characters of JSON                                                                                     |
| Result of a tool call from a view         | 1,048,576 characters of JSON                                                                                  |
| Views reported to the model               | 4 per message, the most recently updated                                                                      |
| One view's report                         | 16,384 characters of JSON and 20 content blocks. A larger report is refused to the view                       |
| Messages a view sends                     | 1 per second                                                                                                  |
| View loads and tool calls                 | The same per-agent window as chat requests, counted separately from them. Over it, a `429` with `Retry-After` |
| Time for a view to start                  | 15 seconds                                                                                                    |
| View height                               | 2,000 px                                                                                                      |

An agent with [`PreToolUse` or `PostToolUse` hooks](/api/hooks.md) still shows views, but a view can't call
tools on it: those calls are refused with `409` `mcp_app_tool_hooks_unsupported`, because they would
skip the hooks.

## On the API

If you render the API directly rather than through the SDK, a view reaches you as a component named
`mcp-app` (see [Components on the API](/api/web-sdk/response-components.md#components-on-the-api)). Its `props`
carry `integrationId`, `connectionVersion`, `toolName`, `resourceUri`, `toolInput` and `toolResult`. Load and run it with
two endpoints, both accepted with an API key or with the conversation's
[session pass](/api/web-sdk/authentication.md#what-a-session-pass-can-do):

| Endpoint                                              | Body                                                    | Returns                                                                  |
| ----------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------ |
| `POST /v1/threads/{threadId}/mcp-apps/resources/read` | `{ integrationId, connectionVersion, resourceUri }`     | `{ resourceUri, mimeType, html, meta }`, the view's HTML and its `_meta` |
| `POST /v1/threads/{threadId}/mcp-apps/tools/call`     | `{ integrationId, connectionVersion, name, arguments }` | The MCP `CallToolResult`, unchanged                                      |

Render `html` only inside a sandboxed iframe on an origin other than your page's, as the MCP Apps
specification requires. To tell the model what the views show, send their latest reports on the next
run as `config.mcpAppContexts`: up to 4 entries of `{ componentId, toolName, content?,
structuredContent? }`, with at most 20 blocks in `content`. A run with more is refused with `400`.

A `404` covers a thread that doesn't exist, worded like any other missing thread. It also covers a
connection, view or tool that doesn't exist, a connection that never showed a view in this thread or whose settings were saved since,
and an organization without MCP Apps, with the code `mcp_app_not_found`; those cases are
deliberately indistinguishable. So both endpoints serve only an MCP server whose view this
conversation actually showed. A `409` carries one of these
codes:

| Code                             | Meaning                                |
| -------------------------------- | -------------------------------------- |
| `mcp_app_auth_required`          | The MCP server needs to be reconnected |
| `mcp_app_unsupported_connection` | The connection uses OAuth              |
| `mcp_app_too_large`              | The view's HTML is over 5 MB           |
| `mcp_app_result_too_large`       | The tool result is over the limit      |
| `mcp_app_tool_hooks_unsupported` | The agent has tool hooks               |
| `mcp_app_server_unavailable`     | The MCP server couldn't be reached     |
| `mcp_app_server_error`           | The MCP server failed the request      |

A `403` with `organization_suspended` means the organization is suspended; no view request runs until it is reinstated. A `400` is `invalid_resource_uri` for a URI that isn't `ui://`, `arguments_too_large`, or
`invalid_arguments` when the tool's input schema rejects the arguments. Full schemas are in the
[OpenAPI reference](https://api.runbear.io/v1/docs).

## Related

- [Custom MCP](/agents/tools/custom-mcp.md): connect the MCP server
- [Response components](/api/web-sdk/response-components.md): how the widget renders components
- [Configuration](/api/web-sdk/configuration.md#mcpappssandboxurl): `config.mcpApps.sandboxUrl`
- [Authentication](/api/web-sdk/authentication.md#proxy-mode): the routes a proxy forwards
