> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trygravity.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Build your sign-up app

> Ship a branded sign-up MCP App: three tools, one HTML View, your existing provisioning endpoints

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](/gravity-index/signup-apps) yet, start
there for the flow and the trust model.

<Note>
  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.
</Note>

## Hand it to your agent

Building with Devin, Claude Code, or Cursor? Paste this prompt — it carries
everything the agent needs:

```text theme={null}
Build our company a Gravity sign-up MCP App (an MCP Apps card that lets a
user sign up and collect API keys inside any MCP Apps host).

1. Build a zero-dependency Node 18+ MCP server (JSON-RPC 2.0 over
   Streamable HTTP, POST /mcp) exposing three tools — offer, provision,
   credentials — plus one ui:// resource: a single self-contained HTML View
   served as text/html;profile=mcp-app with a closed CSP (no connect
   domains). Link every tool to the View via _meta.ui.resourceUri.
2. Wire the backend: (a) the provision handler must call our account/key-
   creation API (idempotent per email) and return a one-time
   `credentials_url`, never raw key values; (b) the credentials handler
   redeems that URL from our one-time store (delete-on-read, short TTL,
   same-origin enforced).
3. Brand the View: our real logo as an inline SVG, our exact colors as CSS
   vars with both dark and light variants, tagline, CTA label, and our
   actual fonts embedded as data-URI @font-face rules (the CSP allows no
   network).
4. Enforce the security invariants: consent gating (`user_consent === true`
   from the user's click), Gravity grant verification
   (signature/expiry/slug/search_id, HMAC with our Gravity signing secret),
   one-time link burn, browser-origin POST rejection, and key values never
   in tool text or model context (structuredContent only, app-only
   credentials tool).
5. Test the golden path in a browser against a stubbed backend: card renders
   with our branding (both themes) → email + Sign up → confirmation →
   masked keys → per-key copy and Copy all work while masked →
   click-to-reveal/hide → model context contains key names only.

The Gravity signing secret comes from the Index dashboard (Your products →
Generate signing secret); load it from env, never hard-code it. Full
contract: https://docs.trygravity.ai/gravity-index/build-signup-app
```

## The contract: three tools

Your MCP server exposes exactly three tools, all linked to your `ui://`
resource via `_meta.ui.resourceUri`:

<Steps>
  <Step title="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)`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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:

```js theme={null}
mycompany: {
  logo: '<svg ...>...</svg>',        // your official mark, inline SVG
  vars: {
    "--bg": "#0c0d0d",               // card background
    "--accent": "#00e599",           // CTA button
    "--accent-fg": "#0c0d0d",        // CTA label
    "--font": "'Inter', sans-serif", // your actual typeface
    "--mono": "'Geist Mono', monospace",
  },
  tagline: "Serverless Postgres",
  cta: "Sign up",
  fonts: ["Inter", "Geist Mono"],    // embedded as data URIs at startup
},
```

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

<Steps>
  <Step title="Stand up the server">
    Three tools, one `ui://` resource, plain-text fallbacks — the
    [contract above](#the-contract-three-tools). A single Node 18+ process
    with no dependencies is enough.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Add your brand pack">
    Logo SVG, CSS vars, tagline, fonts — keep branding in one place,
    separate from the bridge and flow code.
  </Step>

  <Step title="Keep the security invariants">
    Consent gating, grant verification, one-time credential redemption, and
    the value-free model context — non-negotiable for a listed app.
  </Step>

  <Step title="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.
  </Step>
</Steps>

Want your service listed and provisioning-enabled first? Start at
[For Service Providers](/gravity-index/for-service-providers).
