The host side of the MCP catalog's McpApp component: what the
component renders from its props, and the checks the host makes on
what the embedded app sends over the MCP Apps bridge (see the A2UI
MCP App component specification). A2UIPhoenix.Session.mcp_app_request/5
acts on the requests; priv/static/a2ui_phoenix.js runs the bridge in
the browser.
Every payload the app sends is checked before the host acts on it: no
__proto__, constructor or prototype key at any depth, at most 10
levels of nesting, and at most 64 KiB of JSON.
Summary
Functions
The path a ui/notifications/data-model-change writes: the key's bound
path, extended by subpath (a JSON pointer or a plain key), or nil
when the key is not bound.
Checks a payload from the app; {:error, reason} when it is unsafe.
The app's HTML: htmlContent, decoded when it starts with
url_encoded:, or nil when there is none.
The data model paths the app binds (data.paths), by key, resolved
against context (the component's data scope).
The value at path of the surface's data model (nil when absent).
The reserved data.paths keys: toolInput, toolResult, modelContext.
The current values of the bound paths, by key — what the host keeps
the app in step with. modelContext only flows from the app, so it is
left out.
Functions
The path a ui/notifications/data-model-change writes: the key's bound
path, extended by subpath (a JSON pointer or a plain key), or nil
when the key is not bound.
iex> A2UIPhoenix.MCPApp.change_path(%{"game" => "/game"}, "game", "score")
"/game/score"
iex> A2UIPhoenix.MCPApp.change_path(%{"game" => "/game"}, "game", nil)
"/game"
Checks a payload from the app; {:error, reason} when it is unsafe.
iex> A2UIPhoenix.MCPApp.check_payload(%{"a" => [1, 2]})
:ok
iex> A2UIPhoenix.MCPApp.check_payload(%{"__proto__" => %{}})
{:error, "forbidden key '__proto__'"}
The app's HTML: htmlContent, decoded when it starts with
url_encoded:, or nil when there is none.
iex> A2UIPhoenix.MCPApp.html(%{"htmlContent" => "url_encoded:%3Cp%3EHi%3C%2Fp%3E"})
"<p>Hi</p>"
@spec paths(map(), A2UICore.Resolution.DataContext.t()) :: %{ optional(String.t()) => String.t() }
The data model paths the app binds (data.paths), by key, resolved
against context (the component's data scope).
@spec read(A2UICore.State.Surface.t(), String.t()) :: term()
The value at path of the surface's data model (nil when absent).
@spec reserved_keys() :: [String.t()]
The reserved data.paths keys: toolInput, toolResult, modelContext.
@spec state(map(), A2UICore.State.Surface.t(), A2UICore.Resolution.DataContext.t()) :: map()
The current values of the bound paths, by key — what the host keeps
the app in step with. modelContext only flows from the app, so it is
left out.