Skip to main content
Glama

create_app

Destructive

Create a new interactive web app and render it inline. Use this to build a new app from a request. Existing-contract apps return after publication. Eligible authors can select the ESM manifest schema with an idempotency_key to accept a background build; that result contains a buildId to poll with get_app_build, and has no app URL before publication. If module reuses a manifest.id the caller already owns, the new app replaces the existing app module, UI, and styles in place; use update_app to edit a known app ID by UUID. Apps persist state through a backend env.storage (Workers KV) binding and call backend operations from the frontend via window.charming.api. The module, ui, and description parameter docs carry the authoring rules — including the fill-viewport outer-container rule on ui; the charming:app-guide prompt has the full guide and a canonical example. Read the public docs without a browser using read_docs({ path: "build-mcp.md" }); read_docs({}) lists available pages. If a request needs in-app AI calls, external integrations, native apps, scheduled jobs, SQL, notifications, or view-only sharing, do not fake support. Build the closest local-state version, then log the gap with submit_feedback. The charming:app-guide prompt has alternatives. The charming:design-guide prompt documents Charming's default visual style for generated ui (single accent, warm neutrals, real type hierarchy; no gradients or decorative emoji) — a default that yields to any user-requested aesthetic. When telling the user where to open or share the app, always give them shareUrl from the result — never the url field, which is machine-only and embeds a write-capable access token.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
uiNoUnder the explicit ESM contract, this is an ES module: import declared client packages and import { api, onStateChange } from "charming:ui/app@1.0" to call this app and subscribe to its state changes. Under the existing contract, follow the classic JavaScript rules below. Frontend JavaScript rendered into #app. The #app mount point starts empty, so (a) populate its innerHTML before attaching event listeners, and (b) call operations as `window.charming.api('<manifest-id>').<opName>(input)` (operation name = method name, strips the { ok, value } envelope — you receive the value only, never ok) — not `api.operation(name, params)` and not raw fetch(), which fails in the null-origin srcdoc iframe. The signed-in caller is exposed synchronously as `window.charming.user` (`{ id, handle?, name?, image? }`, or `null` for an anonymous visitor) — read `user.name` to greet, attribute, or personalize, instead of asking the user to type their name. PUBLIC fields only (never email); it is a convenience signal, not enforcement. The outermost container MUST fill the viewport — use `<main class="min-h-screen">` (or a grid/flex layout that spans width) as the root shell. Do NOT wrap the root in `max-w-md`, `max-w-2xl`, or `container mx-auto`: those cap the entire app to a narrow central column with wide empty margins on 2K+ monitors — the loudest "AI-generated app" tell. If the view is text-heavy (a note, an article, a form with long prose), cap the reading measure on an INNER wrapper only, e.g. `<main class="min-h-screen"><div class="mx-auto max-w-2xl">…</div></main>`. Dashboards, kanban, tables, canvases, galleries, and split views should use the full width. For live updates when an agent mutates state from another session, register `window.charming.onStateChange((e) => { ... })` and update the DOM surgically rather than wiping #app. See the charming:app-guide prompt for a canonical example.
moduleYesEligible ESM authors may instead select `$schema: "https://charm.ing/schema/app-manifest/2026-09-05.json"`, declare target-specific `dependencies.server` and `dependencies.client`, and use ordinary package imports plus exact versioned platform imports. For ESM persistence, use `import { kv } from 'charming:storage/kv@1.0'` and call `kv.get`/`kv.put` inside route handlers; omit `capabilities`. ESM creation takes its name from `manifest.meta.name`; omit `display_name`. For the existing contract, export a strict canonical `manifest` with `$schema: "https://charm.ing/schema/app-manifest/2026-07-31.json"`, `id`, and `meta: { name, icon? }`. The server may insert the exact schema URL on create when the rest of the source is canonical. Unknown manifest keys are rejected. To persist state in the existing contract, `capabilities.imports` must include "charming:storage/kv@1.0" (without it `env.storage` is undefined and every read/write throws `storage capability not granted`). A route handler in `export const routes = [...]` receives `(input, { env, ctx, request })` and returns exactly the value declared by `outputSchema`; for an array schema, use `handler: async (_input, { env }) => (await env.storage.get("key")) ?? []`. Charming creates the transport envelope. Do not add a `{ ok, value }` or `{ value }` envelope unless those fields belong to `outputSchema` itself. A named context reads storage as `context.env.storage`. The optional unmatched-request fallback has the separate signature `export default { fetch(request, env, ctx) { ... } }`, where the second argument is the environment itself; it adds no discoverable route metadata. `env.user` is always present, not gated by any import: the caller's public identity (`{ id, handle?, name?, image? }`) or `null`. It lives only on `env` — read `env.user` (or `context.env.user`); there is no `ctx.user`. Full reference: call read_docs with path `llms-full.txt` (https://charm.ing/docs/llms-full.txt). For the existing contract, persistence goes through `env.storage` using Workers KV semantics — `get(key)`, `put(key, value)`, `delete(key)`, `list()`; `.set`/`.add`/`.write`/`.setItem`/`.removeItem` do not exist and throw `TypeError: env.storage.<x> is not a function`. env.storage stores JSON-compatible values directly; do not JSON.stringify before put or JSON.parse after get. Use env.storage for ALL persistence — it is the only storage that survives inside Claude/ChatGPT and syncs across devices. Do NOT keep app data or user state in localStorage/sessionStorage/IndexedDB: those APIs are empty inside chat hosts, so their data silently vanishes there (the most common cause of an app that appears not to save). The "charming:browser/storage@1.0" capability (claim-gated) unlocks them but only for throwaway, web-only caching; anything the user expects to keep belongs in env.storage. Export a `routes` array with unique `op` values and `handler` functions. Canonical route fields are `inputSchema`, `outputSchema`, and `annotations`; `method` defaults to `POST`, `path` defaults to `/api/<op>`, input defaults to a closed empty-object schema, and `public` defaults to true. Set all four MCP annotation hints when their defaults do not fit; Charming does not infer them from the HTTP method. A default `fetch` handler is an unmatched-request fallback only and is not discoverable. The following capability declarations apply only to the existing contract. Apps that use a sensitive browser capability must declare its import in `capabilities.imports` — "charming:browser/microphone@1.0" (getUserMedia audio), "charming:browser/camera@1.0" (getUserMedia video), "charming:browser/geolocation@1.0", "charming:browser/clipboard-read@1.0" (reading the clipboard), "charming:browser/display-capture@1.0" (getDisplayMedia screen share), "charming:browser/midi@1.0" (Web MIDI, navigator.requestMIDIAccess), "charming:browser/device-motion@1.0" (device orientation/motion: DeviceOrientationEvent/DeviceMotionEvent + iOS requestPermission), "charming:browser/ambient-light@1.0" (ambient light, new AmbientLightSensor), or "charming:browser/storage@1.0" (native client storage — localStorage/sessionStorage/IndexedDB — web-only); access is granted only after the app is claimed/authenticated. To call external HTTPS APIs from backend code, declare "charming:network/fetch@1.0" and list each exact origin in `manifest.permissions.server.fetch`; both are required and public-only. For an endpoint that needs an API key, declare "charming:secrets/fetch@1.0" to get `env.fetch` (claimed apps only) — a sealed outbound fetch that substitutes `{{secret:NAME}}` references in request HEADER values or query-parameter VALUES host-side (never a parameter name, the host, path, fragment, or body), so the key never enters app source or the sandbox; write the placeholder literally in the URL string — `URLSearchParams.set(...)` or `encodeURIComponent(...)` percent-encodes it first and it will NOT resolve; the app OWNER opens App settings, then Secrets, at `/<owner-handle>/~/apps/<app-name>/settings/secrets`, while `/app/<id>/secrets` remains the machine HTTP API. The agent only references the NAME. Never embed API keys in source. To render remote images, list each exact https origin in `manifest.permissions.browser["img-src"]`. To make an image render in ANY host — standalone, ChatGPT, AND Claude inline (their injected CSP blocks a cross-origin `<img src>`) — set the src from `const src = await window.charming.images.load(remoteUrl)` (it fetches through Charming and returns a `data:` URL every embed CSP allows). `window.charming.images.proxy(remoteUrl)` returns a same-origin proxy URL that works standalone/ChatGPT but NOT in Claude inline; prefer `images.load(...)` when the app may be embedded. Both enforce the declared origins; neither bypasses them.
stylesNoOptional CSS for the rendered app
team_idNoOptional destination team id. Only a team owner or admin can create an App there. Omit it to create a personal App.
descriptionYesSearch-friendly summary of what this app does, surfaced by list_apps so a future session can match user intent (e.g. "open my protein tracker", "log food") to this app even when display_name is ambiguous. Write 1-2 sentences covering: (a) what the app tracks/does, (b) the key actions it supports, and (c) synonyms or alternative phrasings the user might say. Example: "Tracks daily protein intake. Log meals, view weekly totals, set a daily goal. Synonyms: meals, food log, nutrition tracker, calorie counter." Limit 500 characters.
display_nameNoOptional display name override
idempotency_keyNoRequired for ESM builds: 8–128 visible ASCII characters. Retry the exact request with the same key to recover its build; use a new key for changed source.
migrate_contractNoSet true to migrate an existing legacy manifest.id to ESM, together with expected_revision. Source submissions do not roll back contracts. History can explicitly restore a retained validated existing-contract revision.
expected_revisionNoRequired when ESM source reuses an existing manifest.id. Pass the desired revision from get_app_source.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
idNoUUID of the created or updated app.
okYes
urlNoApp URL. Present only after successful publication.
iconNoThe effective home-screen icon stored for the app. Canonical source declares it at `manifest.meta.icon`.
appIdNoExisting target app ID, or the created app ID after publication.
errorNo
stateNo
intentNoThe accepted operation: create, update, migrate, restore, or copy.
sourceNoExact accepted source, returned only when include_source is true.
appNameNoURL-safe slug for the app, unique per owner. Distinct from manifestId and displayName, and stable across edits — use rename_app to change it (the title changing does NOT change the slug).
buildIdNoDurable build ID. Use get_app_build to inspect progress or source.
attemptsNo
deadlineNo
revisionNoServer-owned app source revision. Historical null counters read as 0; new apps start at 1; each successful source write advances it once. Pass this value through `expected_revision` when guarding update_app.
shareUrlNoThe link to give humans whenever the user wants to open or share the app. Friendly /<handle>/<app-name> form when the OWNER has a live handle and the app a slug, /app/<uuid> otherwise. Always token-free — safe to show, paste, and send. Not an API base: appending /api/<op> to the friendly form 404s; use `url` for machine calls.
warningsNoNon-blocking publish feedback (#1126): present when static validation found UI/backend contract mismatches or legacy icon input needed a fallback. The write succeeded; fix the named source field.
elapsedMsNo
lockStateNo
statusUrlNoAuthenticated HTTP status URL for this build.
updatedAtNo
acceptedAtNo
advisoriesNoStructured non-fatal advisories, including authoring issues and unread staff feedback responses. Each advisory.summary is also appended to the text content for the LLM path.
finishedAtNo
lockDigestNo
sourceEtagNoETag identifying the immutable input accepted for this build.
inputDigestNo
queueDeadlineNo
activeRevisionNo
desiredRevisionNo
retryAfterSecondsNoWait at least this many seconds before polling again.
inspectionExpiresAtNo
idempotencyExpiresAtNo
resolvedDependenciesNo

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed41 schema fields changed
    • addedInput schema / properties / expected_revision
      Added value: +{
      +  "description": "Required when ESM source reuses an existing manifest.id. Pass the desired revision from get_app_source.",
      +  "maximum": 9007199254740991,
      +  "minimum": -9007199254740991,
      +  "type": "integer"
      +}
    • addedInput schema / properties / idempotency_key
      Added value: +{
      +  "description": "Required for ESM builds: 8–128 visible ASCII characters. Retry the exact request with the same key to recover its build; use a new key for changed source.",
      +  "type": "string"
      +}
    • addedInput schema / properties / migrate_contract
      Added value: +{
      +  "description": "Set true to migrate an existing legacy manifest.id to ESM, together with expected_revision. Source submissions do not roll back contracts. History can explicitly restore a retained validated existing-contract revision.",
      +  "type": "boolean"
      +}
    • changedInput schema / properties / module / description
      Previous value: -"ES module source for the app backend. Export a `manifest`; to persist any state its `capabilities.imports` must include \"buildy:storage/kv@1.0\" (without it `env.storage` is undefined and every read/write throws `storage capability not granted`). All persistence goes through `env.storage` using Workers KV semantics — `get(key)`, `put(key, value)`, `delete(key)`, `list()`; `.set`/`.add`/`.write`/`.setItem`/`.removeItem` do not exist and throw `TypeError: env.storage.<x> is not a function`. env.storage stores JSON-compatible values directly; do not JSON.stringify before put or JSON.parse after get. Use env.storage for ALL persistence — it is the only storage that survives inside Claude/ChatGPT and syncs across devices. Do NOT keep app data or user state in localStorage/sessionStorage/IndexedDB: those APIs are empty inside chat hosts, so their data silently vanishes there (the most common cause of an app that appears not to save). The \"buildy:browser/storage@1.0\" capability (claim-gated) unlocks them but only for throwaway, web-only caching; anything the user expects to keep belongs in env.storage. Export `routes` (recommended) or a default `fetch` handler serving `/api/<opName>`, each returning `{ ok: true, value }` or `{ ok: false, error: { kind, message } }`. Apps that use a sensitive browser capability must declare its import in `capabilities.imports` — \"buildy:browser/microphone@1.0\" (getUserMedia audio), \"buildy:browser/camera@1.0\" (getUserMedia video), \"buildy:browser/geolocation@1.0\", \"buildy:browser/clipboard-read@1.0\" (reading the clipboard), \"buildy:browser/display-capture@1.0\" (getDisplayMedia screen share), \"buildy:browser/midi@1.0\" (Web MIDI, navigator.requestMIDIAccess), \"buildy:browser/device-motion@1.0\" (device orientation/motion: DeviceOrientationEvent/DeviceMotionEvent + iOS requestPermission), \"buildy:browser/ambient-light@1.0\" (ambient light, new AmbientLightSensor), or \"buildy:browser/storage@1.0\" (native client storage — localStorage/sessionStorage/IndexedDB — web-only); access is granted only after the app is claimed/authenticated. To call external HTTPS APIs from backend code, declare \"buildy:network/fetch@1.0\" — backend `fetch` is blocked by default and this enables it (public-only: private/loopback/cloud-metadata addresses stay blocked; claim-gated). Restrict it to the hosts the app needs with `manifest.capabilities.fetchHosts` (concrete https hosts, like imageHosts) — default-deny, any non-listed host is blocked; omit to allow any public host. For an endpoint that needs an API key, declare \"buildy:secrets/fetch@1.0\" to get `env.fetch` (claimed apps only) — a sealed outbound fetch that substitutes `{{secret:NAME}}` references in request HEADER values host-side, so the key never enters app source or the sandbox; the app OWNER sets the value in the dashboard at `/app/<id>/secrets` and the agent only references the NAME. Never embed API keys in source. To render remote images, list each https host in `manifest.capabilities.imageHosts`. To make an image render in ANY host — standalone, ChatGPT, AND Claude inline (their injected CSP blocks a cross-origin `<img src>`) — set the src from `const src = await window.buildy.images.load(remoteUrl)` (it fetches through Charming and returns a `data:` URL every embed CSP allows). `window.buildy.images.proxy(remoteUrl)` returns a same-origin proxy URL that works standalone/ChatGPT but NOT in Claude inline; prefer `images.load(...)` when the app may be embedded. Both enforce the imageHosts allowlist; neither bypasses it."New value: +"Eligible ESM authors may instead select `$schema: \"https://charm.ing/schema/app-manifest/2026-09-05.json\"`, declare target-specific `dependencies.server` and `dependencies.client`, and use ordinary package imports plus exact versioned platform imports. For ESM persistence, use `import { kv } from 'charming:storage/kv@1.0'` and call `kv.get`/`kv.put` inside route handlers; omit `capabilities`. ESM creation takes its name from `manifest.meta.name`; omit `display_name`. For the existing contract, export a strict canonical `manifest` with `$schema: \"https://charm.ing/schema/app-manifest/2026-07-31.json\"`, `id`, and `meta: { name, icon? }`. The server may insert the exact schema URL on create when the rest of the source is canonical. Unknown manifest keys are rejected. To persist state in the existing contract, `capabilities.imports` must include \"charming:storage/kv@1.0\" (without it `env.storage` is undefined and every read/write throws `storage capability not granted`). A route handler in `export const routes = [...]` receives `(input, { env, ctx, request })` and returns exactly the value declared by `outputSchema`; for an array schema, use `handler: async (_input, { env }) => (await env.storage.get(\"key\")) ?? []`. Charming creates the transport envelope. Do not add a `{ ok, value }` or `{ value }` envelope unless those fields belong to `outputSchema` itself. A named context reads storage as `context.env.storage`. The optional unmatched-request fallback has the separate signature `export default { fetch(request, env, ctx) { ... } }`, where the second argument is the environment itself; it adds no discoverable route metadata. `env.user` is always present, not gated by any import: the caller's public identity (`{ id, handle?, name?, image? }`) or `null`. It lives only on `env` — read `env.user` (or `context.env.user`); there is no `ctx.user`. Full reference: call read_docs with path `llms-full.txt` (https://charm.ing/docs/llms-full.txt). For the existing contract, persistence goes through `env.storage` using Workers KV semantics — `get(key)`, `put(key, value)`, `delete(key)`, `list()`; `.set`/`.add`/`.write`/`.setItem`/`.removeItem` do not exist and throw `TypeError: env.storage.<x> is not a function`. env.storage stores JSON-compatible values directly; do not JSON.stringify before put or JSON.parse after get. Use env.storage for ALL persistence — it is the only storage that survives inside Claude/ChatGPT and syncs across devices. Do NOT keep app data or user state in localStorage/sessionStorage/IndexedDB: those APIs are empty inside chat hosts, so their data silently vanishes there (the most common cause of an app that appears not to save). The \"charming:browser/storage@1.0\" capability (claim-gated) unlocks them but only for throwaway, web-only caching; anything the user expects to keep belongs in env.storage. Export a `routes` array with unique `op` values and `handler` functions. Canonical route fields are `inputSchema`, `outputSchema`, and `annotations`; `method` defaults to `POST`, `path` defaults to `/api/<op>`, input defaults to a closed empty-object schema, and `public` defaults to true. Set all four MCP annotation hints when their defaults do not fit; Charming does not infer them from the HTTP method. A default `fetch` handler is an unmatched-request fallback only and is not discoverable. The following capability declarations apply only to the existing contract. Apps that use a sensitive browser capability must declare its import in `capabilities.imports` — \"charming:browser/microphone@1.0\" (getUserMedia audio), \"charming:browser/camera@1.0\" (getUserMedia video), \"charming:browser/geolocation@1.0\", \"charming:browser/clipboard-read@1.0\" (reading the clipboard), \"charming:browser/display-capture@1.0\" (getDisplayMedia screen share), \"charming:browser/midi@1.0\" (Web MIDI, navigator.requestMIDIAccess), \"charming:browser/device-motion@1.0\" (device orientation/motion: DeviceOrientationEvent/DeviceMotionEvent + iOS requestPermission), \"charming:browser/ambient-light@1.0\" (ambient light, new AmbientLightSensor), or \"charming:browser/storage@1.0\" (native client storage — localStorage/sessionStorage/IndexedDB — web-only); access is granted only after the app is claimed/authenticated. To call external HTTPS APIs from backend code, declare \"charming:network/fetch@1.0\" and list each exact origin in `manifest.permissions.server.fetch`; both are required and public-only. For an endpoint that needs an API key, declare \"charming:secrets/fetch@1.0\" to get `env.fetch` (claimed apps only) — a sealed outbound fetch that substitutes `{{secret:NAME}}` references in request HEADER values or query-parameter VALUES host-side (never a parameter name, the host, path, fragment, or body), so the key never enters app source or the sandbox; write the placeholder literally in the URL string — `URLSearchParams.set(...)` or `encodeURIComponent(...)` percent-encodes it first and it will NOT resolve; the app OWNER opens App settings, then Secrets, at `/<owner-handle>/~/apps/<app-name>/settings/secrets`, while `/app/<id>/secrets` remains the machine HTTP API. The agent only references the NAME. Never embed API keys in source. To render remote images, list each exact https origin in `manifest.permissions.browser[\"img-src\"]`. To make an image render in ANY host — standalone, ChatGPT, AND Claude inline (their injected CSP blocks a cross-origin `<img src>`) — set the src from `const src = await window.charming.images.load(remoteUrl)` (it fetches through Charming and returns a `data:` URL every embed CSP allows). `window.charming.images.proxy(remoteUrl)` returns a same-origin proxy URL that works standalone/ChatGPT but NOT in Claude inline; prefer `images.load(...)` when the app may be embedded. Both enforce the declared origins; neither bypasses them."
    • addedInput schema / properties / team_id
      Added value: +{
      +  "description": "Optional destination team id. Only a team owner or admin can create an App there. Omit it to create a personal App.",
      +  "type": "string"
      +}
    • changedInput schema / properties / ui / description
      Previous value: -"Frontend JavaScript rendered into #app. The #app mount point starts empty, so (a) populate its innerHTML before attaching event listeners, and (b) call operations as `window.buildy.api('<manifest-id>').<opName>(input)` (operation name = method name, strips the { ok, value } envelope — you receive the value only, never ok) — not `api.operation(name, params)` and not raw fetch(), which fails in the null-origin srcdoc iframe. The signed-in caller is exposed synchronously as `window.buildy.user` (`{ id, handle?, name?, image? }`, or `null` for an anonymous visitor) — read `user.name` to greet, attribute, or personalize, instead of asking the user to type their name. PUBLIC fields only (never email); it is a convenience signal, not enforcement. The outermost container MUST fill the viewport — use `<main class=\"min-h-screen\">` (or a grid/flex layout that spans width) as the root shell. Do NOT wrap the root in `max-w-md`, `max-w-2xl`, or `container mx-auto`: those cap the entire app to a narrow central column with wide empty margins on 2K+ monitors — the loudest \"AI-generated app\" tell. If the view is text-heavy (a note, an article, a form with long prose), cap the reading measure on an INNER wrapper only, e.g. `<main class=\"min-h-screen\"><div class=\"mx-auto max-w-2xl\">…</div></main>`. Dashboards, kanban, tables, canvases, galleries, and split views should use the full width. For live updates when an agent mutates state from another session, register `window.buildy.onStateChange((e) => { ... })` and update the DOM surgically rather than wiping #app. See the charming:app-guide prompt for a canonical example."New value: +"Under the explicit ESM contract, this is an ES module: import declared client packages and import { api, onStateChange } from \"charming:ui/app@1.0\" to call this app and subscribe to its state changes. Under the existing contract, follow the classic JavaScript rules below. Frontend JavaScript rendered into #app. The #app mount point starts empty, so (a) populate its innerHTML before attaching event listeners, and (b) call operations as `window.charming.api('<manifest-id>').<opName>(input)` (operation name = method name, strips the { ok, value } envelope — you receive the value only, never ok) — not `api.operation(name, params)` and not raw fetch(), which fails in the null-origin srcdoc iframe. The signed-in caller is exposed synchronously as `window.charming.user` (`{ id, handle?, name?, image? }`, or `null` for an anonymous visitor) — read `user.name` to greet, attribute, or personalize, instead of asking the user to type their name. PUBLIC fields only (never email); it is a convenience signal, not enforcement. The outermost container MUST fill the viewport — use `<main class=\"min-h-screen\">` (or a grid/flex layout that spans width) as the root shell. Do NOT wrap the root in `max-w-md`, `max-w-2xl`, or `container mx-auto`: those cap the entire app to a narrow central column with wide empty margins on 2K+ monitors — the loudest \"AI-generated app\" tell. If the view is text-heavy (a note, an article, a form with long prose), cap the reading measure on an INNER wrapper only, e.g. `<main class=\"min-h-screen\"><div class=\"mx-auto max-w-2xl\">…</div></main>`. Dashboards, kanban, tables, canvases, galleries, and split views should use the full width. For live updates when an agent mutates state from another session, register `window.charming.onStateChange((e) => { ... })` and update the DOM surgically rather than wiping #app. See the charming:app-guide prompt for a canonical example."
    • addedOutput schema / anyOf
      Added value: +[
      +  {
      +    "$schema": "https://json-schema.org/draft/2020-12/schema",
      +    "additionalProperties": false,
      +    "properties": {
      +      "advisories": {
      +        "description": "Structured non-fatal advisories, including authoring issues and unread staff feedback responses. Each advisory.summary is also appended to the text content for the LLM path.",
      +        "items": {
      +          "additionalProperties": false,
      +          "properties": {
      +            "data": {
      +              "additionalProperties": {},
      +              "description": "Kind-specific structured payload. Shape varies per advisory kind.",
      +              "propertyNames": {
      +                "type": "string"
      +              },
      +              "type": "object"
      +            },
      +            "doc_url": {
      +              "description": "Optional docs pointer for this advisory kind.",
      +              "type": "string"
      +            },
      +            "kind": {
      +              "description": "Stable advisory identifier (e.g. \"legacy-bridge\").",
      +              "type": "string"
      +            },
      +            "severity": {
      +              "description": "Severity; omitted advisories are treated as 'info'.",
      +              "enum": [
      +                "info",
      +                "warn"
      +              ],
      +              "type": "string"
      +            },
      +            "summary": {
      +              "description": "Agent-facing summary. Self-sufficient; no extra context required.",
      +              "type": "string"
      +            },
      +            "userSummary": {
      +              "description": "End-user-facing summary. Set when the advisory should render in-app.",
      +              "type": "string"
      +            }
      +          },
      +          "required": [
      +            "kind",
      +            "summary"
      +          ],
      +          "type": "object"
      +        },
      +        "type": "array"
      +      },
      +      "appName": {
      +        "description": "URL-safe slug for the app, unique per owner. Distinct from manifestId and displayName, and stable across edits — use rename_app to change it (the title changing does NOT change the slug).",
      +        "type": [
      +          "string",
      +          "null"
      +        ]
      +      },
      +      "icon": {
      +        "additionalProperties": false,
      +        "description": "The effective home-screen icon stored for the app. Canonical source declares it at `manifest.meta.icon`.",
      +        "properties": {
      +          "bg": {
      +            "description": "The icon background as a hex color (e.g. \"#1d8a4e\").",
      +            "type": "string"
      +          },
      +          "emoji": {
      +            "description": "The single emoji rendered on the icon.",
      +            "type": "string"
      +          }
      +        },
      +        "required": [
      +          "emoji",
      +          "bg"
      +        ],
      +        "type": "object"
      +      },
      +      "id": {
      +        "description": "UUID of the created or updated app.",
      +        "type": "string"
      +      },
      +      "ok": {
      +        "const": true,
      +        "description": "Indicates success. Errors arrive as content with isError:true.",
      +        "type": "boolean"
      +      },
      +      "revision": {
      +        "description": "Server-owned app source revision. Historical null counters read as 0; new apps start at 1; each successful source write advances it once. Pass this value through `expected_revision` when guarding update_app.",
      +        "maximum": 9007199254740991,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "shareUrl": {
      +        "description": "The link to give humans whenever the user wants to open or share the app. Friendly /<handle>/<app-name> form when the OWNER has a live handle and the app a slug, /app/<uuid> otherwise. Always token-free — safe to show, paste, and send. Not an API base: appending /api/<op> to the friendly form 404s; use `url` for machine calls.",
      +        "type": "string"
      +      },
      +      "url": {
      +        "description": "Machine/API URL for the app (stable /app/<uuid> form). Embeds a write-capable ?t= access token — NEVER show, paste, or send it to the user. Append /api/<op> to its path for out-of-band operation calls. For anything user-facing, use shareUrl instead.",
      +        "type": "string"
      +      },
      +      "warnings": {
      +        "description": "Non-blocking publish feedback (#1126): present when static validation found UI/backend contract mismatches or legacy icon input needed a fallback. The write succeeded; fix the named source field.",
      +        "items": {
      +          "type": "string"
      +        },
      +        "type": "array"
      +      }
      +    },
      +    "required": [
      +      "ok",
      +      "id",
      +      "url",
      +      "shareUrl",
      +      "revision",
      +      "icon"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "$schema": "https://json-schema.org/draft/2020-12/schema",
      +    "additionalProperties": false,
      +    "properties": {
      +      "acceptedAt": {
      +        "type": "string"
      +      },
      +      "activeRevision": {
      +        "maximum": 9007199254740991,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "appId": {
      +        "description": "Existing target app ID, or the created app ID after publication.",
      +        "type": "string"
      +      },
      +      "attempts": {
      +        "maximum": 9007199254740991,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "buildId": {
      +        "description": "Durable build ID. Use get_app_build to inspect progress or source.",
      +        "type": "string"
      +      },
      +      "deadline": {
      +        "type": "string"
      +      },
      +      "desiredRevision": {
      +        "maximum": 9007199254740991,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "elapsedMs": {
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "error": {
      +        "additionalProperties": false,
      +        "properties": {
      +          "column": {
      +            "maximum": 9007199254740991,
      +            "minimum": -9007199254740991,
      +            "type": "integer"
      +          },
      +          "kind": {
      +            "type": "string"
      +          },
      +          "line": {
      +            "maximum": 9007199254740991,
      +            "minimum": -9007199254740991,
      +            "type": "integer"
      +          },
      +          "message": {
      +            "type": "string"
      +          },
      +          "retryable": {
      +            "type": "boolean"
      +          },
      +          "specifier": {
      +            "type": "string"
      +          },
      +          "target": {
      +            "type": "string"
      +          }
      +        },
      +        "required": [
      +          "kind",
      +          "message",
      +          "retryable"
      +        ],
      +        "type": "object"
      +      },
      +      "finishedAt": {
      +        "type": [
      +          "string",
      +          "null"
      +        ]
      +      },
      +      "idempotencyExpiresAt": {
      +        "type": [
      +          "string",
      +          "null"
      +        ]
      +      },
      +      "inputDigest": {
      +        "type": "string"
      +      },
      +      "inspectionExpiresAt": {
      +        "type": [
      +          "string",
      +          "null"
      +        ]
      +      },
      +      "intent": {
      +        "description": "The accepted operation: create, update, migrate, restore, or copy.",
      +        "type": "string"
      +      },
      +      "lockDigest": {
      +        "type": "string"
      +      },
      +      "lockState": {
      +        "enum": [
      +          "locked",
      +          "unresolved"
      +        ],
      +        "type": "string"
      +      },
      +      "ok": {
      +        "const": true,
      +        "type": "boolean"
      +      },
      +      "queueDeadline": {
      +        "type": "string"
      +      },
      +      "resolvedDependencies": {
      +        "additionalProperties": false,
      +        "properties": {
      +          "client": {
      +            "additionalProperties": {
      +              "type": "string"
      +            },
      +            "propertyNames": {
      +              "type": "string"
      +            },
      +            "type": "object"
      +          },
      +          "server": {
      +            "additionalProperties": {
      +              "type": "string"
      +            },
      +            "propertyNames": {
      +              "type": "string"
      +            },
      +            "type": "object"
      +          }
      +        },
      +        "required": [
      +          "server",
      +          "client"
      +        ],
      +        "type": "object"
      +      },
      +      "retryAfterSeconds": {
      +        "description": "Wait at least this many seconds before polling again.",
      +        "exclusiveMinimum": 0,
      +        "maximum": 9007199254740991,
      +        "type": "integer"
      +      },
      +      "revision": {
      +        "description": "Published app source revision.",
      +        "exclusiveMinimum": 0,
      +        "maximum": 9007199254740991,
      +        "type": "integer"
      +      },
      +      "source": {
      +        "additionalProperties": false,
      +        "description": "Exact accepted source, returned only when include_source is true.",
      +        "properties": {
      +          "description": {
      +            "type": [
      +              "string",
      +              "null"
      +            ]
      +          },
      +          "module": {
      +            "type": "string"
      +          },
      +          "styles": {
      +            "type": [
      +              "string",
      +              "null"
      +            ]
      +          },
      +          "ui": {
      +            "type": [
      +              "string",
      +              "null"
      +            ]
      +          }
      +        },
      +        "required": [
      +          "module",
      +          "ui",
      +          "styles",
      +          "description"
      +        ],
      +        "type": "object"
      +      },
      +      "sourceEtag": {
      +        "description": "ETag identifying the immutable input accepted for this build.",
      +        "type": "string"
      +      },
      +      "state": {
      +        "enum": [
      +          "queued",
      +          "resolving",
      +          "building",
      +          "validating",
      +          "published",
      +          "failed",
      +          "superseded",
      +          "canceled",
      +          "expired"
      +        ],
      +        "type": "string"
      +      },
      +      "statusUrl": {
      +        "description": "Authenticated HTTP status URL for this build.",
      +        "type": "string"
      +      },
      +      "updatedAt": {
      +        "type": "string"
      +      },
      +      "url": {
      +        "description": "App URL. Present only after successful publication.",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "ok",
      +      "buildId",
      +      "intent",
      +      "state",
      +      "sourceEtag",
      +      "statusUrl",
      +      "attempts",
      +      "acceptedAt",
      +      "updatedAt",
      +      "finishedAt",
      +      "queueDeadline",
      +      "deadline",
      +      "inspectionExpiresAt",
      +      "idempotencyExpiresAt",
      +      "elapsedMs",
      +      "lockState",
      +      "inputDigest"
      +    ],
      +    "type": "object"
      +  }
      +]
    • addedOutput schema / properties / acceptedAt
      Added value: +{
      +  "type": "string"
      +}
    • addedOutput schema / properties / activeRevision
      Added value: +{
      +  "maximum": 9007199254740991,
      +  "minimum": 0,
      +  "type": "integer"
      +}
    • changedOutput schema / properties / advisories / description
      Previous value: -"Structured advisories attached when the tool detected a non-fatal authoring issue (e.g. legacy bridge usage). Each advisory.summary is also appended to the text content for the LLM path."New value: +"Structured non-fatal advisories, including authoring issues and unread staff feedback responses. Each advisory.summary is also appended to the text content for the LLM path."
    • addedOutput schema / properties / appId
      Added value: +{
      +  "description": "Existing target app ID, or the created app ID after publication.",
      +  "type": "string"
      +}
    • removedOutput schema / properties / appName / anyOf
      Removed value: -[
      -  {
      -    "type": "string"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / appName / type
      Added value: +[
      +  "string",
      +  "null"
      +]
    • addedOutput schema / properties / attempts
      Added value: +{
      +  "maximum": 9007199254740991,
      +  "minimum": 0,
      +  "type": "integer"
      +}
    • addedOutput schema / properties / buildId
      Added value: +{
      +  "description": "Durable build ID. Use get_app_build to inspect progress or source.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / deadline
      Added value: +{
      +  "type": "string"
      +}
    • addedOutput schema / properties / desiredRevision
      Added value: +{
      +  "maximum": 9007199254740991,
      +  "minimum": 0,
      +  "type": "integer"
      +}
    • addedOutput schema / properties / elapsedMs
      Added value: +{
      +  "minimum": 0,
      +  "type": "number"
      +}
    • addedOutput schema / properties / error
      Added value: +{
      +  "additionalProperties": false,
      +  "properties": {
      +    "column": {
      +      "maximum": 9007199254740991,
      +      "minimum": -9007199254740991,
      +      "type": "integer"
      +    },
      +    "kind": {
      +      "type": "string"
      +    },
      +    "line": {
      +      "maximum": 9007199254740991,
      +      "minimum": -9007199254740991,
      +      "type": "integer"
      +    },
      +    "message": {
      +      "type": "string"
      +    },
      +    "retryable": {
      +      "type": "boolean"
      +    },
      +    "specifier": {
      +      "type": "string"
      +    },
      +    "target": {
      +      "type": "string"
      +    }
      +  },
      +  "required": [
      +    "kind",
      +    "message",
      +    "retryable"
      +  ],
      +  "type": "object"
      +}
    • addedOutput schema / properties / finishedAt
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • changedOutput schema / properties / icon / description
      Previous value: -"The effective home-screen icon stored for the app, after normalization. Read this back to confirm what stuck: if you sent a `manifest.icon` and this is the default `{ emoji: \"🧱\", bg: \"#3b82f6\" }`, your icon was invalid and rejected — see `warnings`."New value: +"The effective home-screen icon stored for the app. Canonical source declares it at `manifest.meta.icon`."
    • addedOutput schema / properties / idempotencyExpiresAt
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / inputDigest
      Added value: +{
      +  "type": "string"
      +}
    • addedOutput schema / properties / inspectionExpiresAt
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / intent
      Added value: +{
      +  "description": "The accepted operation: create, update, migrate, restore, or copy.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / lockDigest
      Added value: +{
      +  "type": "string"
      +}
    • addedOutput schema / properties / lockState
      Added value: +{
      +  "enum": [
      +    "locked",
      +    "unresolved"
      +  ],
      +  "type": "string"
      +}
    • removedOutput schema / properties / ok / description
      Removed value: -"Indicates success. Errors arrive as content with isError:true."
    • addedOutput schema / properties / queueDeadline
      Added value: +{
      +  "type": "string"
      +}
    • addedOutput schema / properties / resolvedDependencies
      Added value: +{
      +  "additionalProperties": false,
      +  "properties": {
      +    "client": {
      +      "additionalProperties": {
      +        "type": "string"
      +      },
      +      "propertyNames": {
      +        "type": "string"
      +      },
      +      "type": "object"
      +    },
      +    "server": {
      +      "additionalProperties": {
      +        "type": "string"
      +      },
      +      "propertyNames": {
      +        "type": "string"
      +      },
      +      "type": "object"
      +    }
      +  },
      +  "required": [
      +    "server",
      +    "client"
      +  ],
      +  "type": "object"
      +}
    • addedOutput schema / properties / retryAfterSeconds
      Added value: +{
      +  "description": "Wait at least this many seconds before polling again.",
      +  "exclusiveMinimum": 0,
      +  "maximum": 9007199254740991,
      +  "type": "integer"
      +}
    • addedOutput schema / properties / revision
      Added value: +{
      +  "description": "Server-owned app source revision. Historical null counters read as 0; new apps start at 1; each successful source write advances it once. Pass this value through `expected_revision` when guarding update_app.",
      +  "maximum": 9007199254740991,
      +  "minimum": 0,
      +  "type": "integer"
      +}
    • addedOutput schema / properties / source
      Added value: +{
      +  "additionalProperties": false,
      +  "description": "Exact accepted source, returned only when include_source is true.",
      +  "properties": {
      +    "description": {
      +      "type": [
      +        "string",
      +        "null"
      +      ]
      +    },
      +    "module": {
      +      "type": "string"
      +    },
      +    "styles": {
      +      "type": [
      +        "string",
      +        "null"
      +      ]
      +    },
      +    "ui": {
      +      "type": [
      +        "string",
      +        "null"
      +      ]
      +    }
      +  },
      +  "required": [
      +    "module",
      +    "ui",
      +    "styles",
      +    "description"
      +  ],
      +  "type": "object"
      +}
    • addedOutput schema / properties / sourceEtag
      Added value: +{
      +  "description": "ETag identifying the immutable input accepted for this build.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / state
      Added value: +{
      +  "enum": [
      +    "queued",
      +    "resolving",
      +    "building",
      +    "validating",
      +    "published",
      +    "failed",
      +    "superseded",
      +    "canceled",
      +    "expired"
      +  ],
      +  "type": "string"
      +}
    • addedOutput schema / properties / statusUrl
      Added value: +{
      +  "description": "Authenticated HTTP status URL for this build.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / updatedAt
      Added value: +{
      +  "type": "string"
      +}
    • changedOutput schema / properties / url / description
      Previous value: -"Machine/API URL for the app (stable /app/<uuid> form). Embeds a write-capable ?t= access token — NEVER show, paste, or send it to the user. Append /api/<op> to its path for out-of-band operation calls. For anything user-facing, use shareUrl instead."New value: +"App URL. Present only after successful publication."
    • removedOutput schema / properties / version
      Removed value: -{
      -  "description": "Monotonic int version of the app. 0 on truly-legacy rows that predate versioning (still editable — pass `expected_version: 0`); otherwise a positive int bumped on every successful update_app/PUT. Pass back as `expected_version` on edits-aware update_app and as `If-Match: \"v<N>\"` on PATCH /app/:id/source.",
      -  "maximum": 9007199254740991,
      -  "minimum": -9007199254740991,
      -  "type": "integer"
      -}
    • changedOutput schema / properties / warnings / description
      Previous value: -"Non-blocking publish feedback (#1126): present when static validation found UI/backend contract mismatches, or when a provided `manifest.icon` was invalid and coerced to the default. The write succeeded; fix by adding the backend op, renaming the UI call, or correcting the icon `{ emoji, bg }`."New value: +"Non-blocking publish feedback (#1126): present when static validation found UI/backend contract mismatches or legacy icon input needed a fallback. The write succeeded; fix the named source field."
    • changedOutput schema / required
      Previous value: -[
      -  "ok",
      -  "id",
      -  "url",
      -  "shareUrl",
      -  "version",
      -  "icon"
      -]New value: +[
      +  "ok"
      +]
  2. Changed1 schema field changed
    • changedInput schema / properties / ui / description
      Previous value: -"Frontend JavaScript rendered into #app. The #app mount point starts empty, so (a) populate its innerHTML before attaching event listeners, and (b) call operations as `window.buildy.api('<manifest-id>').<opName>(input)` (operation name = method name, strips the { ok, value } envelope — you receive the value only, never ok) — not `api.operation(name, params)` and not raw fetch(), which fails in the null-origin srcdoc iframe. The signed-in caller is exposed synchronously as `window.buildy.user` (`{ id, handle?, name?, image? }`, or `null` for an anonymous visitor) — read `user.name` to greet, attribute, or personalize, instead of asking the user to type their name. PUBLIC fields only (never email); it is a convenience signal, not enforcement. The outermost container MUST fill the viewport — use `<main class=\"min-h-screen\">` (or a grid/flex layout that spans width) as the root shell. Do NOT wrap the root in `max-w-md`, `max-w-2xl`, or `container mx-auto`: those cap the entire app to a narrow central column with wide empty margins on 2K+ monitors — the loudest \"AI-generated app\" tell. If the view is text-heavy (a note, an article, a form with long prose), cap the reading measure on an INNER wrapper only, e.g. `<main class=\"min-h-screen\"><div class=\"mx-auto max-w-2xl\">…</div></main>`. Dashboards, kanban, tables, canvases, galleries, and split views should use the full width. For live updates when an agent mutates state from another session, register `window.buildy.onStateChange((e) => { ... })` and update the DOM surgically rather than wiping #app. See the buildy:app-guide prompt for a canonical example."New value: +"Frontend JavaScript rendered into #app. The #app mount point starts empty, so (a) populate its innerHTML before attaching event listeners, and (b) call operations as `window.buildy.api('<manifest-id>').<opName>(input)` (operation name = method name, strips the { ok, value } envelope — you receive the value only, never ok) — not `api.operation(name, params)` and not raw fetch(), which fails in the null-origin srcdoc iframe. The signed-in caller is exposed synchronously as `window.buildy.user` (`{ id, handle?, name?, image? }`, or `null` for an anonymous visitor) — read `user.name` to greet, attribute, or personalize, instead of asking the user to type their name. PUBLIC fields only (never email); it is a convenience signal, not enforcement. The outermost container MUST fill the viewport — use `<main class=\"min-h-screen\">` (or a grid/flex layout that spans width) as the root shell. Do NOT wrap the root in `max-w-md`, `max-w-2xl`, or `container mx-auto`: those cap the entire app to a narrow central column with wide empty margins on 2K+ monitors — the loudest \"AI-generated app\" tell. If the view is text-heavy (a note, an article, a form with long prose), cap the reading measure on an INNER wrapper only, e.g. `<main class=\"min-h-screen\"><div class=\"mx-auto max-w-2xl\">…</div></main>`. Dashboards, kanban, tables, canvases, galleries, and split views should use the full width. For live updates when an agent mutates state from another session, register `window.buildy.onStateChange((e) => { ... })` and update the DOM surgically rather than wiping #app. See the charming:app-guide prompt for a canonical example."
  3. First observed

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations, the description discloses significant behavioral details: reusing a manifest.id the caller owns replaces the existing app module/UI/styles in place; idempotency_key enables background builds with a buildId to poll; the shareUrl field is user-facing while the url field embeds a write-capable token. It also documents storage semantics, capability declarations, and failure modes (e.g., localStorage silently vanishes). The destructiveHint annotation is consistent with the described replacement behavior, with no contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is exceptionally long and dense, but nearly every sentence carries a rule or operational constraint necessary for this tool's dual-contract complexity. It is well front-loaded with the core purpose and key side-effect warnings. It loses one point because it duplicates some content that already lives in the rich parameter docs and could be tightened without losing meaning.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity—two authoring contracts, background builds, destructive replace semantics, capability declarations, storage constraints, and a machine-only URL security warning—the description is remarkably complete. The presence of an output schema reduces the need to explain return values, and the description still covers the critical result fields (buildId, shareUrl). Nothing an agent needs to call this tool correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3, but the description adds substantial operational meaning: idempotency_key is tied to background builds and retries, expected_revision is required for reuse of an existing manifest.id, team_id is owner/admin-gated, and the module/ui/description params carry distinct authoring contracts. The description also explains result semantics (shareUrl vs url) that are not in the schema. It goes far beyond merely restating parameter names.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Create a new interactive web app and render it inline. Use this to build a new app from a request.' It immediately distinguishes itself from the sibling update_app by saying 'use update_app to edit a known app ID by UUID,' and it clarifies the existing-contract vs ESM contract behavior. An agent can confidently select this tool for new-app creation rather than mutation or query tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use guidance: build a new app from a request, with a clear alternative for edits (update_app). It also handles unsupported requests explicitly: 'do not fake support. Build the closest local-state version, then log the gap with submit_feedback.' It points to read_docs for full documentation and to the charming:app-guide prompt for canonical examples. This is exemplary routing and exclusion guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.