Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

MCP Apps UI resources

std/ui_resource packages interactive HTML widgets as portable UI resource records that follow the MCP Apps overview and fall back to text or structured tool output when a host does not advertise UI support.

Run an app locally

Register at least one UI resource and one linked tool, then launch the same script in Harn's standalone host:

harn app run examples/apps/logo-studio.harn

The host binds to a random loopback port, opens the default browser, and places the app inside two nested frames. The middle frame uses a separate loopback origin and applies the app's permissions and content security policy. Use --no-open in automation, --bind 127.0.0.1:4321 for a stable port, or --resource ui://... to select among multiple declared views. Non-loopback binds are rejected; deploy remote apps through an authenticated host instead.

The logo studio canvas after a live tool round trip

For new applications, use std/ui so product state and behavior stay in Harn. Use std/ui_resource directly when you already have a portable HTML view.

The app view speaks standard MCP JSON-RPC over postMessage. Calls to tools/call and resources/read are checked by the host and sent to the same in-process MCP server used by harn serve mcp; app-only tool visibility is therefore enforced at the protocol boundary rather than by UI convention. RPC requests must also carry the exact host origin, which blocks another browser origin or a DNS-rebinding page from driving tools through the loopback server.

For each View-initiated tools/call, the standalone host sends ui/notifications/tool-input before it calls the server. It returns the matching JSON-RPC response, then sends ui/notifications/tool-result after a successful CallToolResult. A server or transport error returns an error and does not send a successful-result notification. Calls such as resources/read receive their response without tool input or result notifications. These messages follow the stable MCP Apps communication protocol, including for Views that do not use Harn's shared renderer.

The View must send one ui/initialize request with protocolVersion, appInfo, and appCapabilities, then send ui/notifications/initialized. The standalone host rejects tool and resource requests until both steps finish. A View that supplies availableDisplayModes must include fullscreen because the standalone host fills its browser window. The host advertises tool and resource access without listChanged, because it does not forward notifications when the available tools or resources change. It also rejects ui/update-model-context: a standalone app has no conversation or model context to update. It does not forward server requests that MCP Apps does not define. Calls that can cause work, such as tools/call, must carry a JSON-RPC request ID.

The sandbox keeps ui/notifications/sandbox-* messages between the host and the sandbox frame. App HTML cannot send or receive those reserved messages. The host accepts only origin-shaped CSP domains and the four MCP Apps browser permissions: camera, microphone, location, and clipboard writing. The app does not start when this metadata is malformed. The host normalizes it once and reports the settings it enforces in hostCapabilities.sandbox.

import {
  ui_resource,
  ui_resource_to_mcp,
  ui_select_for_host,
  ui_structured_fallback,
  ui_tool_result,
  ui_tool_result_validate,
} from "std/ui_resource"

const resource = ui_resource(
  "ui://harn-dashboard/kpis@v1",
  "Weekly KPIs",
  weekly_kpi_html,
  {capabilities: ["tools/call", "resources/read"]},
)

harness.tools.mcp_resource(ui_resource_to_mcp(resource))

const result = ui_tool_result(
  resource,
  {structured_fallback: ui_structured_fallback({signups: 42, churn: 3})},
)

ui_tool_result_validate(result)
const rendered = ui_select_for_host(result, host_capabilities)

Resource record

ui_resource(uri, name, html, options?: UiResourceOptions) returns UiResource (harn.ui_resource.v1):

FieldPurpose
uriui://... resource URI; hosts fetch this through their MCP resource interface
mime_typeDefaults to text/html;profile=mcp-app, matching the MCP Apps profile contract
contents / contents_encodingUTF-8 (default) or base64-encoded HTML
content_sha256 / size_bytesIntegrity hash and size for host caches and audit
permissionsHarn-level capability labels; browser permissions belong in ui_resource_to_mcp options
capabilitiesJSON-RPC methods the resource may use over postMessage
cspSource-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
validationSummary of the embedded std/artifact/web validation: ok, error_codes, warning_codes
metaFree-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) record and ui_tool_meta_to_mcp(meta) serializes it into the stable MCP Apps shape served from a tool's _meta.ui:

MCP keyHarn field
resourceUriui.resource_uri
visibilityui.visibility (some combination of model and app)

Use visibility: ["app"] for tools callable only by the embedded app, ["model"] for model-only tools, and ["model", "app"] for both.

ui_resource_to_mcp(resource, options?) produces the exact record accepted by harness.tools.mcp_resource. Its meta.ui block carries the resource CSP domains, browser permissions, dedicated domain, and border preference; Harn's MCP server projects it as _meta.ui on both resource discovery and resources/read content. External domains and permissions default empty.

Fallbacks

ui_tool_result(resource, options?: UiToolResultOptions) wraps a resource with a mandatory text fallback (defaulting to a web_artifact_text_fallback text copy 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 capabilityui_select_for_host selection
apps: true and resource validation passedui_resource
Otherwise, structured fallback presentstructured_fallback
Otherwisetext_fallback

ui_host_capabilities(input?: UiHostCapabilityInput) accepts the current MCP extension shape at capabilities.extensions["io.modelcontextprotocol/ui"].mimeTypes, older client_capabilities.apps shapes, 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 records

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 stable guest→host ui/update-model-context request, storing the keyed value in structuredContent for future model turns.

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.v1 schema.

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.

The standalone host advertises the current extension during startup and sends serverTools, serverResources, logging, and enforced sandbox settings to the View. It reads the current _meta.ui.resourceUri tool link and the deprecated flat _meta["ui/resourceUri"] link for compatibility.

Start with the shared renderer in examples/apps/decision-card.harn. Use examples/ui_resource/dashboard-widget.harn and examples/ui_resource/review-form.harn when the app needs its own portable HTML and JavaScript.