postMessage.
If you haven’t read Sign-up MCP Apps yet, start
there for the flow and the trust model.
Gravity maintains a complete reference implementation of everything on this
page — the server, the View framework with brand packs, a host-side SDK, and
a one-command scaffold that emits a runnable branded app. Ask your Gravity
contact for access and you can start from a working app instead of a blank
file.
Hand it to your agent
Building with Devin, Claude Code, or Cursor? Paste this prompt — it carries everything the agent needs:The contract: three tools
Your MCP server exposes exactly three tools, all linked to yourui://
resource via _meta.ui.resourceUri:
1
Offer tool (model + app)
Returns the recommended service — name, slug, offer data — as
structuredContent, plus a useful plain-text fallback for hosts without
MCP Apps. When facilitation signing is enabled, the result also carries
the short-lived signed grant binding (search_id, slug).2
Provision tool (model + app)
Takes
user_email, user_consent, grant. Refuses unless
user_consent === true (the user’s click on the Sign up button is the
consent) and the grant verifies — signature, expiry, slug, and search ID.
Calls your normal account-creation API (idempotent per email) and returns
a one-time credentials_url — never the key values.3
Credentials tool (app-only)
Declared with
visibility: ["app"] so the model can never call it.
Redeems the one-time URL; the link burns on first read. Values go only in
structuredContent, rendered inside the View. The tool’s text — and the
model context — carry key names only.Protocol surface
Your server implements the standard MCP Apps surface:initializeadvertisescapabilities.extensions["io.modelcontextprotocol/ui"]withmimeTypes: ["text/html;profile=mcp-app"]- one
ui://resource returns the View with that MIME type and a closed CSP (connectDomains: []— the card makes zero network requests of its own) - flow tools carry
_meta.ui.resourceUri(plus"openai/outputTemplate"for Apps SDK hosts) - every tool result includes a plain-text fallback so non-UI hosts still work
The View: framework + brand pack
Structure the View as two layers so branding never touches the plumbing. The framework layer owns the postMessage bridge (accept messages only fromwindow.parent, lock onto the first origin), the sign-up flow, and the
masked key rows. The brand pack is a plain object you swap per brand:
@font-face rules because the card’s CSP
allows no network — the user sees your real typography, not a system
fallback.
What the user should experience: logo + tagline + email + Sign up →
spinner → “Account created” → your keys appear in place, masked (••••).
Copy — per key or Copy all as .env lines — works while masked;
clicking a value reveals it, clicking again hides it. Ship dark and light
variants of your brand pack; the card follows the host’s theme.
Wiring your backend
A common question: how does the card in the iframe reach your backend to create the account? It doesn’t — the View has zero network access (its CSP declares no connect domains). Every hop is:- The card sends
tools/calloverpostMessageto the host. - The host proxies the call to your MCP server — the one Node process you deployed.
- Your server (server-side, with your secrets) calls your normal
account-creation API and returns a one-time
credentials_url. - The card redeems that URL through the same bridge via the app-only credentials tool; values render only inside the iframe.
How hosts render your card
You don’t build anything host-side. An MCP Apps-capable host fetches yourui:// resource, renders it in a sandboxed iframe (sandbox="allow-scripts"
— opaque origin), and proxies the View’s tools/call requests back to your
MCP server over its own connection. One consequence worth designing for:
the browser must never call your MCP server directly — reject requests that
carry a browser Origin header and let hosts proxy the JSON-RPC
server-side.
Access control
Two layers, both required for a deployed app:- Transport: your MCP server is a protected resource per the MCP
authorization spec. Unauthenticated requests get
401+WWW-Authenticate: Bearer resource_metadata=…; hosts authenticate with OAuth 2.1 (authorization-code + PKCE, audience-bound tokens) and retry. - Facilitation grants: account creation only for Gravity-facilitated flows. Gravity signs a short-lived grant with your account signing secret (generate it in the Index dashboard: Your products → Generate signing secret); your provision tool verifies signature, expiry, slug, and search ID before creating anything. An unfacilitated caller cannot provision.
credentials_url points at your own API, burn one-time links on redeem, and
never log credentials or PII.
Steps to ship
1
Stand up the server
Three tools, one
ui:// resource, plain-text fallbacks — the
contract above. A single Node 18+ process
with no dependencies is enough.2
Wire your backend
Point the two integration points at your account-creation and credential
endpoints — the same partner/provisioning API you likely already have.
Provision must be idempotent per email; credentials one-time and
expiring.
3
Add your brand pack
Logo SVG, CSS vars, tagline, fonts — keep branding in one place,
separate from the bridge and flow code.
4
Keep the security invariants
Consent gating, grant verification, one-time credential redemption, and
the value-free model context — non-negotiable for a listed app.
5
Test locally, then deploy and register
Run the golden path against a stubbed backend: card renders → email +
Sign up → confirmation → masked keys → Copy + reveal. Then deploy one
Node process and register the endpoint in the Index dashboard: open
your product’s configuration, set the Provisioning mode to MCP App,
and enter your MCP server URL.

