Skip to main content

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 on constants.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:
{
  "name": "my_tool_abc12345",
  "description": "...",
  "inputSchema": { "type": "object", "properties": { ... } },
  "annotations": {
    "readOnlyHint": true,
    "openWorldHint": true,
    "destructiveHint": false
  },
  "_meta": {
    "ui": {
      "resourceUri": "ui://widget/{workerId}/{hash}.html",
      "domain": "https://constants.io",
      "prefersBorder": true,
      "csp": {
        "connectDomains": ["https://constants.io"],
        "resourceDomains": ["https://constants.io"]
      }
    },
    "openai/outputTemplate": "ui://widget/{workerId}/{hash}.html"
  }
}
  • annotationsreadOnlyHint, 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 via resources/read.
  • _meta["openai/outputTemplate"] — ChatGPT compatibility alias. Same URI, same semantics.
Interactive workers also emit a hidden {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

ui://widget/{workerId}/{componentHash}-{baseRuntimeFingerprint}.html
  • 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.
Hosts should use the URI as a cache key. Both hash segments rotate naturally — no manual invalidation required.

Tool-call response shape

tools/call for a worker returns the MCP Apps tri-payload shape:
{
  "structuredContent": {
    "summary": "Tool ran successfully.",
    "runId": "...",
    "status": "success",
    "outputPreview": { /* trimmed preview for the model */ }
  },
  "content": [
    { "type": "text", "text": "Tool ran successfully. Run: https://constants.io/run/..." }
  ],
  "_meta": {
    "ui": { "resourceUri": "ui://widget/..." },
    "openai/outputTemplate": "ui://widget/...",
    "widget": {
      "toolName": "my_tool_abc12345",
      "output": { /* full untruncated output */ },
      "outputSchema": { /* per worker.interfaceSchema.outputSchema */ },
      "runId": "...",
      "runUrl": "https://constants.io/run/...",
      "files": [
        {
          "hash": "...",
          "filename": "report.pdf",
          "mimeType": "application/pdf",
          "size": 120034,
          "downloadUrl": "https://...signed..."
        }
      ],
      "source": null
    }
  }
}
Field-level visibility per the MCP Apps spec:
  • 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:
{
  "structuredContent": {
    "summary": "Tool needs credentials before it can run.",
    "runId": "",
    "status": "setup_required",
    "missingCredentials": ["gcpServiceAccount"]
  },
  "content": [
    { "type": "text", "text": "Tool needs credentials before it can run. Configure credentials: https://constants.io/tools/my-tool?setup=credentials" }
  ],
  "_meta": {
    "widget": {
      "output": null,
      "setupRequired": {
        "missingCredentials": ["gcpServiceAccount"],
        "setupUrl": "https://constants.io/tools/my-tool?setup=credentials"
      }
    }
  }
}
The widget renders a CTA linking back to 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:
DirectionMethodPurpose
Widget → Hostui/initializeHandshake. Host replies with capabilities ack.
Host → Widgetui/notifications/tool-inputPre-flight: what arguments came in on the tool call.
Host → Widgetui/notifications/tool-resultDelivers the tri-payload result.
Widget → Hosttools/callRe-run or action dispatch. Host routes back to Constants.
Widget → Hostui/messagePost a text message into the host’s chat.
Widget → Hostui/update-model-contextPush state the model should know.
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:
  1. Authenticate with an API key (wk_...) or OAuth JWT over Authorization: Bearer ....
  2. tools/list to discover widgets — look for _meta.ui.resourceUri.
  3. resources/read with that URI to fetch the HTML.
  4. Render inside a sandboxed iframe with mimeType: "text/html;profile=mcp-app" — this mime type is required for the host bridge to be enabled.
  5. Wire up the ui/* bridge over postMessage.
The App Bridge module from @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.resourceUri on the tool descriptor,
  • ignore _meta.widget on the result,
  • read content[0].text for a one-line summary and the run URL,
  • optionally parse structuredContent if they want machine-readable output.
Nothing breaks — the widget is purely additive.