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 — same HTML bundle that runs onconstants.app.
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
componentHash— first 12 hex chars of sha256(componentHtml). Flips when the author edits the tool.baseRuntimeFingerprint— first 8 hex chars of sha256 over the in-repo widget runtime (INTERACTIVE_BASE_JS+INTERACTIVE_BASE_CSS+INTERACTIVE_BASE_HTML+COMPONENTS_BUNDLE+ the shadcn CSS vars). 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. Host replies with capabilities ack. |
| 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. |
| Widget → Host | tools/call | Re-run or action dispatch. Host routes back to Constants. |
| Widget → Host | ui/message | Post a text message into the host’s chat. |
| Widget → Host | ui/update-model-context | Push state the model should know. |
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.
