HETIBANK SDK Read the docs

Apps guide (MCP Apps / OpenAI Apps SDK)

Ship a HETIBANK result as an interactive view inside an agent host (ChatGPT and other MCP clients).

How it works

An MCP tool returns structuredContent; a ui:// resource (an HTML bundle) renders it. The view talks to the host with JSON-RPC over postMessage using the MCP Apps protocol. ChatGPT additionally exposes legacy window.openai globals — the kit supports both.

tools/call ──► structuredContent ──► ui://widget/…html ──► AppProvider ──► your React view
                     │                                         ▲
                     └── _meta (widget-only data) ─────────────┘

Install

pnpm add @hetibank/apps @hetibank/ui @hetibank/theme @hetibank/contracts

View

import { AppProvider, AppFrame, useToolResult, useDisplayMode, callTool, WidgetFallback } from "@hetibank/apps";
import { ThemeProvider } from "@hetibank/ui";
import { hetibankDark } from "@hetibank/theme";
import { propertyCardSchema, type PropertyCard } from "@hetibank/contracts";

function View() {
  const result = useToolResult();
  const mode = useDisplayMode();
  const parsed = propertyCardSchema.safeParse(result?.structuredContent);
  if (!parsed.success) return <WidgetFallback text="Open HETIBANK to see this home." />;
  return <Card component={parsed.data} displayMode={mode} />;
}

export function Root() {
  return (
    <ThemeProvider theme={hetibankDark} as="section">
      <AppProvider appInfo={{ name: "HETIBANK buyer view", version: "0.1.0" }}>
        <AppFrame><View /></AppFrame>
      </AppProvider>
    </ThemeProvider>
  );
}

Server side

The plugin server registers the tool and the widget resource:

import { createHetibankMcpServer } from "@hetibank/plugins";
import { widgetHtml } from "@hetibank/apps";
import { readFileSync } from "node:fs";

const server = createHetibankMcpServer({
  client: hetibankClient,             // injected, authenticated per request
  widget: {
    uri: "ui://widget/hetibank-buyer-ui-v1.html",
    html: widgetHtml({
      title: "HETIBANK",
      script: readFileSync("dist/widget.js", "utf8"),
      rootId: "hetibank-root",
      data: { siteOrigin: "https://www.hetibank.com" },
    }),
  },
});

widgetHtml produces a CSP-safe document: default-src 'none', no remote scripts, inline styles only for the bundle you pass, a root element and data attributes. Do not hand-write the HTML shell — CSP mistakes are how widgets leak.

Bridge API

ExportPurpose
AppProviderstarts the bridge, handles ui/initialize, provides context
useAppBridge()low-level bridge object
useToolResult(), useToolMeta()result and widget-only metadata, subscription-based
useDisplayMode()inline / fullscreen / pip as negotiated
callTool(name, args)call one of the server's tools from the view
sendMessage(text)add a user message to the conversation
openLink(url)open a URL in the host's browser
reportSize()report height changes (the frame also observes resizes)
parseStructuredContent(schema, value)validate before render
WidgetFallbackhonest text fallback when structured content is absent or invalid

Security

  • The view never receives a server secret; tool calls are authorized by the host's session.
  • _meta is widget-only: never render private documents the model should not see.
  • Navigation carries identifiers, not tokens; the target reauthorizes.
  • No remote script tags or eval; the widget bundle is the only script.
  • Validate structuredContent with the contract schema before rendering — a drifted server should

show a fallback, not a broken card.

Local development

Run the app-widget example: it spawns a mock host that performs the handshake, delivers a property_card, exercises size-changed, tools/call and ui/message:

pnpm --filter @hetibank/example-widget dev

Host compatibility

HostPath
MCP Apps hosts (spec 2026-01-26)postMessage JSON-RPC, ui/initialize handshake
ChatGPT Apps SDKwindow.openai globals (toolOutput, callTool, sendFollowUpMessage)
Plain iframe without hostrenders WidgetFallback text; no errors thrown

Always test the fallback: a widget that only works with a host is a widget that breaks silently.