---
name: build-a-charming-app
description: Create, host, and iterate on a personal interactive web app with Charming — a real URL, persistent storage, and an MCP or HTTP API — from any agent.
---

# Build a Charming app

Charming turns a natural-language description into a hosted web app with a real URL and a database. Use it from any MCP-capable client or over plain HTTP; the HTTP path builds a first app with no signup.

## When to use this

Reach for Charming when the user wants a small app that outlives the chat: a tracker, a dashboard, a form, a personal tool. Charming gives it a persistent URL, storage, and an API your agent can keep using and updating across sessions.

## Two surfaces

- **MCP** (Claude, ChatGPT, Cursor, Claude Code, and more): add `https://charm.ing/mcp` as an MCP server, then call the tools. Per-client setup strings: https://usecharming.com/clients.txt
- **HTTP**: `POST https://charm.ing/app` with the app source. Full contract: https://charm.ing/.well-known/openapi.json

## The app contract

An app is one ES module with two named exports, `manifest` and `routes`. Every new app uses this shape on both surfaces.

```js
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: 'Add one and return the new value.',
    inputSchema: { type: 'object', properties: {}, additionalProperties: false },
    outputSchema: { type: 'object', properties: { count: { type: 'integer' } } },
    annotations: { readOnlyHint: false },
    public: true,
    examples: [{ input: {}, output: { count: 1 } }],
    handler: async (input, { env, ctx, request }) => {
      const count = ((await env.storage.get('count')) ?? 0) + 1;
      await env.storage.put('count', count);
      return { count };
    },
  },
];
```

`manifest` is parsed statically, so keep it a plain literal with no computed values. `capabilities` accepts `imports` and nothing else: `charming:storage/kv@1.0` gives `env.storage`, `charming:storage/blob@1.0` gives `env.assets`, `charming:logging/emit@1.0` gives `env.log`, `charming:network/fetch@1.0` gives plain outbound `fetch` and `charming:secrets/fetch@1.0` gives the sealed `env.fetch` that substitutes `{{secret:NAME}}` into headers. Both are claimed-apps-only, and `permissions.server.fetch` gates every backend egress path, plain and sealed alike: list each allowed origin there, because an import without origins leaves egress blocked and origins without the import do too. Remote image origins go in `permissions.browser["img-src"]`. Each origin is a concrete `https://` host. `$schema` may be omitted on create; Charming inserts the dated URL.

`routes` is an array, never an object keyed by operation name. Only `op` and `handler` are required; the rest of the metadata is what Charming reads to route `query_app` versus `mutate_app`, to answer `get_app`, and to build the app's OpenAPI document, so declare a read with `annotations: { readOnlyHint: true }`. A default export is optional: Charming serves the unmatched-path 404 itself.

## Build flow (MCP)

1. `create_app` — author a new app from a name plus source (an ES module for logic + UI/styles). The response carries the live URL.
2. `update_app` — iterate on the source.
3. `query_app` / `mutate_app` — read and write the app's stored state.
4. `share_app` / `set_remixable` — share with people or let any visitor fork the URL.

Generated app HTML talks to its backend through the injected `window.charming` API, never raw `fetch`: `const api = window.charming.api('counter'); await api.increment({});`.

## Learn more

- Start here: https://usecharming.com/start.md
- Full build manual: https://usecharming.com/llms-full.txt
- Design rules (avoid the generic AI-app look): https://usecharming.com/design.md

## Auth

MCP always needs a bearer token: a request without one gets a 401 whose `WWW-Authenticate` header points at `https://charm.ing/.well-known/oauth-protected-resource`, and a client that speaks OAuth Dynamic Client Registration bootstraps the token itself through a one-time consent screen. Anonymous creation is the HTTP path only: `POST /app` with no `Authorization` header returns the app plus a one-time `chrm_app_*` token, and an unclaimed app expires 7 days after creation. For an account-scoped `chrm_user_*`, run device pairing: `POST https://charm.ing/api/pair/start`, show the returned `user_code` to the user, then poll `POST https://charm.ing/api/pair/poll`. See https://usecharming.com/auth.md.
