Overview
Every Constants tool ships as an MCP App widget. When a host that supports MCP Apps (ChatGPT, Claude Desktop, VS Code Insiders, Goose, Postman, MCPJam) calls a Constants tool, it receives a pointer to a UI resource and renders the widget inline in the chat. Under the hood, each worker’s frontend is split into two independent LLM-generated React components: an input renderer (collects parameters onconstants.io) and an output renderer (displays the result). External MCP Apps hosts serve only the output renderer — the model has already collected arguments via the tool call, so no input UI is needed in the chat. The on-site tool page at constants.io renders both renderers in adjacent iframes.
Hosts that do not implement the MCP Apps extension still work fine. They read the content[].text narration and ignore the widget pointer.
The MCP Apps standard is described at
modelcontextprotocol.io/docs/extensions/apps.
Constants implements the standard fields alongside optional ChatGPT
extensions for backwards compatibility with the Apps SDK.
Tool descriptor shape
tools/list returns each worker with three MCP Apps fields beyond the
baseline spec:
annotations—readOnlyHint,openWorldHint,destructiveHint. Required by ChatGPT for safe tool ranking. Inferred from the tool description during generation; authors can override in the studio._meta.ui.resourceUri— the widget HTML lives at this MCP resource URI. Fetch it viaresources/read._meta["openai/outputTemplate"]— ChatGPT compatibility alias. Same URI, same semantics.
{toolName}__interact companion tool with _meta.ui.visibility: ["app"]. The model never sees it; only the rendered widget can call it via tools/call over the bridge.
Widget URI
outputHash— first 12 hex chars of sha256(output renderer blob). Flips when the author edits the output renderer. Input-side edits do NOT rotate the widget URI — external hosts don’t load the input renderer, so an input change is irrelevant to them.baseRuntimeFingerprint— first 8 hex chars of sha256 over the in-repo widget runtime (COMMON_BASE_JS+OUTPUT_BASE_JS+INTERACTIVE_BASE_CSS+INTERACTIVE_BASE_HTML+COMPONENTS_BUNDLE+ shadcn CSS vars + the assembly template). Flips on every Constants deploy that touches the runtime.
Tool-call response shape
tools/call for a worker returns the MCP Apps tri-payload shape:
structuredContent— model-visible; keep it small.content[].text— model-visible (CLI hosts render this as text)._meta.widget.*— widget-only; never surfaced to the model.
Setup required (missing credentials)
When a widget-bound tool is called but its credentials are unresolved, Constants short-circuits the execution and returns:constants.io where the user can bind the missing credentials. The sandbox never fires — no credits, no logs.
Widget bridge (ui/* JSON-RPC over postMessage)
Each widget speaks the standard MCP Apps bridge. Hosts should implement these methods:
| Direction | Method | Purpose |
|---|---|---|
| Widget → Host | ui/initialize | Handshake step 1. Host replies with capabilities + host context. |
| Widget → Host | ui/notifications/initialized | Handshake step 2 (required). Spec-compliant hosts (Claude) keep the iframe at visibility: hidden and withhold tool-result until they receive this. |
| Host → Widget | ui/notifications/tool-input | Pre-flight: what arguments came in on the tool call. |
| Host → Widget | ui/notifications/tool-result | Delivers the tri-payload result. |
| Host → Widget | ui/notifications/host-context-changed | Notifies the widget when theme, displayMode, container dimensions, or other host context fields change. |
| Widget → Host | tools/call | Re-run or action dispatch. Host routes back to Constants. |
| Widget → Host | ui/notifications/size-changed | Pushed automatically by the SDK’s auto-resize ResizeObserver so the iframe grows to fit content. |
| Widget → Host | ui/request-display-mode | Ask the host to switch the widget to inline / fullscreen / pip. Host returns the actual mode (which may differ). |
| Widget → Host | ui/message | Post a text message into the host’s chat ({ role: "user", content: ContentBlock[] }). |
| Widget → Host | ui/update-model-context | Push state the model should know. |
@modelcontextprotocol/ext-apps’s App class, which is bundled into the widget HTML at build time. The handshake, auto-resize, and bridge requests are all handled by the SDK; widgets only need to set app.ontoolresult and call app.connect().
See the MCP Apps specification for full method semantics.
Embedding the MCP endpoint directly
If you’re building a custom MCP client and want to render Constants widgets yourself:- Authenticate with an API key (
wk_...) or OAuth JWT overAuthorization: Bearer .... tools/listto discover widgets — look for_meta.ui.resourceUri.resources/readwith that URI to fetch the HTML.- Render inside a sandboxed iframe with
mimeType: "text/html;profile=mcp-app"— this mime type is required for the host bridge to be enabled. - Wire up the
ui/*bridge overpostMessage.
@modelcontextprotocol/ext-apps handles rendering and bridge plumbing for you. The mcp-ui React component library is another option.
Compatibility with non-Apps MCP clients
Clients like Codex, Claude Code CLI, and the Cursor remote MCP path don’t implement the MCP Apps extension. They:- ignore
_meta.ui.resourceUrion the tool descriptor, - ignore
_meta.widgeton the result, - read
content[0].textfor a one-line summary and the run URL, - optionally parse
structuredContentif they want machine-readable output.
