Gadgets SDK
read as.md @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 —
createGguiGadgetfor Leaflet, Mapbox, Stripe, Chart.js, Three.js, or anything else with a browser API.
See Glossary for the gadget / tool / blueprint vocabulary, and Marketplace Registry for distribution.
Installation
Section titled “Installation”npm install @ggui-ai/gadgetsAuthoring a wrapper
Section titled “Authoring a wrapper”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). 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
Section titled “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:
{ "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
Section titled “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.gadgetson aDataContractis 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 aGadgetExportUse—{ description?, usage? }, usually just{}(optional intent-specific override prose). There is NOhook/componentfield on the wire — the export-name GRAMMAR is the discriminator:useGeolocationis a hook,LeafletMapis a component. There is NOversion, NOpermission, NO transport metadata, and NObindingkey. The wire carries(package, exportName)and nothing else. GadgetDescriptor— the full registry-side PACKAGE descriptor (what operators register onApp.gadgets). Package-level identity (package+version) plus transport metadata (bundle URLs,connect,typesUrl/typesSri) declared once, plus anexports: GadgetExport[]array (≥1). EachGadgetExportis a field-presence-discriminated union — a hook export{ hook, … }or a component export{ component, … }— carrying that export’s teaching text + per-exportpermission.version, transport metadata,permission, andrequiresALL resolve server-side from this descriptor at render time; the contract author never restates them.createGguiGadgetauthors a single-export package; multi-export packages add furtherexportsentries.
Agent reference shape
Section titled “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:
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
Section titled “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'onscript-srcis needed because the render shell embeds the bootstrap as an inline<script>tag.img-srcderives fromconnect[]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)
Section titled “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
Section titled “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:
- Wrapper declares what it needs on
requires:
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… },});- Operator stamps the value on
ggui.json#app.publicEnv:
{ "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.
-
Render gate enforces at commit time.
assertPublicEnvSatisfiedruns alongside the registry gate; if any declared wrapper’srequiresaren’t satisfied byApp.publicEnv, render rejects withgadget_public_env_missing— the error names the missing key + the wrapper that required it, so the operator fix is unambiguous. -
Wrapper reads via
getPublicEnvfrom@ggui-ai/gadgets:
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.
Reference gadgets
Section titled “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 demoggui.jsonuses 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
Section titled “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 and Self-Hosted Registry.
Roadmap
Section titled “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-renderpublicEnvinput once the per-user shape is locked.