Skip to main content
A sign-up app is deliberately small: one MCP server (a zero-dependency Node 18+ HTTP server is enough — no framework, no build step) plus one self-contained HTML View. The server exposes three tools and calls your existing account-creation and credential endpoints; the View is your card — logo, colors, tagline, fonts — talking to the host over 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 your ui:// 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:
  • initialize advertises capabilities.extensions["io.modelcontextprotocol/ui"] with mimeTypes: ["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 from window.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:
Fonts are embedded as data-URI @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:
  1. The card sends tools/call over postMessage to the host.
  2. The host proxies the call to your MCP server — the one Node process you deployed.
  3. Your server (server-side, with your secrets) calls your normal account-creation API and returns a one-time credentials_url.
  4. The card redeems that URL through the same bridge via the app-only credentials tool; values render only inside the iframe.
So there are exactly two integration points in your server: the provision handler → your account/key-creation endpoint, and the credentials handler → your one-time credential store (delete-on-read, short TTL). Your API keys live on your server; the browser, the host, and the model never see them.

How hosts render your card

You don’t build anything host-side. An MCP Apps-capable host fetches your ui:// 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:
  1. 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.
  2. 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.
Plus the always-on invariants: reject browser-origin POSTs, enforce that 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.
Want your service listed and provisioning-enabled first? Start at For Service Providers.