Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 139 additions & 2 deletions specification/draft/apps.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,18 @@ interface UIResourceMeta {
* - omitted: host decides border
*/
prefersBorder?: boolean,
/**
* MIME types of dynamic content payloads this View renders
*
* When present, the View acts as a renderer for typed payloads returned
* by its associated tools as embedded resources (see Data Passing:
* Dynamic View Content). Does not affect the resource's own `mimeType`,
* which remains `text/html;profile=mcp-app`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a comment on native UI host to this spec change?

It's in the conversation of this PR.

Maybe just have a section saying

mimeTypes can support other types besides text/html;profile=mcp-app. If that's supported the dynamic view content is rendered using the host's native renderer. How that's implemented is done by the mimeType's SDKs. Thsi is useful for natively rendering content such as in mobile apps that use native components over iframes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of this PR is actually to explicitly not change the mimeType the hosts need to render or understand - but to standardize the "bridge" between any other mimeType and the text/html;profile=mcp-app that MCP Apps hosts already know how to render.
Any additional mimeType top-level support is out of this PR's scope and should be discussed carefully to understand if it should even be under the MCP Apps spec

*
* @example
* ["application/a2ui+json"]
*/
contentMimeTypes?: string[],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. As far as I can tell, this lets a single resource (likely a static renderer) handle multiple EmbeddedResource types in a single tool call result, right? So I think it unlocks things like:

  1. A ui://chart-viewer static resources is packaged with two static renderers: A2UI renderer, and a Geo JSON renderer, for example. The resource declares support for both A2UI and geo JSON mime types here.
  2. The iframe loads, and when the tool call response comes in, it has two EmbeddedResources:
    • One containing a bunch of A2UI bytes
    • A second containing a bunch of geo JSON bytes
  3. Each embedded resource result gets injected into the iframe
  4. The iframe has some orchestration code that knows how to handle:
    • Embedded resource A2UI bytes ➡️ pass it to the A2UI renderer
    • Embedded resource Geo JSON bytes ➡️ pass it to the geo JSON renderer

Do I have this right? Are there known use cases where a single view wants to mix types like this? If it's not common / nobody will use it, I'm wondering if we should just make this a single string for now, but I guess that makes forward compat hard if the use case ever arises...

}
```

Expand DownExpand Up@@ -254,6 +266,7 @@ The resource content is returned via `resources/read`:
};
domain?: string;
prefersBorder?: boolean;
contentMimeTypes?: string[]; // Dynamic content payload types this View renders
};
};
}];
Expand All@@ -262,7 +275,7 @@ The resource content is returned via `resources/read`:

#### Metadata Location

`UIResourceMeta` (CSP, permissions, domain, prefersBorder) may be provided on either or both:
`UIResourceMeta` (CSP, permissions, domain, prefersBorder, contentMimeTypes) may be provided on either or both:

- **`resources/list`:** On the resource entry's `_meta.ui` field. Useful as a static default that hosts can review at connection time.
- **`resources/read`:** On each content item's `_meta.ui` field. Useful for per-response overrides or dynamic metadata that is only known at read time.
Expand DownExpand Up@@ -1377,6 +1390,8 @@ View behavior (optional):

Host MUST send this notification when tool execution completes (if the View is displayed during tool execution).

When the host has negotiated dynamic content support (see Client\<\>Server Capability Negotiation), the delivered `CallToolResult` MUST include, unmodified, any embedded resource content blocks marked with `_meta.ui.content` (see Data Passing: Dynamic View Content). Hosts MAY omit unmarked content blocks per their existing policies.

`ui/notifications/tool-cancelled` - Tool execution was cancelled

```typescript
Expand DownExpand Up@@ -1718,6 +1733,7 @@ The tool's execution result:

- `content`: Text representation for model context and text-only hosts
- `structuredContent`: Structured data optimized for UI rendering (not added to model context)
- Marked embedded resources: Typed dynamic content payloads for the View (not added to model context; see Dynamic View Content below)
- `_meta`: Additional metadata (timestamps, version info, etc.) not intended for model context

#### 3. Interactive Updates
Expand All@@ -1735,6 +1751,90 @@ This pattern enables interactive, self-updating views.

Note: Tools with `visibility: ["app"]` are hidden from the agent but remain callable by apps via `tools/call`. This enables UI-only interactions (refresh buttons, form submissions) without exposing implementation details to the model. See the Visibility section under Resource Discovery for details.

#### 4. Dynamic View Content (via embedded resources)

Some UI systems are generative in nature: the server produces a declarative, typed UI description at tool-call time (a document, not code), and a generic predeclared View renders it. [A2UI](https://a2ui.org) (`application/a2ui+json`) is the primary example. `structuredContent` is a poor fit for these payloads: it is untyped (no MIME type), single-valued, bound to the tool's `outputSchema`, and offers no interoperability path for non-MCP-Apps hosts that natively render the payload format.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "document" the right word here? It definitely describes JSON (since "JSON documents"), but how about:

Some UI systems are generative in nature: the server produces a declarative, typed UI description at tool-call time (a UI schema, not executable code),

Thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally like Vercel's JSON renderer and its use of specs to reference UI declaration content (source)

A spec (specification) is the actual JSON that describes a UI. It uses components from a catalog and can optionally follow a schema. Specs can be:

  • Generated by AI in real-time
  • Stored in a database
  • Streamed progressively from a server
  • Hand-authored as JSON files

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and offers no interoperability path for non-MCP-Apps hosts that natively render the payload format.

I'm not sure I follow this part. I agree with the other limitations of structuredContent, but what is the interop downside?


For these cases, tool results MAY carry dynamic content payloads as standard MCP embedded resource content blocks, marked for View consumption:

```typescript
interface McpUiContentBlockMeta {
/**
* URI of the ui:// renderer this payload targets.
*
* OPTIONAL. If omitted, the payload targets the calling tool's
* `_meta.ui.resourceUri`. Explicit targeting supports future
* multi-view tool results.
*/
rendererUri?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The need for this escapes me. At first, I thought it's giving tool authors the chance to return an embedded resource whose bytes don't match any mime type declared in the resource's UIResourceMeta#contentMimeTypes... thus letting an iframe fetch a supporting renderer to make use of otherwise-foreign declarative UI bytes in a tool call result. But I guess it doesn't allow an embedded resource's mimeType drift out of sync from the view's contentMimeTypes—we still impose the subset requirement between the two.

Rather, it just lets an individual embedded resource elect to not be rendered inside of a pre-existing resource, but rather spin up a new view altogether? Is that it? Is this necessary now, or can it be added when a use case really depends on it?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems duplicative with the contentMimeType, maybe just pick one or the other? The rendererUri is a bit more flexible (it doesn't assume every mimetype is going to the same ui resource)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wrenj there's no duplication actually, as contentMimeTypes is not a reference mechanism but just a guardrail. The reference is being done by rendererUri (or resourceUri if omitted)

}

// Embedded resource content block within CallToolResult.content:
{
type: "resource",
resource: {
uri: string, // Ephemeral payload identifier (any scheme except ui://)
mimeType: string, // MUST match a declared contentMimeTypes entry
text?: string, // Payload as string
blob?: string // OR base64-encoded payload
},
_meta: {
ui: {
content: McpUiContentBlockMeta
}
}
}
```

**Requirements:**

- The target View MUST declare the payload's `mimeType` in its `contentMimeTypes` (see UI Resource Format)
- Payload URIs are ephemeral identifiers per RFC 3986; servers are NOT required to serve them via `resources/read`. The `ui://` scheme MUST NOT be used for payload URIs; it remains reserved for renderable UI resources
- A tool result MAY contain multiple marked payloads; Views SHOULD process them in array order
- Marked payloads are presentation data. Consistent with `structuredContent`, servers SHOULD still return a meaningful text `content` block for model context and text-only hosts

**Host behavior** (when dynamic content support is negotiated, for tools linked to a View declaring `contentMimeTypes`):

- Host MUST deliver marked embedded resource blocks, unmodified, in the `CallToolResult` sent via `ui/notifications/tool-result`
- Host MUST deliver marked embedded resource blocks, unmodified, in `tools/call` responses returned to Views during the interactive phase
- Host SHOULD NOT add marked payloads to model context (they are presentation data, analogous to `structuredContent`). Host MAY note their presence to the model
- Host MAY drop marked blocks whose `mimeType` is not declared in the target View's `contentMimeTypes`, and SHOULD log such drops
- Host MAY enforce payload size limits; when dropping a payload, Host SHOULD deliver the remainder of the result rather than failing

No new messages are introduced: the existing `ui/notifications/tool-result` notification and proxied `tools/call` responses are the delivery channel. The View extracts marked payloads from the delivered result and renders them; interactive updates return new payloads through the same loop.

**Example (A2UI renderer):**

```json
// Renderer resource (predeclared, prefetchable, reviewable)
{
"uri": "ui://a2ui-server/renderer",
"name": "a2ui_renderer",
"mimeType": "text/html;profile=mcp-app",
"_meta": {
"ui": { "contentMimeTypes": ["application/a2ui+json"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this requires all a2ui mimetypes to go to the same mcp resource, what if we want different resources to handle different toolcalls, eg
https://github.com/a2ui-project/a2ui/blob/main/samples/community/mcp/a2ui-in-mcpapps/server/server.py#L54-L69

where two different resources are used ot render a2ui

@liadyliadyAug 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preferably the renderer should be generic and not dependant on a specific tool call result. It should just be the bridge that converts any A2UI resources (returned from every tool call) into MCP Apps compatible content. There's no actualy duplication as contentMimeTypes is not a reference mechanism, but a guardrail

}
}

// Tool result
{
"content": [
{ "type": "text", "text": "Found 3 flights TLV→SFO. Best: UA954, $1,240." },
{
"type": "resource",
"resource": {
"uri": "a2ui://a2ui-server/surfaces/flight-search-8f3a",
"mimeType": "application/a2ui+json",
"text": "{\"beginRendering\":{...}}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do we handle multiple createSurface calls? A2UI can create multiple surfaces, eg each conversation turn we load a new UI inline. Are all tool cals going to the same iframe for the mcp apps resource

},
"_meta": { "ui": { "content": {} } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this preferred to putting

"_meta": { "ui": { "content": { ui_host: "ui://a2ui-server/renderer" } } }

and not pre-registering the mime type with a ui resource

}
]
}
```

The renderer translates payload-level events into `tools/call` requests (typically to tools with `visibility: ["app"]`); responses carry new marked payloads (e.g., incremental A2UI updates), which the renderer applies. A host that natively renders a payload format MAY instead advertise that format in the top-level `mimeTypes` extension setting, render marked payloads directly, and skip instantiating the HTML renderer — allowing servers to serve a single tool response to MCP Apps hosts, native hosts, and text-only hosts.

### App-Provided Tools

Apps can register their own tools that hosts and agents can call, making apps **introspectable and accessible** to the model. This complements the existing capability where apps call server tools (via host proxy).
Expand DownExpand Up@@ -2191,7 +2291,8 @@ Clients advertise MCP Apps support in the initialize request using the extension
"capabilities": {
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
"mimeTypes": ["text/html;profile=mcp-app"],
"contentMimeTypes": ["application/a2ui+json"]
}
}
},
Expand All@@ -2206,6 +2307,7 @@ Clients advertise MCP Apps support in the initialize request using the extension
**Extension Settings:**

- `mimeTypes`: Array of supported content types (REQUIRED, e.g., `["text/html;profile=mcp-app"]`)
- `contentMimeTypes`: Array of dynamic content payload types the host will forward to Views per Data Passing: Dynamic View Content (OPTIONAL). Hosts MAY advertise `["*"]` to indicate they forward any payload type declared by a View's `contentMimeTypes` — hosts never need to interpret payloads, only route them into the sandboxed View. Servers SHOULD check this setting before registering renderer-pattern tools and SHOULD degrade to text-only or `structuredContent`-driven variants when absent. A host that natively renders a payload format (without an HTML renderer) advertises that format in `mimeTypes` instead.

Future versions may add additional settings:

Expand DownExpand Up@@ -2473,6 +2575,25 @@ Apps are forward-deployed emanations of server tools, running in the client cont

See [Security Implications: App-Provided Tools Security](#5-app-provided-tools-security) for detailed considerations.

#### 7. Dynamic View Content via Embedded Resources

**Decision:** Allow tool results to carry typed dynamic content payloads as marked embedded resources, delivered to the tool's predeclared View through existing channels.

**Rationale:**

- Enables generative UI formats (e.g., A2UI) where the server emits a declarative UI document at call time and a generic predeclared renderer interprets it
- Embedded resources are typed (MIME), multi-valued, URI-addressed, and part of core MCP — unlike `structuredContent`, which remains the channel for template-bound data
- Non-MCP-Apps hosts that natively render a payload format can consume the same tool response by reading the marked embedded resource directly, enabling one server response across host classes
- Requires no new messages: delivery rides on `ui/notifications/tool-result` and proxied `tools/call` responses

This does not revisit decision #1 (Predeclared Resources vs. Inline Embedding): the renderer remains a predeclared, prefetchable, reviewable `ui://` resource. Embedded resources here carry only data payloads consumed by that renderer — the same template/data split as `structuredContent`, extended with typed, self-describing documents.

**Alternatives considered:**

- **`structuredContent`:** Untyped, single-valued, `outputSchema`-bound, and invisible to native payload-format hosts
- **Dedicated `ui/notifications/content` message:** Duplicates delivery semantics, complicates ordering relative to `tool-result`, and grows host surface area for no expressive gain. Server-push content injection outside tool calls can be added later as a separate message
- **MIME-type inference without a marker:** Servers legitimately return embedded resources for other purposes (files, records); the explicit `_meta.ui.content` marker makes routing intent unambiguous and provides a forward-compatible `rendererUri` slot for multi-view results

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not this route?


### Backward Compatibility

The proposal builds on the existing core protocol. There are no incompatibilities.
Expand DownExpand Up@@ -2642,6 +2763,20 @@ App tools MUST be tied to the app's lifecycle:
- Hosts MUST NOT persist app tool registrations across sessions
- Calling a tool from a closed app MUST return an error

#### 6. Dynamic Content Payloads

Dynamic View Content payloads are data, not code: they are interpreted by a renderer that is itself sandboxed, CSP-constrained, and reviewable under this specification's existing model. Declarative formats narrow the attack surface relative to arbitrary HTML precisely because the executable component (the renderer) is static and predeclared.

**View behavior:**

- Renderers MUST treat payloads as untrusted input (no `eval` or direct `innerHTML` of payload-derived strings)
- Payload-referenced network and media origins remain subject to the renderer's declared CSP; a payload cannot expand the View's network reach

**Host behavior:**

- Payloads flow through auditable JSON-RPC with declared MIME types; hosts MAY log, size-limit, and type-filter them
- Marked payloads are excluded from model context by default, limiting prompt injection surface

### Other risks

- **Social engineering:** UI can still display misleading content. Hosts should clearly indicate sandboxed UI boundaries.
Expand All@@ -2651,3 +2786,5 @@ App tools MUST be tied to the app's lifecycle:

- The resource prefix `ui://` will be reserved for MCP Apps
- The label `io.modelcontextprotocol/ui` is reserved
- The `_meta.ui.content` key on tool result content blocks is reserved for Dynamic View Content
- The `contentMimeTypes` field is reserved in `UIResourceMeta` and in the `io.modelcontextprotocol/ui` extension settings
Loading