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.
An MCP App is an interactive view that an MCP server returns with a tool result, as defined by the MCP Apps extension 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 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:
- If your page sends a Content-Security-Policy that restricts frames, add
https://runbear-mcp-app-sandbox.runbear.workers.devtoframe-src. That is the sandbox Runbear hosts, where every view runs. - In proxy mode, forward the two MCP Apps routes listed under 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, 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. 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.
The agent's response component 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 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). 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:
| 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.
Related#
- Custom MCP: connect the MCP server
- Response components: how the widget renders components
- Configuration:
config.mcpApps.sandboxUrl - Authentication: the routes a proxy forwards