Interactive tool views (MCP Apps)
Show the interactive views that MCP servers attach to their tools, sandboxed in web chat and the playground, and opened from a card in Teams.
- Know when a tool shows a view
- Explain how a view is sandboxed
- Control which tools the model and the view can call
- Turn the feature off
Some MCP servers attach an interactive view to a tool: a chart, a form or an issue card. This uses the MCP Apps extension (io.modelcontextprotocol/ui). The tool names a ui:// resource in its _meta.ui.resourceUri. After the agent calls the tool, Liyagent reads that resource and shows the view to the person who asked, beside the agent's answer.
The model still reads the tool's ordinary result. The view is extra, and it never changes what the agent is told. A server without UI resources works exactly as before.
Where views appear
- Web chat and the agent playground show the view inline, in a sandboxed frame.
- Teams cannot run a server's HTML. The card names the view and has an Open button. It opens the view on a Liyagent page, where you sign in as usual. The button only appears when
TICKETIQ_PUBLIC_BASE_URLis an https address.
A view is kept for 7 days. Only the person the run was for can open it. A view from a run that had no person (a trigger, for example) can be opened by the tenant's agent readers.
How a view is sandboxed
A view's HTML comes from a third-party server, so it never runs with your Liyagent session:
- The console frames a small sandbox proxy page with
sandbox="allow-scripts". Withoutallow-same-origin, the proxy has an opaque origin, so it has no cookies, no storage and no access to the console. - The proxy frames the view's own HTML, also with
sandbox="allow-scripts". It addsallow-formsonly when the server asks for that permission. The proxy passes messages between the view and the console. - Liyagent serves the view's HTML under a Content Security Policy built from the
_meta.ui.cspits server declared. By default the view cannot connect, frame or load anything outside itself. Only declaredhttps://origins are added. Any other entry (http:, a path, a stray directive) is dropped and recorded in the audit log. - The frame URLs carry a signed token that expires after 10 minutes. Without that token, neither page is served.
Liyagent trusts a message by the window it came from, never by its origin, because both origins are opaque.
What a view can do
A view talks to Liyagent with the MCP Apps JSON-RPC messages:
| Message | What Liyagent does |
|---|---|
ui/initialize | Answers with the host's capabilities and context (theme, locale). |
ui/notifications/initialized | Sends the tool's arguments (tool-input) and its result (tool-result). |
tools/call | Runs a tool from the same server, but only one whose visibility includes "app". The call goes through the agent's ordinary governed path: its policies, data rules and audit. |
ui/open-link | Opens https:// links in a new tab. Other links are refused. |
ui/message | Hands the message to the page. It is never sent to the model. |
ui/notifications/size-changed | Resizes the frame, within limits. |
Any other request gets -32601 (method not found).
Tool visibility
A tool's _meta.ui.visibility lists who may call it: "model", "app" or both. Both is the default.
- A tool without
"model"is never offered to the agent's model. The run notes say why. - A view's
tools/callfor a tool without"app"is refused with 403 and audited.
Audit
| Action | When |
|---|---|
agent.mcp_app.view | A view was made: the server, tool, ui:// URI, the HTML's SHA-256 and the CSP that applied. |
agent.mcp_app.refused | The resource was not a valid MCP App (wrong MIME type, not ui://, too large). The agent's answer still stands. |
agent.mcp_app.call | A view called a tool, with the result ok, error or refused. |
Turn it off
TICKETIQ_MCP_APPS_ENABLED is on by default. When it is off, Liyagent does not advertise the extension, offers every tool to the model as before, and makes no views.
API
GET /api/mcp-apps/{id}returns a view's sandbox URLs (each with a fresh token), the tool's arguments and its result. It needsagents.read.POST /api/mcp-apps/{id}/calltakes{name, arguments}and returns aCallToolResult. It needsagents.runand the Cedarinvokeon the agent.