# Charming

> Create, update, inspect, or troubleshoot Charming-hosted interactive web apps via the Charming MCP server, or connect an AI client (Claude, Cursor, Codex, Gemini CLI, ChatGPT) to Charming.

- Skill: `tambo-labs/charming` (Agent Skill, multi-file: 14 files)
- Install (CLI): `npx skillmds@latest add tambo-labs/charming`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tambo-labs/charming/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: tambo-labs (https://skillmd.com/u/tambo-labs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tambo-labs/charming

---


# Charming

## Overview

Use this skill when the user asks to create an app with Charming, edit an existing Charming app, connect the Charming MCP server, or inspect Charming-generated app source.

Charming is a hosted service that generates, hosts, and updates small interactive web apps. Each app gets a real URL, persistent storage, and inline rendering inside MCP Apps-capable chat clients. It has two authoring paths:

- **MCP tools** (preferred): use this path whenever Charming tools such as `create_app`, `update_app`, `list_apps`, or `get_app` are available to you.
- **HTTP API** (fallback): use only when Charming MCP tools are not available and you have outbound HTTP access.

## Connect

Charming is a remote server; there is nothing to clone, build, or run locally. Add the endpoint to your client's MCP settings:

| Client | Endpoint |
|--------|----------|
| Claude, Claude Code, Cursor, Codex, Gemini CLI, and most MCP clients | `https://charm.ing/mcp` |
| ChatGPT | the Charming listing in the ChatGPT Apps directory is one click; `https://charm.ing/mcp/chatgpt` is the Developer Mode fallback |

Charming's MCP endpoints always require a bearer token. OAuth with Dynamic Client Registration bootstraps one automatically on first connect: clients that support it show a one-time consent screen, with no API key to paste. Anonymous creation with no token is available on the HTTP path only (`POST https://charm.ing/app`), and an unclaimed app has a 7-day TTL until someone claims it. To mint a token by hand, use device pairing: `POST https://charm.ing/api/pair/start`, then poll. Full auth guide: [usecharming.com/auth.md](https://usecharming.com/auth.md). Per-client paste-strings and setup steps: [usecharming.com/clients.txt](https://usecharming.com/clients.txt).

## MCP workflow

When Charming MCP tools are available, build with the tools directly. Do not use `curl`, `fetch`, shell HTTP calls, or the raw HTTP API for app creation.

1. Read the starter guide at [usecharming.com/build-mcp.md](https://usecharming.com/build-mcp.md).
2. Read the design guide at [usecharming.com/design.md](https://usecharming.com/design.md) before writing UI code.
3. Create the first version with `create_app({ description, module, ui, styles? })`. `description` is required.
4. Give the user the returned app URL and `app_id`.
5. If the host does not render the app inline, share the returned URL so the user can open it directly. Do not claim the app is visible inline unless the host actually rendered it.
6. Offer one concrete next iteration, then call `update_app` only after the user agrees.
7. If the user hits a Charming limitation, call `submit_feedback({ app_id, text })` instead of silently working around it.

Charming exposes 24 tools; connected clients discover them via `tools/list`. The core authoring set is `create_app`, `update_app`, `get_app`, `get_app_source`, `list_apps`, and `delete_app`. Beyond those, `query_app` / `mutate_app` run an app's backend operations without editing code; `upload_asset` stores static assets; `share_app` / `unshare_app` / `list_app_shares`, `set_public` / `unset_public`, and `set_remixable` / `unset_remixable` control access and remixing; `set_template` / `unset_template` publish an app as a copyable template and `search_templates` searches the public directory; `rename_app`, `set_handle`, and `set_starter_prompt` manage URLs and share prompts; `submit_feedback` / `list_feedback` capture app feedback. The full descriptions live in the [README](./README.md) and [usecharming.com/llms-full.txt](https://usecharming.com/llms-full.txt).

Generated app code must follow the Charming contract:

- `module` is one ES module with two named exports and no required default export:
  - `manifest`: a plain literal, parsed statically, so no computed values. It carries `$schema` (the current dated manifest schema), `id`, `meta.name`, an optional `meta.icon` of one emoji plus a hex background, and `capabilities.imports` listing only what the app uses.
  - `routes`: an array of operations. Each has `op` (unique), `method`, `path`, `title`, `description`, `inputSchema`, `outputSchema`, `annotations`, optional `public` and `examples`, and a `handler(input, { env, ctx, request })` that returns a JSON-compatible value. Set `annotations.readOnlyHint` to `true` on reads, or `query_app` and viewers cannot call them.
- `ui` is one inline JavaScript program that populates `#app` and calls the backend through `window.charming.api(manifest.id).<op>(input)`, which resolves to the value directly and throws on failure.
- `env.storage` is Workers KV with only `.get(key)`, `.put(key, value)`, `.delete(key)`, and `.list()`. It stores JSON-compatible values directly: never `JSON.stringify` before `put` or `JSON.parse` after `get`.
- Subscribe with `window.charming.onStateChange(cb)` so the UI updates when the agent runs the app's operations from another session. Apply surgical updates such as `textContent` rather than replacing `innerHTML`, so the user's focus, selection, and typing survive.
- `window.charming` also carries what a shared or embedded app needs: `viewer.role` and `viewer.can(op)` to render only what this visitor may do, `user` for the caller's public identity, `login()` to trigger sign-in, `assets` and `images` for files and pictures, plus `openLink`, `sendFollowUp`, `updateContext`, `recordAction`, and `isConnected` / `onConnectionChange`. Neither `viewer` nor `user` is an enforcement boundary; the server gates are.
- To show an external image inside a Claude or ChatGPT embed, use `window.charming.images.load(url)`, which returns a `data:` URL. Both hosts inject a CSP that blocks a cross-origin `images.proxy(url)` URL.
- Do not manage tokens in UI code; credentials attach automatically.
- The `ui` program runs as a classic script, so no ESM syntax and no top-level `await`.
- No Node APIs, no DOM APIs in the backend, no external UI scripts, no native form submit, and no `alert`, `confirm`, or `prompt` (they silently no-op in the sandboxed iframe). Outbound backend `fetch` is not banned; it is off until declared, see capabilities below.

Declare only the capabilities the app uses in `manifest.capabilities.imports`; the host rejects strings it does not know, so never invent one:

| Import | Grants |
|--------|--------|
| `charming:storage/kv@1.0` | `env.storage`, the key-value store |
| `charming:storage/blob@1.0` | `env.assets`, for uploaded files |
| `charming:logging/emit@1.0` | `env.log` |
| `charming:network/fetch@1.0` | plain backend `fetch`, claimed apps only |
| `charming:secrets/fetch@1.0` | sealed `env.fetch` that substitutes `{{secret:NAME}}` into headers, claimed apps only |
| `charming:browser/<name>@1.0` | a claim-gated browser permission such as camera or microphone |
| `charming:app/<id>@x.y` | operations from another app |

Two allowlists sit outside `capabilities`, both under `permissions`, and both take exact `https://host` origins:

- `permissions.server.fetch` gates every backend egress path, plain and sealed alike. The import and the origins are both required: origins on their own persist but leave egress blocked (the server only warns), and the sealed path denies outright on an empty list.
- `permissions.browser["img-src"]` is required for external images. Without it the image simply does not render.

Legacy `export default { fetch(request, env, ctx) }` still works, and on a canonical app it serves as the unmatched-path fallback, but a legacy operation carries no method or read-only metadata: it defaults to `POST` and `readOnly: false`, so it cannot be reached through `query_app` or by a viewer. Author operations as routes. `manifest.capabilities.exports` belongs to the older manifest shape and is rejected outright by the current schema, and an app created over MCP must use the canonical contract. Editing an existing legacy app over MCP returns `contract_migration_required`: resend the full canonical source with `migrate_contract: true`, rewriting any legacy `window.buildy` call in the UI to `window.charming`.

```js
// module
export const manifest = {
  $schema: 'https://charm.ing/schema/app-manifest/2026-07-31.json',
  id: 'counter',
  meta: { name: 'Counter', icon: { emoji: '➕', bg: '#1f6e68' } },
  capabilities: { imports: ['charming:storage/kv@1.0'] },
};

export const routes = [
  {
    op: 'increment',
    method: 'POST',
    path: '/api/increment',
    title: 'Increment counter',
    description: 'Increment the counter and return the new value.',
    inputSchema: { type: 'object', properties: {}, additionalProperties: false },
    outputSchema: {
      type: 'object',
      required: ['count'],
      properties: { count: { type: 'integer' } },
      additionalProperties: false,
    },
    annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
    handler: async (_input, { env }) => {
      const count = ((await env.storage.get('count')) ?? 0) + 1;
      await env.storage.put('count', count);
      return { count };
    },
  },
];

// ui
const api = window.charming.api('counter');
const root = document.getElementById('app');
root.innerHTML = '<button id="b">+1</button><span id="c">0</span>';
const display = document.getElementById('c');
document.getElementById('b').onclick = async () => {
  const { count } = await api.increment({});
  display.textContent = count;
};
// e is { kind: 'state-changed', op, source: 'agent' | 'reconnect-resync', ts, result }.
// onStateChange returns an unsubscribe function.
window.charming.onStateChange((e) => {
  if (e.source === 'reconnect-resync') {
    // The connection was idle too long for the server to say what was missed.
    // Refetch through a read route, or no-op until the next agent action.
    return;
  }
  if (e.result && typeof e.result === 'object' && 'count' in e.result) {
    display.textContent = String(e.result.count);
  }
});
```

## HTTP fallback

When Charming MCP tools are not available, follow [usecharming.com/start.md](https://usecharming.com/start.md) and then [usecharming.com/build-http.md](https://usecharming.com/build-http.md). Prefer MCP setup first if the user asked for a connector or if the client can install the MCP server.

## Output expectations

When creating or updating an app, end with the app URL, the `app_id`, what changed, and the next practical iteration.

