# Build with AI

> This page is for people building apps with AI tools (Claude Code, Cursor, Codex, Copilot…) and for the AI tools themselves.

## Give your AI tool the context

| Option | Use when | What the AI can do |
|---|---|---|
| **`pos-app` MCP server** (recommended) | Your AI tool supports MCP | Look up the docs, see the shop's real ids, check the app, try it in POS, read errors, deploy, report bugs to Pancake |
| **`AGENTS.md`** | Every project created with `create-pos-app` | Read the mandatory rules on every change |
| **`llms-full.txt`** | Tools without MCP, or chat on the web | Read all the docs at once |

They work together: a `create-pos-app` project already has `AGENTS.md`; just add the MCP server.

## MCP

`pos-app mcp` starts an [MCP](https://modelcontextprotocol.io) server so AI tools can look up the docs, see the shop's real metadata, check the app, read runtime errors and deploy without leaving the chat. It runs on your machine (stdio), in the app's project folder, with the account signed in through `pos-app login`.

### Connect the MCP server

1. Sign the CLI in once: in POS go to **Settings › Advanced › Custom apps**, click **Connect CLI › Create token** and run the command it shows (see [Manage apps › Connect the CLI](/developers/apps/en/manage-apps#connect-the-cli)).
2. Add the server to your AI tool, in the app's project folder (replace `123` with your shop id):

```bash
# Claude Code
claude mcp add pos-app -- npx @pancake-pos/app-cli mcp --shop 123
```

Cursor (`.cursor/mcp.json` in the project):

```json
{
  "mcpServers": {
    "pos-app": { "command": "npx", "args": ["@pancake-pos/app-cli", "mcp", "--shop", "123"] }
  }
}
```

VS Code with Copilot (`.vscode/mcp.json` in the project):

```json
{
  "servers": {
    "pos-app": { "type": "stdio", "command": "npx", "args": ["@pancake-pos/app-cli", "mcp", "--shop", "123"] }
  }
}
```

```toml
# Codex: ~/.codex/config.toml
[mcp_servers.pos-app]
command = "npx"
args = ["@pancake-pos/app-cli", "mcp", "--shop", "123"]
```

Claude Desktop (`claude_desktop_config.json`) does not run inside the project folder, so add `--cwd`:

```json
{
  "mcpServers": {
    "pos-app": { "command": "npx", "args": ["@pancake-pos/app-cli", "mcp", "--shop", "123", "--cwd", "/path/to/my-app"] }
  }
}
```

3. Restart the AI tool and ask it to "list the pos-app tools". In Claude Code, `/mcp` shows whether the server is connected.

### Tools

| Tool | Input | Returns |
|---|---|---|
| `search_docs` | `query` | Matching sections of these docs, as Markdown |
| `get_target` | `target`, for example `purchase.details.action` | Context data schema, expected return value, manifest fields and a short example |
| `list_shop_meta` | `resource` | Ids and names the app can use in `conditions` and code: statuses, tags, warehouses, product categories, order sources |
| `sample_context` | `target`, optional record id | A realistic `pos.data` built from the shop's data, filtered by the app's `access` and the signed-in user's permissions |
| `check_app` | none | The `pos-app check` result: each problem with file, line, rule id and a suggested fix |
| `dev_start`, `dev_status` | none | Starts `pos-app dev`; returns the local URL, the POS link to open in development mode and build errors |
| `get_errors` | `version` (`dev` by default), `since` | Runtime errors POS recorded for the app: target, message, stack trace, time |
| `deploy` | `message` | Runs `check` and `build`, then uploads a new **inactive** version; returns the version and the link to the custom apps page |
| `send_feedback` | `kind` (`bug` or `feature`), `title`, `description`, `include_errors` | Sends a POS Apps bug report or feature request to Pancake's POS support team; returns an `FB-…` id |

- `deploy` never activates a version. An admin reviews access and activates it on the [Custom apps](/developers/apps/en/manage-apps#review-and-activate-a-version) page, exactly as with `pos-app deploy`.
- If `version` in `pos-app.json` already exists, `deploy` fails with `duplicate_version`; bump `version` and deploy again.
- Tools act as the signed-in user on the given shop. They never return app tokens or API keys, and they do not write shop data: `list_shop_meta` and `sample_context` only read. App code still reads and writes data through `pos.api`.

### Typical loop

1. `get_target` and `search_docs` to pick the right targets and types.
2. `list_shop_meta` and `sample_context` to write `conditions` and code against real ids and fields.
3. Write the code, then `check_app` until it reports no errors.
4. `dev_start`, try the app in POS, then `get_errors` to fix runtime errors.
5. `deploy` and ask an admin to activate the new version.

### Sample prompts

```text
Create a POS app "Debt reminder": a block in the order form showing the customer's total debt,
and a "Send reminder" button that calls https://crm.example.com/api/remind. Read-only access is enough.
Use sample_context for real order data, run check_app until it is clean, then dev_start so I can try it in POS.
```

```text
The "Push to ERP" button in version 1.2.0 is failing. Read get_errors for that version, find the cause, fix it,
bump the version and deploy with a note.
```

```text
Show the "Approve order" action only for orders waiting for confirmation that come from Facebook.
Get the status and order source ids with list_shop_meta instead of hard-coding names.
```

## Report bugs and request features

When you hit a platform bug (runtime, components, SDK, CLI, MCP, docs, Open API) or need something that does not exist yet, ask your AI tool to send it to Pancake with the `send_feedback` tool:

```text
Report to Pancake: pos-table does not show the date column when format is "date".
```

1. The AI drafts a title and description (what happened, what you expected, steps to reproduce; or the need and the use case) and shows it to you first.
2. Only after you agree does it send. The report goes straight to Pancake's POS support channel; the AI gives you an `FB-…` id to quote when following up.

Reports automatically include the CLI and runtime versions, OS, Node, the app handle and version, and the app's 5 most recent runtime errors (skip them with `include_errors: false`). Code and shop data are never sent; tokens pasted by mistake are masked before sending. Each shop can send up to 20 reports per day.

## Without MCP

- All docs as Markdown: `/developers/apps/llms-full.txt`. Index: `/developers/apps/llms.txt`. Every page has a Markdown version at the same path with `.md`, for example `/developers/apps/targets.md`.
- These LLM files are in English by default. Vietnamese versions live under `/developers/apps/vi/`, for example `/developers/apps/vi/llms-full.txt`.
- The `create-pos-app` template generates an `AGENTS.md` with the content in the [last section](#sample-agents-md). Keep it in the project so AI tools always read the rules.

Sample prompt:

```text
Read https://pos.pages.fm/developers/apps/llms-full.txt.
Create a POS app "Debt reminder": a block in the order form showing the customer's total debt,
and a "Send reminder" button that calls https://crm.example.com/api/remind. Read-only access is enough.
```

## Mandatory rules

### Runtime environment

1. Code in `extensions/` runs in a **Web Worker** with no real DOM.
   - Do not use `window`, `document.querySelector`, `document.getElementById`, `localStorage`, `sessionStorage`, `alert`, `confirm`, `canvas`, `getBoundingClientRect`.
   - Do not import UI libraries (antd, MUI, Tailwind, chart libraries, drag-and-drop libraries) or CSS files.
   - Use Preact (recommended) or plain JS to render `pos-*` elements into `document.body`. Do not use React in `extensions/`.
   - Do not install any library to connect to POS or render UI; the POS runtime is already in the worker.
2. Iframe pages (`page/`) are regular web apps: any library is allowed, but they must get `pos` from `connect()`, must not access `window.top`/`window.parent`, and must not use routes starting with `/_pos/` (reserved for POS).

### UI

3. Worker UI extensions use **only** elements from [UI components](/developers/apps/en/components). Do not use `div`, `span`, `p`, `img`, `table`, `button`, `input`.
4. Text must be inside `pos-text`, `pos-heading`, `pos-badge`, `pos-link`, `pos-button` or `pos-banner`.
5. Do not use `style` or `className`. Event values come from `event.detail`.
6. Display amounts with `pos-money` or a column with `format: 'money'`; do not format them yourself.

### Data

7. Get POS data only through `pos.api`, with paths like `/shops/${pos.shop.id}/...` and endpoints listed in the [Open API](/developers/api/en).
8. Never hardcode a shop id, API key or token; never send the app token elsewhere, except to the backend of a [self-hosted page](/developers/apps/en/runtime-api#self-hosted-pages); never put secrets in code.
9. Every method used in code (`POST`, `PUT`, `DELETE`) must be listed in `access.methods`. Every external domain (including `pos-image` sources) must be listed in `network.allowed_origins`.
10. Read the current record from `data.record` (`data.records` in bulk actions). Call the Open API with `record.system_id` for orders and `record.id` for products, purchases, exports and warehouse transfers.

### Modules and manifest

11. Every module has `export default async function (data)`.
    - Blocks, `modal`/`fullscreen` actions, `render: "worker"` pages: render into `document.body`.
    - `headless` actions: return `{ message?, refresh? }` or throw.
    - `should_render`: return a `boolean`, finish within 300 ms, no slow API calls.
    - `*.before_submit`: return `{ ok: true }` or `{ ok: false, message }`, finish within `timeout_ms`.
12. Every extension in `pos-app.json` has a unique `id`; `target` must exist in [Extension points](/developers/apps/en/targets).
13. Prefer wrapping modules with `defineExtension(target, fn)` from `@pancake-pos/ui-extensions` so the types of `data` and of the return value match the target.

### Quality

14. Wrap every API call in `try/catch`; show errors with `pos-banner` (in UI) or `pos.toast.show(..., { tone: 'error' })`.
15. Handle `403` (the user lacks permission) and `429` (retry later with an increasing delay).
16. Do not call the API on every render; use `useEffect` with explicit dependencies, pagination and `AbortController`.
17. Write the UI in the user's language (`pos.user.locale`).
18. After every change, run `npx eslint .` (or the `check_app` tool) and fix every problem. Do not add `eslint-disable` to silence a rule; `pos-app check` still blocks the deploy.

## Common mistakes

| Wrong | Right |
|---|---|
| `<div className="box">…</div>` | `<pos-section heading="…">…</pos-section>` |
| `<input onChange={(e) => set(e.target.value)} />` | `<pos-text-field onChange={(e) => set(e.detail)} />` |
| `fetch('https://pos.pages.fm/api/v1/shops/123/orders?api_key=...')` | `` pos.api.get(`/shops/${pos.shop.id}/orders`) `` |
| `localStorage.setItem(...)` in an extension | Store it through the Open API (CRM tables) or your own backend |
| `alert('Done')` | `pos.toast.show('Done')` |
| `data.order`, `data.purchase` | `data.record` (`data.resource` tells which resource) |
| `` `/shops/${shopId}/purchases/${record.display_id}` `` | `` `/shops/${shopId}/purchases/${record.id}` `` |
| `import { Table } from 'antd'` in an extension | `<pos-table columns={…} rows={…} />` |
| Calling `PUT` while `access.methods` only has `GET` | Add `"PUT"` to `access.methods` |
| `new Intl.NumberFormat(...).format(total) + ' VND'` | `<pos-money value={total} />` |

## Pre-deploy checklist

- [ ] `pos-app.json` is valid: `handle`, a new `version`, `api_version`, no duplicate `id`.
- [ ] `access.methods` covers every write request; no unnecessary permissions.
- [ ] Every external domain is in `network.allowed_origins`.
- [ ] Modules in `extensions/` use no real DOM, UI libraries or CSS.
- [ ] No tokens, API keys or secrets in code.
- [ ] Errors and loading states are handled.
- [ ] `npx pos-app check` reports no errors.
- [ ] Tested with `npx pos-app dev` (an account with the Settings permission), then deployed to a test shop and tried with a narrow-permission staff account.

## Sample AGENTS.md

````markdown
# POS App

This project is a custom app that runs inside Pancake POS.
Docs: https://pos.pages.fm/developers/apps/llms-full.txt (read before changing code).

## Structure
- pos-app.json: manifest (permissions, external domains, extensions).
- extensions/: modules that run in a Web Worker and render pos-* elements with Preact.
- page/: iframe page (React + Vite), gets `pos` from `connect()` in `@pancake-pos/ui-extensions/page`; routes starting with /_pos/ are reserved for POS.

## Rules
- extensions/: no real DOM, window, localStorage, React, UI libraries or CSS. Only pos-* elements from the docs.
- Text goes inside pos-text/pos-heading/pos-badge/pos-link/pos-button/pos-banner. Event values are in event.detail.
- POS data only through pos.api with paths like /shops/${pos.shop.id}/... and Open API endpoints.
- Never hardcode shop ids, API keys, tokens or secrets.
- Write methods must be in access.methods; external domains must be in network.allowed_origins.
- Modules: export default async function (data). Headless returns { message }; before_submit returns { ok } or { ok: false, message }.
- Catch errors on every API call; handle 403 and 429.

## Commands
- npx pos-app dev: try in POS (turn on Development mode on the custom apps management page); POS reloads when code changes.
- npx eslint .: check the code after every change; do not add eslint-disable to silence rules.
- npx pos-app check: validate the manifest and code; fix every error before deploying.
- npx pos-app deploy: build, validate the manifest, upload a new version.
- With the pos-app MCP server: get_target/list_shop_meta/sample_context before writing code, check_app after every change, get_errors after testing in POS, deploy when done (uploads an inactive version; an admin activates it). For a platform bug or a missing feature: draft a report, show it to the user, and call send_feedback only after they agree.
````
