---
title: Gadgets SDK
description: Wrap any 3rd-party browser library (Leaflet, Mapbox, Stripe, …) into an LLM-callable React hook with `createGguiGadget` from `@ggui-ai/gadgets`.
---

`@ggui-ai/gadgets` is the standard library of browser-capability hooks **and** the wrapper SDK that lets you bind any 3rd-party library into the same uniform contract the LLM already knows.

- **STDLIB hooks** — `useGeolocation`, `useCamera`, `useClipboardWrite`, `useClipboardPaste`, `useNotifications`, `useFilePicker`, `useMicrophone`.
- **Wrapper SDK** — `createGguiGadget` for Leaflet, Mapbox, Stripe, Chart.js, Three.js, or anything else with a browser API.

See [Glossary](/glossary/) for the gadget / tool / blueprint vocabulary, and [Marketplace Registry](/sdk/marketplace/) for distribution.

<!-- prettier-ignore-start -->

:::tip[Wrapper-only — by design]
3rd-party libraries enter the system ONLY through `createGguiGadget`. The LLM never sees raw library APIs. Three architectural non-negotiables make this work:

1. **Wrapper-only entry** — every 3rd-party library wraps to a stable React hook contract. The LLM learns ONE pattern.
2. **Operator-controlled registry gating** — only exports the operator has registered on `App.gadgets` may appear in `contract.clientCapabilities.gadgets`. Render validates and rejects with `gadget_not_registered` on any miss.
3. **Teaching text is part of the spec** — `description` (what), `usage` (when), `example` (concrete shape) are required on every wrapper. Optional `gotchas` (anti-patterns / known misuse). Without them, wrappers are technically valid but practically unusable: the LLM either skips them or misuses them.
:::
<!-- prettier-ignore-end -->

## Installation

```bash
npm install @ggui-ai/gadgets
```

## Authoring a wrapper

```ts
import { createGguiGadget } from "@ggui-ai/gadgets";

export const useLeafletMap = createGguiGadget<
  // TOutput — what the hook resolves with on `status: 'completed'`.
  { containerRef: (el: HTMLDivElement | null) => void },
  // TOptions — what the calling component passes in.
  { center: [number, number]; zoom: number }
>({
  // Canonical hook name — the export name a contract references
  // under clientCapabilities.gadgets[<package>][<exportName>]. The
  // `use`-prefixed camelCase name marks this export as a hook.
  hook: "useLeafletMap",

  // REQUIRED teaching text. Synth + decision LLM both see this with
  // a 300-char per-entry budget; description is preserved, usage
  // truncates first when over budget.
  description:
    "Render an interactive Leaflet map with tile layer and pan/zoom controls. Returns a container ref to attach to a <div>.",
  usage:
    "Mount when the intent names a rendered map (location browsing, route preview, points-of-interest grid). Pass `center: [lat, lng]` + `zoom: 2..20`.",

  // REQUIRED example. JsonValue — concrete shape the LLM uses to
  // pattern-match on cold gen.
  example: {
    call: "const map = useLeafletMap({ center: [37.7749, -122.4194], zoom: 12 });",
    returns: {
      status: "completed",
      value: { containerRef: "<DOM ref callback>" },
    },
  },

  // Optional anti-patterns / library quirks. Surfaces in the
  // boilerplate generator's prompt; the LLM uses them to avoid known
  // misuse patterns.
  gotchas:
    "Leaflet requires the container <div> to have a non-zero height before the map mounts — apply `style={{ height: 400 }}` (or similar) directly. Default-marker icons require leaflet.css to be in the document; the styleUrl on this descriptor covers that.",

  // REQUIRED. `version` is part of the blueprint cache key, so a
  // wrapper bump automatically invalidates stale cached generations.
  version: "0.0.1",

  // `package` AND `version` are both REQUIRED — bare npm name, exact
  // semver pin (no URLs, no ranges). `bundleUrl` is optional; when
  // set it wins for boilerplate import emission.
  //
  // Resolution order (operator wins over author wins over spec
  // default): explicit `bundleUrl` → operator `bundleHost` override
  // → author `bundleHost` → spec default `registry.ggui.ai`. With
  // `bundleHost` + `package` + `version` the server assembles
  // `https://<bundleHost>/bundles/<scope>/<name>/<version>/bundle.js`
  // — prefer this over hardcoded `bundleUrl`.
  package: "@my-org/ggui-leaflet",
  bundleHost: "registry.ggui.ai",

  // Optional companion CSS bundle. The wrapper SHOULD self-inject
  // any required CSS in its hook; the styleUrl is purely an origin
  // declaration so the renderer's CSP allows the load. Same
  // `bundleHost`-based resolution applies — explicit `styleUrl`
  // overrides the host-derived `style.css` path.

  // API-call origins (XHR / fetch / WebSocket / `<img src>`). The
  // renderer's Content-Security-Policy unions these into BOTH
  // connect-src AND img-src so map tiles loaded via <img> work.
  connect: ["https://tile.openstreetmap.org"],

  // Public env keys this wrapper consumes (see "Public env channel"
  // below). Both wire + registry zod schemas enforce the same
  // `GGUI_PUBLIC_APP_[A-Z0-9_]+` prefix as App.publicEnv. Leaflet
  // doesn't need a token; Mapbox does.
  // requires: ["GGUI_PUBLIC_APP_MAPBOX_TOKEN"],

  // The React hook. Wrappers bundle their underlying library at
  // build time (esbuild / tsup) and `import` it inside the hook.
  // The function MUST return the standard `{value, status, start, …}`
  // shape that satisfies `GadgetHook<TOutput, TOptions>`.
  hookImpl: (props) => {
    // … wrap Leaflet's lifecycle into ggui's stable hook contract …
    return {
      value: { containerRef: () => undefined },
      status: "completed",
      start: async () => undefined,
    };
  },
});
```

The factory **synchronously zod-validates** the spec at module load. Missing `description` / `usage` / `example`, or a missing `package` / `version`, throws `WrapperConformanceError` with the field path. Authors see the error at import time — not at runtime.

`useLeafletMap` is a callable hook; the immutable wrapper descriptor is attached as `useLeafletMap.descriptor`. Operators register the descriptor on `App.gadgets`.

For distribution, wrappers ship as a `ggui.gadget.json` manifest (see [Marketplace Registry](/sdk/marketplace/)). The registry's publish endpoint computes the bundle's `sha384` and stamps it on the registered entry's `bundleSri`; `ggui gadget install` writes the descriptor (including `bundleSri` + resolved `bundleUrl`) into `ggui.json#app.gadgets[]`. Hand-authored entries omit `bundleSri` — the iframe runtime falls back to integrity-less dynamic `import()`.

Non-stdlib registered descriptors also carry `typesUrl` (HTTPS URL to the wrapper's `.d.ts`) and `typesSri` (SHA-384 SRI over those types), so the generator can type-check against the wrapper's real surface. `typesUrl` is REQUIRED at registration time for any non-stdlib package; `typesSri` is stamped by the registry on publish (optional for hand-authored entries).

## Operator registration

Add the descriptor (or its JSON shape) to `ggui.json#app.gadgets`:

A `GadgetDescriptor` is a PACKAGE — `package` + `version` + transport
metadata declared once, plus an `exports[]` array carrying each
export's teaching text:

```json
{
  "app": {
    "slug": "leaflet-demo",
    "name": "Leaflet gadget demo",
    "gadgets": [
      {
        "package": "@my-org/ggui-leaflet",
        "version": "0.0.1",
        "bundleHost": "registry.ggui.ai",
        "connect": ["https://tile.openstreetmap.org"],
        "exports": [
          {
            "hook": "useLeafletMap",
            "description": "Render an interactive Leaflet map…",
            "usage": "Mount when the intent names a rendered map…",
            "example": {
              "call": "const map = useLeafletMap({ center: [37.7749, -122.4194], zoom: 12 });",
              "returns": {
                "status": "completed",
                "value": { "containerRef": "<DOM ref callback>" }
              }
            },
            "gotchas": "Leaflet requires the container <div>…"
          }
        ]
      }
    ]
  }
}
```

A package that ships more than one export (two hooks, or a hook plus a
React `component`) adds further entries to the same `exports` array —
the transport metadata is declared once and shared.

The CLI threads `app.gadgets` into the in-process `AppMetadataStore` so the same singleton powers `ggui_list_gadgets`, handshake-time prompt injection, render-time validation + enrichment, and CSP derivation.

## Contract gadget refs vs `GadgetDescriptor`

Two distinct shapes — do not confuse them. One is the wire map a host
agent authors on a contract; the other is the registry-side package
descriptor an operator registers. Both are organized around a gadget
**package** that ships one or more **exports**, where each export is
either a hook (a `use`-prefixed camelCase name) or a component (a
PascalCase name):

- **Contract gadget refs** — `clientCapabilities.gadgets` on a `DataContract` is a **package-keyed two-level map** carrying IDENTITY ONLY. The outer key is the npm package name; the inner key is the export name; the inner value is a `GadgetExportUse` — `{ description?, usage? }`, usually just `{}` (optional intent-specific override prose). There is NO `hook` / `component` field on the wire — the export-name GRAMMAR is the discriminator: `useGeolocation` is a hook, `LeafletMap` is a component. There is NO `version`, NO `permission`, NO transport metadata, and NO `binding` key. The wire carries `(package, exportName)` and nothing else.
- **`GadgetDescriptor`** — the full registry-side PACKAGE descriptor (what operators register on `App.gadgets`). Package-level identity (`package` + `version`) plus transport metadata (bundle URLs, `connect`, `typesUrl` / `typesSri`) declared once, plus an `exports: GadgetExport[]` array (≥1). Each `GadgetExport` is a field-presence-discriminated union — a hook export `{ hook, … }` or a component export `{ component, … }` — carrying that export's teaching text + per-export `permission`. `version`, transport metadata, `permission`, and `requires` ALL resolve server-side from this descriptor at render time; the contract author never restates them. `createGguiGadget` authors a single-export package; multi-export packages add further `exports` entries.

## Agent reference shape

Once a package is registered on `App.gadgets`, contracts reference its
exports through the package-keyed `clientCapabilities.gadgets` map —
outer key = package name, inner key = export name:

```ts
const contract = {
  // … propsSpec / actionSpec / contextSpec …
  clientCapabilities: {
    gadgets: {
      "@my-org/ggui-leaflet": {
        // PascalCase key ⇒ a component export. Empty value is the
        // common case; an optional `{ description?, usage? }` override
        // sharpens the teaching text for this specific intent.
        LeafletMap: {},
      },
      "@ggui-ai/gadgets": {
        // `use`-prefixed camelCase key ⇒ a hook export.
        useGeolocation: {},
      },
    },
  },
};
```

The wire is identity-only: render resolves each `(package, exportName)`
pair against the registered `GadgetDescriptor` and enriches the
persisted `ComponentGguiSession` with the canonical `version`, transport metadata,
teaching text, and `permission`. To walk the map in code, call
`listContractGadgets(contract)` — it flattens the package-keyed map
into a `readonly GadgetUse[]`, each entry `{ package, name, description?, usage? }`.

References to unregistered exports reject at render validation with `gadget_not_registered` and a did-you-mean hint when a close stdlib match exists (Levenshtein < 3 cutoff). Render also rejects with `gadget_package_mismatch` when a referenced export name belongs to a different registered package than the one keyed.

## What the renderer derives

When ANY gadget in `clientCapabilities.gadgets` declares `bundleUrl` / `styleUrl` / `connect[]`, the render's spec-canonical `_meta.ui.csp` block carries the derived `{ connectDomains, resourceDomains }` (per the MCP Apps spec). The MCP-Apps host applies them to the sandboxed iframe as a Content-Security-Policy — conceptually:

```
script-src 'self' 'unsafe-inline' <bundleUrl-origins>;
style-src  'self' 'unsafe-inline' <styleUrl-origins>;
connect-src 'self' <connect-origins>;
img-src    'self' data: <connect-origins>
```

- `'unsafe-inline'` on `script-src` is needed because the render shell embeds the bootstrap as an inline `<script>` tag.
- `img-src` derives from `connect[]` so map tiles loaded via `<img>` work without redeclaring origins.

The `_meta.ui.csp` block is populated ONLY when gadgets declare external origins. Pre-gadget scenarios stay CSP-clean.

## STDLIB hooks (no operator config required)

Seven first-party browser-capability hooks are always available — `useGeolocation`, `useCamera`, `useClipboardWrite`, `useClipboardPaste`, `useNotifications`, `useFilePicker`, `useMicrophone`. The stdlib package (`STDLIB_GADGETS` from `@ggui-ai/protocol`) is a structural floor, not a fallback: declaring your own `App.gadgets` layers on top, and stdlib hooks stay referenceable in any contract (a declared package only overrides stdlib on an exact `package` collision).

## Public env channel — wrappers that need a token

Wrappers like Mapbox can't render without an operator-provided token. The public env channel is the typed seam for that: wrappers declare which public keys they consume, operators stamp values on the App, and a single accessor reads them at hook-mount.

**The four moving parts:**

1. **Wrapper declares** what it needs on `requires`:

```ts
export const useMapbox = createGguiGadget({
  hook: "useMapbox",
  // …description / usage / example / package / bundleUrl…
  requires: ["GGUI_PUBLIC_APP_MAPBOX_TOKEN"],
  hookImpl: () => {
    // Lazy read inside the hook body — see the rule below.
    const token = getPublicEnv("GGUI_PUBLIC_APP_MAPBOX_TOKEN");
    mapboxgl.accessToken = token;
    // …mount Mapbox…
  },
});
```

2. **Operator stamps** the value on `ggui.json#app.publicEnv`:

```json
{
  "app": {
    "slug": "mapbox-demo",
    "publicEnv": {
      "GGUI_PUBLIC_APP_MAPBOX_TOKEN": "pk.eyJ..."
    },
    "gadgets": [
      /* …useMapbox descriptor… */
    ]
  }
}
```

Keys MUST match `^GGUI_PUBLIC_APP_[A-Z0-9_]+$` — the prefix is a security boundary, not a convention. **Public means visible to anyone with iframe-source access** (the server inlines projected values verbatim in the bootstrap script tag). Sensitive credentials belong on the agent-side tools surface, NOT here. The `GGUI_PUBLIC_USER_` namespace is reserved for a future per-user channel.

3. **Render gate enforces** at commit time. `assertPublicEnvSatisfied` runs alongside the registry gate; if any declared wrapper's `requires` aren't satisfied by `App.publicEnv`, render rejects with `gadget_public_env_missing` — the error names the missing key + the wrapper that required it, so the operator fix is unambiguous.

4. **Wrapper reads** via `getPublicEnv` from `@ggui-ai/gadgets`:

```ts
import { getPublicEnv } from "@ggui-ai/gadgets";

// Throws with the available-keys diagnostic if the key is missing
// AND opts.optional is not set. Use the throwing default for keys
// you declared on `requires` (render gate guarantees presence) and
// {optional: true} only when the wrapper has a sensible fallback.
const token = getPublicEnv("GGUI_PUBLIC_APP_MAPBOX_TOKEN");
const base = getPublicEnv("GGUI_PUBLIC_APP_API_BASE", { optional: true });
```

**The server projects only what wrappers need.** `derivePublicEnvProjection` computes the union of every declared wrapper's `requires` for the render, then filters `App.publicEnv` to that subset before stamping `_meta["ai.ggui/render"].publicEnv`. Operator-stamped keys that no wrapper consumes stay server-side. Minimum-disclosure by construction.

:::caution[Lazy-call rule]

`getPublicEnv` MUST be called LAZILY inside the hook body (or any code path triggered after first render). **Never at module top level.**

```ts
// ❌ Throws at wrapper-bundle eval — `globalThis.__ggui__` is not yet installed.
const TOKEN = getPublicEnv("GGUI_PUBLIC_APP_MAPBOX_TOKEN");

// ✅ Called per-render after the iframe runtime booted.
hookImpl: () => {
  const token = getPublicEnv("GGUI_PUBLIC_APP_MAPBOX_TOKEN");
  // …
};
```

The iframe runtime loads wrapper bundles BEFORE `installGlobalRegistry`. The hook body, `useEffect`, and the consumer's `start()` callback all run after globals are ready; module-top code does not.

:::

## Reference gadgets

Reference samples ship in the OSS repo under `samples/gadgets/` and `samples/gguis/` — start with the Leaflet pair (no token), then Mapbox (public-env channel end-to-end):

- `@ggui-samples/gadget-leaflet` + `@ggui-samples/ggui-leaflet-demo` — Leaflet wrapper, no token required. The recommended "first wrapper" example.
- `@ggui-samples/gadget-mapbox` + `@ggui-samples/ggui-mapbox-demo` — Mapbox wrapper with the public env channel wired end-to-end. The committed demo `ggui.json` uses a `<set-me-before-running>` placeholder for the token (the render gate accepts it; the Mapbox SDK rejects it on first tile request — replace before running).

## Distribution

Publish wrappers to the marketplace registry at `registry.ggui.ai` for public discovery, or self-host your own registry for private gadgets. See [Marketplace Registry](/sdk/marketplace/) and [Self-Hosted Registry](/sdk/self-hosted-registry/).

## Roadmap

- **Per-user public env (`GGUI_PUBLIC_USER_*`).** Reserved namespace — the wire regex rejects it today. A follow-up wires per-user auth-overlay defaults + explicit per-render `publicEnv` input once the per-user shape is locked.