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. Per-client paste-strings and setup steps: 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.
- Read the starter guide at usecharming.com/build-mcp.md.
- Read the design guide at usecharming.com/design.md before writing UI code.
- Create the first version with
create_app({ description, module, ui, styles? }). description is required.
- Give the user the returned app URL and
app_id.
- 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.
- Offer one concrete next iteration, then call
update_app only after the user agrees.
- 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 and 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.
// 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 and then 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.
Codex notes
- In Codex app sessions with a browser tool available, open the returned URL in Codex's in-app browser when MCP Apps inline UI does not visibly mount in the conversation.
- Codex CLI users who connect Charming by hand instead of installing the plugin use this
config.toml entry:
[mcp_servers.charming]
url = "https://charm.ing/mcp"
1---2name: charming-23description: 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.4---56<!-- Generated by scripts/build-packages.mjs from canonical/. Do not edit. -->78# Charming910## Overview1112Use 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.1314Charming 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:1516- **MCP tools** (preferred): use this path whenever Charming tools such as `create_app`, `update_app`, `list_apps`, or `get_app` are available to you.17- **HTTP API** (fallback): use only when Charming MCP tools are not available and you have outbound HTTP access.1819## Connect2021Charming is a remote server; there is nothing to clone, build, or run locally. Add the endpoint to your client's MCP settings:2223| Client | Endpoint |24|--------|----------|25| Claude, Claude Code, Cursor, Codex, Gemini CLI, and most MCP clients | `https://charm.ing/mcp` |26| ChatGPT | the Charming listing in the ChatGPT Apps directory is one click; `https://charm.ing/mcp/chatgpt` is the Developer Mode fallback |2728Charming'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).2930## MCP workflow3132When 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.33341. Read the starter guide at [usecharming.com/build-mcp.md](https://usecharming.com/build-mcp.md).352. Read the design guide at [usecharming.com/design.md](https://usecharming.com/design.md) before writing UI code.363. Create the first version with `create_app({ description, module, ui, styles? })`. `description` is required.374. Give the user the returned app URL and `app_id`.385. 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.396. Offer one concrete next iteration, then call `update_app` only after the user agrees.407. If the user hits a Charming limitation, call `submit_feedback({ app_id, text })` instead of silently working around it.4142Charming 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).4344Generated app code must follow the Charming contract:4546- `module` is one ES module with two named exports and no required default export:47 - `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.48 - `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.49- `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.50- `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`.51- 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.52- `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.53- 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.54- Do not manage tokens in UI code; credentials attach automatically.55- The `ui` program runs as a classic script, so no ESM syntax and no top-level `await`.56- 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.5758Declare only the capabilities the app uses in `manifest.capabilities.imports`; the host rejects strings it does not know, so never invent one:5960| Import | Grants |61|--------|--------|62| `charming:storage/kv@1.0` | `env.storage`, the key-value store |63| `charming:storage/blob@1.0` | `env.assets`, for uploaded files |64| `charming:logging/emit@1.0` | `env.log` |65| `charming:network/fetch@1.0` | plain backend `fetch`, claimed apps only |66| `charming:secrets/fetch@1.0` | sealed `env.fetch` that substitutes `{{secret:NAME}}` into headers, claimed apps only |67| `charming:browser/<name>@1.0` | a claim-gated browser permission such as camera or microphone |68| `charming:app/<id>@x.y` | operations from another app |6970Two allowlists sit outside `capabilities`, both under `permissions`, and both take exact `https://host` origins:7172- `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.73- `permissions.browser["img-src"]` is required for external images. Without it the image simply does not render.7475Legacy `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`.7677```js78// module79export const manifest = {80 $schema: 'https://charm.ing/schema/app-manifest/2026-07-31.json',81 id: 'counter',82 meta: { name: 'Counter', icon: { emoji: '➕', bg: '#1f6e68' } },83 capabilities: { imports: ['charming:storage/kv@1.0'] },84};8586export const routes = [87 {88 op: 'increment',89 method: 'POST',90 path: '/api/increment',91 title: 'Increment counter',92 description: 'Increment the counter and return the new value.',93 inputSchema: { type: 'object', properties: {}, additionalProperties: false },94 outputSchema: {95 type: 'object',96 required: ['count'],97 properties: { count: { type: 'integer' } },98 additionalProperties: false,99 },100 annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },101 handler: async (_input, { env }) => {102 const count = ((await env.storage.get('count')) ?? 0) + 1;103 await env.storage.put('count', count);104 return { count };105 },106 },107];108109// ui110const api = window.charming.api('counter');111const root = document.getElementById('app');112root.innerHTML = '<button id="b">+1</button><span id="c">0</span>';113const display = document.getElementById('c');114document.getElementById('b').onclick = async () => {115 const { count } = await api.increment({});116 display.textContent = count;117};118// e is { kind: 'state-changed', op, source: 'agent' | 'reconnect-resync', ts, result }.119// onStateChange returns an unsubscribe function.120window.charming.onStateChange((e) => {121 if (e.source === 'reconnect-resync') {122 // The connection was idle too long for the server to say what was missed.123 // Refetch through a read route, or no-op until the next agent action.124 return;125 }126 if (e.result && typeof e.result === 'object' && 'count' in e.result) {127 display.textContent = String(e.result.count);128 }129});130```131132## HTTP fallback133134When 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.135136## Output expectations137138When creating or updating an app, end with the app URL, the `app_id`, what changed, and the next practical iteration.139140## Codex notes141142- In Codex app sessions with a browser tool available, open the returned URL in Codex's in-app browser when MCP Apps inline UI does not visibly mount in the conversation.143- Codex CLI users who connect Charming by hand instead of installing the plugin use this `config.toml` entry:144145```toml146[mcp_servers.charming]147url = "https://charm.ing/mcp"148```