MCP apps UI resource envelopes
std/ui_resource packages interactive HTML widgets as portable UI resource
envelopes that follow the MCP Apps overview and degrade cleanly
to text or structured tool output when a host does not advertise UI support.
import {
ui_resource,
ui_select_for_host,
ui_structured_fallback,
ui_tool_result,
ui_tool_result_validate,
} from "std/ui_resource"
let resource = ui_resource(
"ui://harn-dashboard/kpis@v1",
"Weekly KPIs",
weekly_kpi_html,
{permissions: ["tools/call"], capabilities: ["tools/call", "context/read"]},
)
let result = ui_tool_result(
resource,
{structured_fallback: ui_structured_fallback({signups: 42, churn: 3})},
)
ui_tool_result_validate(result)
let rendered = ui_select_for_host(result, host_capabilities)
Resource envelope
ui_resource(uri, name, html, options?: UiResourceOptions) returns
UiResource (harn.ui_resource.v1):
| Field | Purpose |
|---|---|
uri | ui://... resource URI; hosts fetch this through their MCP resource interface |
mime_type | Defaults to text/html;profile=mcp-app, matching the MCP Apps profile contract |
contents / contents_encoding | UTF-8 (default) or base64-encoded HTML |
content_sha256 / size_bytes | Integrity hash and size for host caches and audit |
permissions | Host-mediated permissions the resource will request (tools/call, context/update, etc.) |
capabilities | JSON-RPC methods the resource may use over postMessage |
csp | Source-list directives Harn surfaces back as a Content-Security-Policy header value via ui_resource_csp_header and a sandbox attribute via ui_resource_sandbox_attr |
validation | Summary of the embedded std/artifact/web validation: ok, error_codes, warning_codes |
meta | Free-form metadata for host-specific extensions |
Validation reuses std/artifact/web
so embedded UI payloads share the same network/secret/dangerous-navigation
rules used by safe artifact patching. The validator defaults to
allow_host_bridge: true because MCP Apps explicitly use
parent.postMessage as the host bridge; tighten the policy by passing
{validation: {allow_host_bridge: false}} to ui_resource.
Tool-declaration metadata
ui_tool_meta(resource, options?: UiToolMetaOptions) returns a
UiToolMeta (harn.ui_tool_meta.v1) envelope and
ui_tool_meta_to_mcp(meta) serializes it into the
camelCase dict shape MCP Apps hosts read from _meta.ui:
| MCP key | Harn field |
|---|---|
resourceUri | ui.resource_uri |
resourceName | ui.resource_name |
profile | ui.profile (defaults to mcp-app) |
visibility | ui.visibility (app_only, model_visible, or always_visible) |
initialView | ui.initial_view (host-side initial state, never trusted as durable storage) |
permissions / capabilities | Mirrored from the resource envelope |
Use visibility: "app_only" for UIs the model should not see in its
own context, model_visible when the structured fallback is also model
context, and always_visible when the same payload is useful in both
places.
Fallbacks
ui_tool_result(resource, options?: UiToolResultOptions) wraps a resource
with a mandatory text fallback (defaulting to a web_artifact_text_fallback
projection of the resource HTML) and an optional UiStructuredFallback.
Wrap raw structured data with
ui_structured_fallback(data, options?: UiStructuredFallbackOptions).
Hosts without UI support receive both fallbacks instead of the
resource:
| Host capability | ui_select_for_host selection |
|---|---|
apps: true and resource validation passed | ui_resource |
| Otherwise, structured fallback present | structured_fallback |
| Otherwise | text_fallback |
ui_host_capabilities(input?: UiHostCapabilityInput) accepts the MCP
client_capabilities.apps shape, the OpenAI Apps SDK ui.apps shape,
or a bare {apps: true} record. ui_host_supports_apps(caps) returns
whether the host can render the mcp-app profile.
Message envelopes
ui_tool_call_envelope(name, params?, options?) produces the
host→guest JSON-RPC tools/call payload a sandboxed iframe receives
through window.parent.postMessage. ui_context_update_envelope(key, value, options?) produces the guest→host envelope used to keep
model-visible context in sync as the user interacts with the widget.
Both default to model_visible: true for context updates so the model
sees the same state the UI does.
Validation contract
ui_tool_result_validate(result) rejects:
- Missing or empty text fallbacks.
- Tool-meta blocks with the wrong schema.
- UI resources whose HTML failed validation (network calls, host bridge abuses, dangerous navigation, embedded secrets).
- Structured fallbacks that do not match the
harn.ui_fallback.structured.v1schema.
ui_tool_result already withholds the resource when validation fails,
so the typical flow is: build the resource, build the result, validate,
then dispatch through ui_select_for_host. Set
allow_invalid_resource: true for preview-only renders where the
host needs to surface validation errors without shipping the resource;
ui_tool_result_validate still refuses that record so previews stay
explicit.
See examples/ui_resource/dashboard-widget.harn
and examples/ui_resource/review-form.harn
for end-to-end vanilla-JS examples.