# 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.

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_URL` is 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:

1. The console frames a small **sandbox proxy** page with `sandbox="allow-scripts"`. Without `allow-same-origin`, the proxy has an opaque origin, so it has no cookies, no storage and no access to the console.
2. The proxy frames the view's own HTML, also with `sandbox="allow-scripts"`. It adds `allow-forms` only when the server asks for that permission. The proxy passes messages between the view and the console.
3. Liyagent serves the view's HTML under a Content Security Policy built from the `_meta.ui.csp` its server declared. By default the view cannot connect, frame or load anything outside itself. Only declared `https://` origins are added. Any other entry (`http:`, a path, a stray directive) is dropped and recorded in the audit log.
4. 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/call` for 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 needs `agents.read`.
- `POST /api/mcp-apps/{id}/call` takes `{name, arguments}` and returns a `CallToolResult`. It needs `agents.run` and the Cedar `invoke` on the agent.
