@bettercms-ai/mcp 0.83.0 → 0.83.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +86 -86
  2. package/SKILL.md +160 -160
  3. package/dist/index.js +35 -7
  4. package/dist/index.js.map +1 -1
  5. package/dist/kit-catalog.json +1509 -1509
  6. package/dist/skills/bettercms/SKILL.md +206 -206
  7. package/dist/skills/bettercms/references/accessibility.md +171 -171
  8. package/dist/skills/bettercms/references/animation.md +296 -296
  9. package/dist/skills/bettercms/references/astro.md +148 -148
  10. package/dist/skills/bettercms/references/bindings.md +366 -366
  11. package/dist/skills/bettercms/references/componentize.md +185 -185
  12. package/dist/skills/bettercms/references/components.md +415 -415
  13. package/dist/skills/bettercms/references/content-experiments.md +62 -62
  14. package/dist/skills/bettercms/references/content-modeling.md +382 -382
  15. package/dist/skills/bettercms/references/detection.md +465 -465
  16. package/dist/skills/bettercms/references/dock-input.md +121 -121
  17. package/dist/skills/bettercms/references/editor-chrome.md +92 -92
  18. package/dist/skills/bettercms/references/external-content.md +78 -78
  19. package/dist/skills/bettercms/references/finish-site.md +377 -377
  20. package/dist/skills/bettercms/references/forms-and-interactions.md +245 -245
  21. package/dist/skills/bettercms/references/frameworks.md +159 -159
  22. package/dist/skills/bettercms/references/getting-started.md +156 -156
  23. package/dist/skills/bettercms/references/integrations.md +364 -364
  24. package/dist/skills/bettercms/references/kit-catalog.md +3259 -3259
  25. package/dist/skills/bettercms/references/kit-scenarios.md +712 -712
  26. package/dist/skills/bettercms/references/localization.md +85 -85
  27. package/dist/skills/bettercms/references/media-and-brand.md +181 -181
  28. package/dist/skills/bettercms/references/next.md +189 -189
  29. package/dist/skills/bettercms/references/other-frameworks.md +85 -85
  30. package/dist/skills/bettercms/references/portable-text.md +213 -213
  31. package/dist/skills/bettercms/references/programmatic-seo.md +98 -98
  32. package/dist/skills/bettercms/references/publishing.md +289 -289
  33. package/dist/skills/bettercms/references/seo.md +357 -357
  34. package/dist/skills/bettercms/references/shopify-headless.md +52 -52
  35. package/dist/skills/bettercms/references/site-blueprints.md +895 -895
  36. package/dist/skills/bettercms/references/site-review.md +74 -74
  37. package/dist/skills/bettercms/references/worked-examples-advanced.md +509 -509
  38. package/dist/skills/bettercms/references/worked-examples.md +885 -885
  39. package/dist/skills/bettercms/references/write-limits.md +217 -217
  40. package/package.json +1 -1
package/README.md CHANGED
@@ -1,86 +1,86 @@
1
- # @bettercms-ai/mcp
2
-
3
- BetterCMS [Model Context Protocol](https://modelcontextprotocol.io) server. Lets an
4
- AI assistant author your content schema and entries through the BetterCMS Management
5
- API, authenticated with a short-lived, per-project `content:manage` key minted via
6
- the OAuth 2.0 Device Authorization Grant (RFC 8628) — no pasted long-lived key.
7
-
8
- ## Tools
9
-
10
- | Tool | Description |
11
- |------|-------------|
12
- | `create_model` | Create a content model (schema) with optional initial fields. |
13
- | `add_field` | Append a field to an existing model (read-modify-write; never removes fields). |
14
- | `create_entry` | Create a content entry; applies `data`/`status` via a follow-up update. |
15
- | `create_form` / `update_form` | Author a form (fields + settings) to embed with `<BcmsForm>`. |
16
- | `create_component` / `update_component` | Author a reusable component (blockJson tree + props) to render with `<BcmsBlocks>`. |
17
- | `list_forms` / `get_form` / `list_components` / `get_component` | Discover forms & components to wire into site code. |
18
-
19
- See [SKILL.md](./SKILL.md) for how an AI agent should author pages, forms, and components.
20
-
21
- All operations are **additive** — there are no destructive tools. Deleting models,
22
- fields, or entries is dashboard-only by design.
23
-
24
- ## Usage
25
-
26
- Add to your MCP client config (e.g. Claude Desktop / Claude Code):
27
-
28
- ```json
29
- {
30
- "mcpServers": {
31
- "bettercms": {
32
- "command": "npx",
33
- "args": ["-y", "@bettercms-ai/mcp"],
34
- "env": { "BETTERCMS_API_URL": "https://api.bettercms.ai" }
35
- }
36
- }
37
- }
38
- ```
39
-
40
- ### Registry auth (current)
41
-
42
- This package is published to **GitHub Packages**, which requires authentication
43
- **even for public scoped packages**. For `npx @bettercms-ai/mcp` to resolve, the
44
- host machine needs an `~/.npmrc` with:
45
-
46
- ```
47
- @bettercms-ai:registry=https://npm.pkg.github.com/
48
- //npm.pkg.github.com/:_authToken=<a GitHub PAT with read:packages>
49
- ```
50
-
51
- > **TODO (future):** publish `@bettercms-ai/mcp` (and `types`/`sdk`) to the public
52
- > **npmjs.org** registry so `npx @bettercms-ai/mcp` works with zero `.npmrc` setup
53
- > for any host/user. Until then the GitHub Packages auth above is required.
54
-
55
- On the first tool call the server prints an authorization prompt to **stderr**
56
- (visible in your MCP client's logs):
57
-
58
- ```
59
- ┌─ BetterCMS authorization required ─────────────────────────
60
- │ Visit: https://bettercms.ai/activate
61
- │ Enter code: WDJB-MJHT
62
- │ Or open: https://bettercms.ai/activate?code=WDJB-MJHT
63
- └────────────────────────────────────────────────────────────
64
- ```
65
-
66
- Approve it in the dashboard (pick a workspace/project) and the server caches the
67
- minted token. It auto-refreshes on expiry; if the refresh token is revoked it
68
- re-runs the device flow.
69
-
70
- ## Environment
71
-
72
- | Var | Default | Purpose |
73
- |-----|---------|---------|
74
- | `BETTERCMS_API_URL` | `https://api.bettercms.ai` | API origin (device-auth + Management API). |
75
- | `BETTERCMS_MCP_CREDENTIALS` | `~/.bettercms/mcp-credentials.json` | Token cache path (0600). |
76
- | `BETTERCMS_MCP_CLIENT_NAME` | `BetterCMS MCP` | Label shown on the approval screen. |
77
- | `BETTERCMS_MCP_TOOLS` | _(unset)_ | Toolsets to register, a comma list: `core` (core tools only), `abm` (adds the ABM campaign lane), `all`. Unset registers every tool except the ABM lane. The hosted server takes the same list as `?tools=`. |
78
-
79
- ## Develop
80
-
81
- ```bash
82
- bun install
83
- bun run --filter @bettercms-ai/mcp typecheck
84
- bun run --filter @bettercms-ai/mcp test
85
- bun run --filter @bettercms-ai/mcp build
86
- ```
1
+ # @bettercms-ai/mcp
2
+
3
+ BetterCMS [Model Context Protocol](https://modelcontextprotocol.io) server. Lets an
4
+ AI assistant author your content schema and entries through the BetterCMS Management
5
+ API, authenticated with a short-lived, per-project `content:manage` key minted via
6
+ the OAuth 2.0 Device Authorization Grant (RFC 8628) — no pasted long-lived key.
7
+
8
+ ## Tools
9
+
10
+ | Tool | Description |
11
+ |------|-------------|
12
+ | `create_model` | Create a content model (schema) with optional initial fields. |
13
+ | `add_field` | Append a field to an existing model (read-modify-write; never removes fields). |
14
+ | `create_entry` | Create a content entry; applies `data`/`status` via a follow-up update. |
15
+ | `create_form` / `update_form` | Author a form (fields + settings) to embed with `<BcmsForm>`. |
16
+ | `create_component` / `update_component` | Author a reusable component (blockJson tree + props) to render with `<BcmsBlocks>`. |
17
+ | `list_forms` / `get_form` / `list_components` / `get_component` | Discover forms & components to wire into site code. |
18
+
19
+ See [SKILL.md](./SKILL.md) for how an AI agent should author pages, forms, and components.
20
+
21
+ All operations are **additive** — there are no destructive tools. Deleting models,
22
+ fields, or entries is dashboard-only by design.
23
+
24
+ ## Usage
25
+
26
+ Add to your MCP client config (e.g. Claude Desktop / Claude Code):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "bettercms": {
32
+ "command": "npx",
33
+ "args": ["-y", "@bettercms-ai/mcp"],
34
+ "env": { "BETTERCMS_API_URL": "https://api.bettercms.ai" }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### Registry auth (current)
41
+
42
+ This package is published to **GitHub Packages**, which requires authentication
43
+ **even for public scoped packages**. For `npx @bettercms-ai/mcp` to resolve, the
44
+ host machine needs an `~/.npmrc` with:
45
+
46
+ ```
47
+ @bettercms-ai:registry=https://npm.pkg.github.com/
48
+ //npm.pkg.github.com/:_authToken=<a GitHub PAT with read:packages>
49
+ ```
50
+
51
+ > **TODO (future):** publish `@bettercms-ai/mcp` (and `types`/`sdk`) to the public
52
+ > **npmjs.org** registry so `npx @bettercms-ai/mcp` works with zero `.npmrc` setup
53
+ > for any host/user. Until then the GitHub Packages auth above is required.
54
+
55
+ On the first tool call the server prints an authorization prompt to **stderr**
56
+ (visible in your MCP client's logs):
57
+
58
+ ```
59
+ ┌─ BetterCMS authorization required ─────────────────────────
60
+ │ Visit: https://bettercms.ai/activate
61
+ │ Enter code: WDJB-MJHT
62
+ │ Or open: https://bettercms.ai/activate?code=WDJB-MJHT
63
+ └────────────────────────────────────────────────────────────
64
+ ```
65
+
66
+ Approve it in the dashboard (pick a workspace/project) and the server caches the
67
+ minted token. It auto-refreshes on expiry; if the refresh token is revoked it
68
+ re-runs the device flow.
69
+
70
+ ## Environment
71
+
72
+ | Var | Default | Purpose |
73
+ |-----|---------|---------|
74
+ | `BETTERCMS_API_URL` | `https://api.bettercms.ai` | API origin (device-auth + Management API). |
75
+ | `BETTERCMS_MCP_CREDENTIALS` | `~/.bettercms/mcp-credentials.json` | Token cache path (0600). |
76
+ | `BETTERCMS_MCP_CLIENT_NAME` | `BetterCMS MCP` | Label shown on the approval screen. |
77
+ | `BETTERCMS_MCP_TOOLS` | _(unset)_ | Toolsets to register, a comma list: `core` (core tools only), `abm` (adds the ABM campaign lane), `all`. Unset registers every tool except the ABM lane. The hosted server takes the same list as `?tools=`. |
78
+
79
+ ## Develop
80
+
81
+ ```bash
82
+ bun install
83
+ bun run --filter @bettercms-ai/mcp typecheck
84
+ bun run --filter @bettercms-ai/mcp test
85
+ bun run --filter @bettercms-ai/mcp build
86
+ ```
package/SKILL.md CHANGED
@@ -1,160 +1,160 @@
1
- # BetterCMS MCP — authoring skill
2
-
3
- How an AI agent should author content, schema, **forms**, and **components** in
4
- BetterCMS through this MCP. The MCP is bound to ONE project (the API key's project);
5
- everything you create lands there.
6
-
7
- The guided prompts ship with the server — prefer them: `studio` (router),
8
- `propose_schema`, `new_page`, `new_form`, `new_component`. This file is the reference
9
- behind them.
10
-
11
- ## Golden rules
12
- - **Confirm before creating.** Propose the shape (fields / block tree) and get the
13
- user's approval (AskUserQuestion) before any create/update. Never silently guess.
14
- - **Read before you edit.** Use the `list_*` / `get_*` tools first so you keep existing
15
- keys/blocks and don't duplicate.
16
- - **Additive where possible.** `add_field` appends (to a model or a page); passing `fields`
17
- to `update_form` or `blockJson` to `update_component` REPLACES — include everything
18
- you want to keep.
19
- - **Drafts are invisible.** A component you create is a DRAFT, and a draft component
20
- renders as an empty string on the live site — no error, no placeholder. Call
21
- `publish_component` on every component you place, then publish the page.
22
-
23
- ## Pages & schema
24
- Decide the architecture before the first call. A marketing site is **components-first**:
25
- a library of `create_component` sections, each with a `sectionType` and a library
26
- category, placed on pages via `create_page`'s `blockJson` as `component` blocks. A blog or
27
- catalogue is **schema-first**: a `create_content_model` collection whose body field is
28
- `document` (the article canvas — pages or collections, one per schema, top level; cannot coexist with a Section zone).
29
-
30
- Within a page's own schema, destructure into a tree: plain fields, **group** (one nested
31
- object), **repeater** (a repeatable array). Never a 1-item repeater, never a repeater of a
32
- single text (that is `array`).
33
-
34
- ### A page is a list of SECTIONS. Never a list of loose blocks.
35
-
36
- The top level of a page's `blockJson` holds only `component` blocks or `section` blocks —
37
- never a bare `heading`/`text`/`image`/`button`/`spacer`.
38
-
39
- **Why it matters, concretely:** the visual editor derives one section per TOP-LEVEL block. A
40
- hero authored as four loose blocks (headline, lede, two CTAs) becomes four separate sections,
41
- each with its own move / duplicate / delete controls and its own "Add section here" gap — and
42
- because each top-level block gets its own block box, the two call-to-action buttons stop being
43
- inline siblings and **stack onto separate lines**. That is not a styling bug to chase in CSS;
44
- it is the page telling you it was authored flat.
45
-
46
- ```jsonc
47
- // ✅ one section, four children — one set of controls, CTAs side by side
48
- { "type": "section", "id": "hero",
49
- "style": { "bg": "surface", "paddingTop": 96, "paddingBottom": 96, "contentWidth": "default" },
50
- "props": { "children": [
51
- { "type": "heading", "id": "hero-title", "props": { "text": "Strategic Planning.", "level": 1 } },
52
- { "type": "text", "id": "hero-lede", "props": { "html": "<p>One place to plan, track and ship.</p>" } },
53
- { "type": "button", "id": "hero-cta", "props": { "text": "Start a project", "href": "/contact" } },
54
- { "type": "button", "id": "hero-cta2", "props": { "text": "See our work", "href": "/work", "variant": "secondary" } }
55
- ] } }
56
- ```
57
-
58
- **Two CTAs are two sibling `button` blocks.** Not a `columns` block — `columns` renders
59
- `grid-template-columns: repeat(N, 1fr)`, so each CTA would stretch to half the container.
60
- Buttons are inline-level and flow side by side on their own. This is what the shipped
61
- `builtin:hero-centered` section does.
62
-
63
- ### `section` block or `component`? Decide by REUSE.
64
-
65
- | | use it when | the catch |
66
- |---|---|---|
67
- | **`component`** (+ `sectionType`) | the band appears on more than one page, or needs layout variants — a Hero that is "Centered" on Home and "Two-column" on About | its children render **without field bindings**, so their text is **not click-to-edit on the canvas**. It is edited through the component's declared `props` in the section dock, where an unset prop shows empty and inherits the definition's default. Authors who want the current copy visible in the dock should set the prop explicitly rather than relying on the inherited default. |
68
- | **`section` block** | a genuinely one-off band on a single page | no family name and no variant switcher, and the editor's "Add a section" picker cannot insert one — only components |
69
-
70
- `component` is the shape the product's reuse machinery is built around: variants, the section
71
- picker and the family name all key off `componentId`. So reach for it whenever a band actually
72
- recurs — and when you do, **declare a prop for every string, link and image a marketer will
73
- ever touch**. A component with un-propped editable copy is the defect, not the component.
74
-
75
- ### Don't hand-write a hero. Start from a built-in section.
76
-
77
- There are built-in section components — `hero-centered`, `hero-split`, `feature-grid-three`,
78
- `feature-grid-two`, `faq-list`, `stats-row`, `logo-cloud-row`, `testimonial-quote`,
79
- `case-study-story`, `cta-banner`, `cta-split`, `pricing-three`, `contact-simple` — available to
80
- every project (`list_components` returns them; they have no `projectId`). Every one is already
81
- rooted in a `section` block with its editable leaves declared as props, and `Hero` ships two
82
- variants, so the shapes above are worked examples you can read.
83
-
84
- Two sanctioned recipes, and which one you want follows the reuse rule above:
85
-
86
- 1. **Inline it** — copy the builtin's `section` blockJson onto the page as a `section` block and
87
- substitute your values. Standard path for a one-off band; children stay click-to-edit.
88
- 2. **Reference it** — place `{ type: "component", props: { componentId, overrides } }`. Use this
89
- when the band recurs across pages or needs the variant switcher.
90
-
91
- Authoring a hero's JSON from scratch is the slow way to reinvent recipe 1 with more mistakes.
92
-
93
- The full decision tree, section anatomy, variant rules and build order live in the
94
- `propose_schema` prompt and in the `bettercms://playbook/schema` resource.
95
-
96
- ## Forms
97
- Tools: `list_forms`, `get_form`, `create_form`, `update_form`.
98
-
99
- 1. Collect the fields with the user. Field: `{ key, label, type, required?, placeholder?,
100
- options?, defaultValue?, showIf? }`. Types: `text, email, textarea, select` (needs
101
- `options`), `checkbox, number, phone, date, url, consent, hidden`. `showIf:
102
- { field, equals }` shows a field conditionally.
103
- 2. Settings: `name` (the handle for `getForm('Name')`), `submitLabel?`, `successMessage?`,
104
- `redirectUrl?`.
105
- 3. Confirm → `create_form { name, fields, ... }` (returns the id) or
106
- `update_form { formId, ... }`.
107
- 4. **Wire it into the site**: in a `@bettercms-ai/next` site,
108
- `import { getForm } from "@bettercms-ai/next"` then render
109
- `<BcmsForm form={await getForm("Name")} />` where the user wants it.
110
-
111
- ## Components (reusable Webflow-style symbols)
112
- Tools: `list_components`, `get_component`, `create_component`, `update_component`.
113
-
114
- 1. Design the `blockJson` tree — an array of blocks `{ type, id, props }`. The full set:
115
- `heading {text, level}`, `text {html}`, `richtext {html}`, `image {src, alt}`,
116
- `button {text, href, variant?, size?, fullWidth?}`, `spacer {height}`, `video {url}`,
117
- `columns {columns: block[][], gap}`, `section {children: block[]}`,
118
- `slider {slides}`, `tabs {tabs}`, `navbar {links, logo?, cta?}`, `footer {columns, copyright?}`,
119
- `form {formId}`, `component {componentId, overrides?}`, `collection {…}`.
120
- `section`, `columns`, `slider` and `tabs` nest child blocks. Every block needs a stable
121
- unique `id`.
122
-
123
- ⚠️ `text` and `richtext` carry **`html`**, never `text`. `props.html` is required with no
124
- default, so `{ text: "…" }` is a **400 on every save** — not a silent strip. This file said
125
- `text {text}` until 2026-08-20; if you learned it from an older copy, unlearn it.
126
- 2. Optional `props` — overridable fields `{ key, label, target: { blockId, path }, type,
127
- defaultValue? }` (type: `text|richtext|image|url|boolean`) so each instance can be
128
- customized.
129
- 3. Confirm the structure → `create_component { name, slug, category?, blockJson, props? }`
130
- (returns the id) or `update_component { componentId, ... }`. **Updating a component
131
- re-bakes every page that embeds it.**
132
- 4. **Render it**: `<BcmsBlocks blocks={component.blockJson} components={...} />` from
133
- `@bettercms-ai/next`.
134
-
135
- Authoring `blockJson` blind is error-prone — keep trees small, confirm with the user,
136
- and read back with `get_component` to verify.
137
-
138
- ## AI content & SEO
139
- Tools: `write_content`, `generate_seo_meta`. Guided prompts: `build_site`,
140
- `generate_landing_pages`, `seo_optimize`. These use the workspace's own Anthropic key
141
- (BYOK, unmetered) or platform AI credits, and return a *suggestion* — you apply it.
142
-
143
- 1. **Write copy** → `write_content { action, text, instructions?, targetLang?, context? }`.
144
- `action`: `write` (draft from a brief), `rewrite` (improve existing copy), `translate`
145
- (needs `targetLang`). Returns the text — apply it with `set_page_content`, `create_entry`,
146
- or `update_entry`.
147
- 2. **SEO meta** → `generate_seo_meta { text, context? }` returns
148
- `{ metaTitle, metaDescription, keywords, jsonLd? }`. Read the page/entry, pass its content
149
- as `text`, then apply the suggestion via the page/entry update tools.
150
- 3. **Programmatic pages** → for a template + a dataset, loop rows: `write_content` per slot →
151
- `create_entry` → `generate_seo_meta`. Confirm the first row or two first. For hundreds of
152
- rows, the dashboard AI Page Builder bulk-imports a CSV.
153
-
154
- Always show suggestions to the user before applying; never invent brand facts.
155
-
156
- ## Errors
157
- - `401/403` → the MCP key needs (re)authorizing (re-run "Connect your AI").
158
- - `409 slug_taken` → offer a different slug.
159
- - `create_form`/`create_component` missing → the host has a stale cached MCP; tell the
160
- user to `rm -rf ~/.npm/_npx` and restart, or that their connector predates this skill.
1
+ # BetterCMS MCP — authoring skill
2
+
3
+ How an AI agent should author content, schema, **forms**, and **components** in
4
+ BetterCMS through this MCP. The MCP is bound to ONE project (the API key's project);
5
+ everything you create lands there.
6
+
7
+ The guided prompts ship with the server — prefer them: `studio` (router),
8
+ `propose_schema`, `new_page`, `new_form`, `new_component`. This file is the reference
9
+ behind them.
10
+
11
+ ## Golden rules
12
+ - **Confirm before creating.** Propose the shape (fields / block tree) and get the
13
+ user's approval (AskUserQuestion) before any create/update. Never silently guess.
14
+ - **Read before you edit.** Use the `list_*` / `get_*` tools first so you keep existing
15
+ keys/blocks and don't duplicate.
16
+ - **Additive where possible.** `add_field` appends (to a model or a page); passing `fields`
17
+ to `update_form` or `blockJson` to `update_component` REPLACES — include everything
18
+ you want to keep.
19
+ - **Drafts are invisible.** A component you create is a DRAFT, and a draft component
20
+ renders as an empty string on the live site — no error, no placeholder. Call
21
+ `publish_component` on every component you place, then publish the page.
22
+
23
+ ## Pages & schema
24
+ Decide the architecture before the first call. A marketing site is **components-first**:
25
+ a library of `create_component` sections, each with a `sectionType` and a library
26
+ category, placed on pages via `create_page`'s `blockJson` as `component` blocks. A blog or
27
+ catalogue is **schema-first**: a `create_content_model` collection whose body field is
28
+ `document` (the article canvas — pages or collections, one per schema, top level; cannot coexist with a Section zone).
29
+
30
+ Within a page's own schema, destructure into a tree: plain fields, **group** (one nested
31
+ object), **repeater** (a repeatable array). Never a 1-item repeater, never a repeater of a
32
+ single text (that is `array`).
33
+
34
+ ### A page is a list of SECTIONS. Never a list of loose blocks.
35
+
36
+ The top level of a page's `blockJson` holds only `component` blocks or `section` blocks —
37
+ never a bare `heading`/`text`/`image`/`button`/`spacer`.
38
+
39
+ **Why it matters, concretely:** the visual editor derives one section per TOP-LEVEL block. A
40
+ hero authored as four loose blocks (headline, lede, two CTAs) becomes four separate sections,
41
+ each with its own move / duplicate / delete controls and its own "Add section here" gap — and
42
+ because each top-level block gets its own block box, the two call-to-action buttons stop being
43
+ inline siblings and **stack onto separate lines**. That is not a styling bug to chase in CSS;
44
+ it is the page telling you it was authored flat.
45
+
46
+ ```jsonc
47
+ // ✅ one section, four children — one set of controls, CTAs side by side
48
+ { "type": "section", "id": "hero",
49
+ "style": { "bg": "surface", "paddingTop": 96, "paddingBottom": 96, "contentWidth": "default" },
50
+ "props": { "children": [
51
+ { "type": "heading", "id": "hero-title", "props": { "text": "Strategic Planning.", "level": 1 } },
52
+ { "type": "text", "id": "hero-lede", "props": { "html": "<p>One place to plan, track and ship.</p>" } },
53
+ { "type": "button", "id": "hero-cta", "props": { "text": "Start a project", "href": "/contact" } },
54
+ { "type": "button", "id": "hero-cta2", "props": { "text": "See our work", "href": "/work", "variant": "secondary" } }
55
+ ] } }
56
+ ```
57
+
58
+ **Two CTAs are two sibling `button` blocks.** Not a `columns` block — `columns` renders
59
+ `grid-template-columns: repeat(N, 1fr)`, so each CTA would stretch to half the container.
60
+ Buttons are inline-level and flow side by side on their own. This is what the shipped
61
+ `builtin:hero-centered` section does.
62
+
63
+ ### `section` block or `component`? Decide by REUSE.
64
+
65
+ | | use it when | the catch |
66
+ |---|---|---|
67
+ | **`component`** (+ `sectionType`) | the band appears on more than one page, or needs layout variants — a Hero that is "Centered" on Home and "Two-column" on About | its children render **without field bindings**, so their text is **not click-to-edit on the canvas**. It is edited through the component's declared `props` in the section dock, where an unset prop shows empty and inherits the definition's default. Authors who want the current copy visible in the dock should set the prop explicitly rather than relying on the inherited default. |
68
+ | **`section` block** | a genuinely one-off band on a single page | no family name and no variant switcher, and the editor's "Add a section" picker cannot insert one — only components |
69
+
70
+ `component` is the shape the product's reuse machinery is built around: variants, the section
71
+ picker and the family name all key off `componentId`. So reach for it whenever a band actually
72
+ recurs — and when you do, **declare a prop for every string, link and image a marketer will
73
+ ever touch**. A component with un-propped editable copy is the defect, not the component.
74
+
75
+ ### Don't hand-write a hero. Start from a built-in section.
76
+
77
+ There are built-in section components — `hero-centered`, `hero-split`, `feature-grid-three`,
78
+ `feature-grid-two`, `faq-list`, `stats-row`, `logo-cloud-row`, `testimonial-quote`,
79
+ `case-study-story`, `cta-banner`, `cta-split`, `pricing-three`, `contact-simple` — available to
80
+ every project (`list_components` returns them; they have no `projectId`). Every one is already
81
+ rooted in a `section` block with its editable leaves declared as props, and `Hero` ships two
82
+ variants, so the shapes above are worked examples you can read.
83
+
84
+ Two sanctioned recipes, and which one you want follows the reuse rule above:
85
+
86
+ 1. **Inline it** — copy the builtin's `section` blockJson onto the page as a `section` block and
87
+ substitute your values. Standard path for a one-off band; children stay click-to-edit.
88
+ 2. **Reference it** — place `{ type: "component", props: { componentId, overrides } }`. Use this
89
+ when the band recurs across pages or needs the variant switcher.
90
+
91
+ Authoring a hero's JSON from scratch is the slow way to reinvent recipe 1 with more mistakes.
92
+
93
+ The full decision tree, section anatomy, variant rules and build order live in the
94
+ `propose_schema` prompt and in the `bettercms://playbook/schema` resource.
95
+
96
+ ## Forms
97
+ Tools: `list_forms`, `get_form`, `create_form`, `update_form`.
98
+
99
+ 1. Collect the fields with the user. Field: `{ key, label, type, required?, placeholder?,
100
+ options?, defaultValue?, showIf? }`. Types: `text, email, textarea, select` (needs
101
+ `options`), `checkbox, number, phone, date, url, consent, hidden`. `showIf:
102
+ { field, equals }` shows a field conditionally.
103
+ 2. Settings: `name` (the handle for `getForm('Name')`), `submitLabel?`, `successMessage?`,
104
+ `redirectUrl?`.
105
+ 3. Confirm → `create_form { name, fields, ... }` (returns the id) or
106
+ `update_form { formId, ... }`.
107
+ 4. **Wire it into the site**: in a `@bettercms-ai/next` site,
108
+ `import { getForm } from "@bettercms-ai/next"` then render
109
+ `<BcmsForm form={await getForm("Name")} />` where the user wants it.
110
+
111
+ ## Components (reusable Webflow-style symbols)
112
+ Tools: `list_components`, `get_component`, `create_component`, `update_component`.
113
+
114
+ 1. Design the `blockJson` tree — an array of blocks `{ type, id, props }`. The full set:
115
+ `heading {text, level}`, `text {html}`, `richtext {html}`, `image {src, alt}`,
116
+ `button {text, href, variant?, size?, fullWidth?}`, `spacer {height}`, `video {url}`,
117
+ `columns {columns: block[][], gap}`, `section {children: block[]}`,
118
+ `slider {slides}`, `tabs {tabs}`, `navbar {links, logo?, cta?}`, `footer {columns, copyright?}`,
119
+ `form {formId}`, `component {componentId, overrides?}`, `collection {…}`.
120
+ `section`, `columns`, `slider` and `tabs` nest child blocks. Every block needs a stable
121
+ unique `id`.
122
+
123
+ ⚠️ `text` and `richtext` carry **`html`**, never `text`. `props.html` is required with no
124
+ default, so `{ text: "…" }` is a **400 on every save** — not a silent strip. This file said
125
+ `text {text}` until 2026-08-20; if you learned it from an older copy, unlearn it.
126
+ 2. Optional `props` — overridable fields `{ key, label, target: { blockId, path }, type,
127
+ defaultValue? }` (type: `text|richtext|image|url|boolean`) so each instance can be
128
+ customized.
129
+ 3. Confirm the structure → `create_component { name, slug, category?, blockJson, props? }`
130
+ (returns the id) or `update_component { componentId, ... }`. **Updating a component
131
+ re-bakes every page that embeds it.**
132
+ 4. **Render it**: `<BcmsBlocks blocks={component.blockJson} components={...} />` from
133
+ `@bettercms-ai/next`.
134
+
135
+ Authoring `blockJson` blind is error-prone — keep trees small, confirm with the user,
136
+ and read back with `get_component` to verify.
137
+
138
+ ## AI content & SEO
139
+ Tools: `write_content`, `generate_seo_meta`. Guided prompts: `build_site`,
140
+ `generate_landing_pages`, `seo_optimize`. These use the workspace's own Anthropic key
141
+ (BYOK, unmetered) or platform AI credits, and return a *suggestion* — you apply it.
142
+
143
+ 1. **Write copy** → `write_content { action, text, instructions?, targetLang?, context? }`.
144
+ `action`: `write` (draft from a brief), `rewrite` (improve existing copy), `translate`
145
+ (needs `targetLang`). Returns the text — apply it with `set_page_content`, `create_entry`,
146
+ or `update_entry`.
147
+ 2. **SEO meta** → `generate_seo_meta { text, context? }` returns
148
+ `{ metaTitle, metaDescription, keywords, jsonLd? }`. Read the page/entry, pass its content
149
+ as `text`, then apply the suggestion via the page/entry update tools.
150
+ 3. **Programmatic pages** → for a template + a dataset, loop rows: `write_content` per slot →
151
+ `create_entry` → `generate_seo_meta`. Confirm the first row or two first. For hundreds of
152
+ rows, the dashboard AI Page Builder bulk-imports a CSV.
153
+
154
+ Always show suggestions to the user before applying; never invent brand facts.
155
+
156
+ ## Errors
157
+ - `401/403` → the MCP key needs (re)authorizing (re-run "Connect your AI").
158
+ - `409 slug_taken` → offer a different slug.
159
+ - `create_form`/`create_component` missing → the host has a stale cached MCP; tell the
160
+ user to `rm -rf ~/.npm/_npx` and restart, or that their connector predates this skill.
package/dist/index.js CHANGED
@@ -2899,6 +2899,9 @@ var LISTING_TAIL = /* @__PURE__ */ new Set([
2899
2899
  "translate_entry",
2900
2900
  "update_field",
2901
2901
  "refresh_search_index",
2902
+ // The owner's questions: a core client answers them through editor_setup `declare` (always loaded).
2903
+ "get_editable_plan",
2904
+ "confirm_editable_plan",
2902
2905
  // Automation, branches and AI images.
2903
2906
  "list_webhooks",
2904
2907
  "create_webhook",
@@ -3003,6 +3006,8 @@ var NEW_TOOL_TEXT = {
3003
3006
  refresh_search_index: "Tell search engines and site search about published changes; costs nothing. action 'indexnow' submits `urls` (this site's own, up to 1000) to IndexNow, or every change since the last submission when omitted. action 'reindex' rebuilds the project's site-search index. 503 means the index is unreachable: try again later.",
3004
3007
  import_redirects: "Bulk-create redirects for a migration, from `rows` or from `text` in `format` (our CSV export, a Netlify `_redirects` file, or a vercel.json). Call it with `dryRun: true` FIRST: nothing is written and each row comes back `would-create`, `existing` or `skipped` with a reason. Rows get create_redirect's rules. Max 5000.",
3005
3008
  editor_setup: "Make the live build editable: derives pages, collections, forms, bindings and components from the live release (skill bindings). Run it after an import or a rebuild, before the conversion brief. Idempotent: `ready` when it already ran for the live sha, else it starts (`running`; poll with `check: true`). `state`: never or stale (run it), running, ready, failed. 409 = nothing is live yet: deploy first. Then get_next_steps.",
3009
+ get_editable_plan: "Questions only the owner answers (logo, header, seeds, kit sections, brand, blocks): `rows` with closed `options` and `recommended`. Ask; never guess.",
3010
+ confirm_editable_plan: "Record answers: each `decisions` item is an option's `decision`, unchanged. `check: true` = dry run.",
3006
3011
  transform_image: "Edit an existing Media Library image with an instruction (e.g. 'remove the background', 'make it dusk'); the result is saved as a NEW asset and the original is kept (5 AI credits). Returns the new asset (id, url, altText).",
3007
3012
  write_alt_text: "Alt text for a Media Library image (mediaId) or an https image URL (url), written from its pixels (1 AI credit). A suggestion: save it with update_media.",
3008
3013
  list_integrations: "List the project's custom integrations (any vendor with a public API) and the workspace's integration `templates`. Each has its hosts, secret NAMES with `secretsPresent` (never a value), syncs, hooks, endpoints, functions and `approved`. Read bettercms://skill/integrations first.",
@@ -3710,10 +3715,15 @@ var HOSTING_TEXT = {
3710
3715
  deploy_to_host: "Deploy this project to the user's own Vercel or Netlify account through their connection. BetterCMS builds the site and the host serves that build (skill publishing). The first call creates the site on the host and makes it the project's host; a later call for the same provider redeploys that site (`redeployed: true`). `connectionId` (from list_hosting_connections) defaults to the newest working one, `siteName` to the project's handle. A refusal carries a `code` and a `fixUrl` (not_connected, reconnect, pick_existing_site, grant_required, ADMIN_REQUIRED): each is fixed by the user at `fixUrl`, so relay it and stop. `deploymentQueued: false` means the project has no live build yet: deploy_project first, and that release reaches the host on its own. Never run the Vercel or Netlify CLI while a connection exists. A headless project is not deployed here: its own front end deploys with `npx @bettercms-ai/cli@latest deploy --to <host>`. Then poll get_host_deploy_status. Needs artifact:write, like deploy_project.",
3711
3716
  get_host_deploy_status: "The connected project's recent deployments to Vercel, Netlify or Cloudflare, newest first: state (queued|deploying|live|failed), providerUrl, error, sha and targetName. Poll it after deploy_to_host until the newest one is `live`, whose `providerUrl` is the live URL to hand the user, or `failed`, whose `error` you read back to them."
3712
3717
  };
3713
- async function builtinCopyId(client, builtinId) {
3718
+ async function builtinCopyId(client, builtinId, verdict) {
3714
3719
  const listed = await client.fetchJSON(client.url(`/management/components?select=summary`));
3715
3720
  const copy = (listed.data ?? []).find((row) => row.sourceKey === builtinId && !row.builtin);
3716
3721
  if (copy) return copy.id;
3722
+ try {
3723
+ await verdict();
3724
+ } catch (err) {
3725
+ if (apiError(err)?.bodyCode !== "BUILTIN_NOT_COPIED") throw err;
3726
+ }
3717
3727
  const made = await client.fetchJSON(client.url(`/management/components`), { method: "POST", body: JSON.stringify({ from: builtinId }) });
3718
3728
  return made.data.id;
3719
3729
  }
@@ -4962,13 +4972,16 @@ ${d.outline}` : "Proposed Content structure.", d);
4962
4972
  async (c, a) => {
4963
4973
  const edit = Array.isArray(a.blockJson) ? void 0 : a.blockJson;
4964
4974
  const insert = edit?.insertSection;
4965
- const insertSection = insert?.componentId.startsWith("builtin:") ? { ...insert, componentId: await builtinCopyId(c, insert.componentId) } : insert;
4975
+ const pageBody = (sectionInsert) => ({ title: a.title, slug: a.slug, blockJson: Array.isArray(a.blockJson) ? a.blockJson : void 0, insertSection: sectionInsert, sectionStyle: edit?.sectionStyle, metaTitle: a.metaTitle, metaDescription: a.metaDescription, schemaType: a.schemaType, schema: a.schema, status: a.status, noindex: a.noindex, canonical: a.canonical, og: a.og, twitter: a.twitter, scheduledAt: a.scheduledAt, unpublishAt: a.unpublishAt, locale: a.locale, translationOf: a.translationOf, ...a.dynamic });
4976
+ const ifMatchHeaders = a.ifMatch !== void 0 ? { headers: { "If-Match": `W/"${a.ifMatch}"` } } : {};
4977
+ const verdict = () => c.fetchJSON(c.url(`/management/pages/${s(a.pageId)}/meta`), { method: "PATCH", ...ifMatchHeaders, body: JSON.stringify({ ...pageBody(insert), dryRun: true }) });
4978
+ const insertSection = insert?.componentId.startsWith("builtin:") ? { ...insert, componentId: await builtinCopyId(c, insert.componentId, verdict) } : insert;
4966
4979
  const res = await c.fetchJSON(
4967
4980
  c.url(`/management/pages/${s(a.pageId)}/meta`),
4968
4981
  {
4969
4982
  method: "PATCH",
4970
- ...a.ifMatch !== void 0 ? { headers: { "If-Match": `W/"${a.ifMatch}"` } } : {},
4971
- body: JSON.stringify({ title: a.title, slug: a.slug, blockJson: Array.isArray(a.blockJson) ? a.blockJson : void 0, insertSection, sectionStyle: edit?.sectionStyle, metaTitle: a.metaTitle, metaDescription: a.metaDescription, schemaType: a.schemaType, schema: a.schema, status: a.status, noindex: a.noindex, canonical: a.canonical, og: a.og, twitter: a.twitter, scheduledAt: a.scheduledAt, unpublishAt: a.unpublishAt, locale: a.locale, translationOf: a.translationOf, ...a.dynamic })
4983
+ ...ifMatchHeaders,
4984
+ body: JSON.stringify(pageBody(insertSection))
4972
4985
  }
4973
4986
  );
4974
4987
  const note = res.redirectNote;
@@ -5512,7 +5525,7 @@ ${d.outline}` : "Proposed Content structure.", d);
5512
5525
  def(
5513
5526
  "get_binding_report",
5514
5527
  "Check what on the live site is editable",
5515
- "The receipt for 'is this site actually EDITABLE?'. Each release scans the built HTML for the element rendering each CMS field value; per slot this returns `mode` ('text-match' = guessed from rendered text, 'declared' = template-declared), what was bound, and `unmatched`: each path with its reason (reasons: skill bindings). DEPLOY FIRST: before any release there is no report (pages 0, mode null, refreshRequired true). `refreshReason`: a HEADLESS site never gets one (`never-generated`); `layoutRoutes` and `coverage.error`: skill bindings. `unaddressable` is the visible live text no field owns, by route (null = unmeasured: diff visible text against entry values); over `count / visible > 0.02`, fix it in the SOURCE and re-read (playbook \xA711). `entryRowUnbound`: a bound card's words no field of its entry claims, by route, with `reason` (reference, boolean, locale-format, counter, no-field); never also in `unaddressable`. `canvas.lane`: `none` \u2192 get_next_steps.",
5528
+ "The receipt for 'is this site actually EDITABLE?'. Each release scans the built HTML for the element rendering each CMS field value; per slot this returns `mode` ('text-match' = guessed from rendered text, 'declared' = template-declared), what was bound, and `unmatched`: each path with its reason (reasons: skill bindings). DEPLOY FIRST: before any release there is no report (pages 0, mode null, refreshRequired true). `refreshReason`: a HEADLESS site never gets one (`never-generated`); `layoutRoutes` and `coverage.error`: skill bindings. `unaddressable` is the visible live text no field owns, by route (null = unmeasured: diff visible text against entry values); over `count / visible > 0.02`, fix it in the SOURCE and re-read (playbook \xA711). `entryRowUnbound`: a bound card's words no field of its entry claims, by route, with `reason` (reference, boolean, locale-format, counter, no-field); never also in `unaddressable`. `canvas.lane`: `none` \u2192 get_next_steps. `sectionsHeld` likewise; `unstampedIds`: no stamp.",
5516
5529
  z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read \u2014 everything here, `unaddressable` included, is measured PER SLOT, so on a promote-gated project pass 'current' to read the tree the editor frames. Defaults to the slot this project's releases land in. The response's `sha` is the build the report describes; `refreshRequired` is true when that is not the build this slot serves. Other per-slot fields: skill bindings.") }).shape,
5517
5530
  async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
5518
5531
  ),
@@ -5783,7 +5796,7 @@ ${d.outline}` : "Proposed Content structure.", d);
5783
5796
  def(
5784
5797
  "get_deploy_status",
5785
5798
  "Get deploy/build status",
5786
- "Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release`, `held` and `publishes` (each `live` or `partial`): skill publishing. `runtime` ({state: ok|crash-loop, restarts, oomKilled}) appears for a server app only: crash-loop means its container keeps restarting (often out of memory) though the build went live fine. Also returns `canvas.lane` (skill bindings) and `editorUrl`, the visual editor to hand the user.",
5799
+ "Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release`, `held` and `publishes` (each `live` or `partial`): skill publishing. `runtime` ({state: ok|crash-loop, restarts, oomKilled}) appears for a server app only: crash-loop means its container keeps restarting (often out of memory) though the build went live fine. Also returns `canvas.lane` (skill bindings) and `editorUrl`, the visual editor to hand the user. `heldSections`: sections held off the page.",
5787
5800
  z.object({ waitSeconds: z.number().optional().describe(PARAM_TEXT.waitSeconds) }).shape,
5788
5801
  // Flat body, no `data` envelope: `data()` read it as undefined.
5789
5802
  async (c, a) => ok("Deploy status.", await c.fetchJSON(c.url(withWait(`/management/projects/deploy-status`, a.waitSeconds)), { method: "GET" }))
@@ -5811,6 +5824,21 @@ ${d.outline}` : "Proposed Content structure.", d);
5811
5824
  // `declare` records binding decisions; with `check: true` it is a dry run (the body's `dryRun`).
5812
5825
  async (c, a) => a.exempt ? ok("Setup gate exceptions.", await c.fetchJSON(c.url(`/management/projects/setup-gate/exceptions`), { method: "POST", body: JSON.stringify({ exempt: a.exempt }) })) : a.declare ? ok("Binding decisions.", await c.fetchJSON(c.url(`/management/projects/binding-decisions`), { method: "POST", body: JSON.stringify({ declare: a.declare, ...a.check === true ? { dryRun: true } : {} }) })) : ok("Editor setup.", await c.fetchJSON(c.url(a.check === true ? withWait(`/management/projects/editor-setup`, a.waitSeconds) : `/management/projects/editor-setup`), a.check === true ? { method: "GET" } : { method: "POST", body: JSON.stringify({ force: a.force }) }))
5813
5826
  ),
5827
+ // Ask, don't guess: the plan, and its answers through editor_setup's declare route.
5828
+ def(
5829
+ "get_editable_plan",
5830
+ "Get the owner's open questions",
5831
+ NEW_TOOL_TEXT.get_editable_plan,
5832
+ z.object({}).shape,
5833
+ async (c) => ok("Editable plan.", await c.fetchJSON(c.url(`/management/projects/editable-plan`), { method: "GET" }))
5834
+ ),
5835
+ def(
5836
+ "confirm_editable_plan",
5837
+ "Record the owner's answers",
5838
+ NEW_TOOL_TEXT.confirm_editable_plan,
5839
+ z.object({ decisions: z.array(z.record(z.string(), z.unknown())), check: z.boolean().optional() }).shape,
5840
+ async (c, a) => ok("Binding decisions.", await c.fetchJSON(c.url(`/management/projects/binding-decisions`), { method: "POST", body: JSON.stringify({ declare: a.decisions, ...a.check === true ? { dryRun: true } : {} }) }))
5841
+ ),
5814
5842
  // ── Deploy to the user's own host (parity with remote /mcp) ──
5815
5843
  // A person connects Vercel or Netlify in the dashboard; nothing here touches OAuth or a host
5816
5844
  // token. deploy_to_host takes artifact:write like deploy_project. @see management/deploy-targets.ts
@@ -6339,7 +6367,7 @@ ${notes.join("\n")}` : summary, created);
6339
6367
  name: "publish_layout",
6340
6368
  config: {
6341
6369
  title: "Publish the Global Layout draft",
6342
- description: "Publish the connected project's GLOBAL Layout draft (navigation, footer, every reserved section). On a site BetterCMS serves, a Layout publish never builds: it writes the VALUES into the live pages, and a STRUCTURE change (Sections added, removed, moved) waits for a Rebuild or a push (`layout-rebuild-required`); a custom host gets its deploy hook. Until published, get_layout copy:'published' answers PUBLISHED_LAYOUT_UNAVAILABLE. Pass get_layout's `revision` as ifMatch; a stale one returns 409 \u2014 re-read, never retry blindly. A 422 lists validation issues (global, or per page override: pageSlug, copy) to fix with update_layout. A new project's chrome is draft: publish_component navigation-default and footer-default first (the order and the optional logo: skill components). Verify with get_layout copy:'published' \u2014 this response is the write's echo, not a receipt. Page Layout overrides go live with the page (update_page status:'published'). 403 PUBLISH_NOT_GRANTED: this connection may author drafts, not publish \u2014 say so; the user allows it or publishes from the dashboard.",
6370
+ description: "Publish the connected project's GLOBAL Layout draft (navigation, footer, every reserved section). On a site BetterCMS serves, a Layout publish never builds: it writes the VALUES into the live pages, and a STRUCTURE change (Sections added, removed, moved) waits for a Rebuild or a push (`layout-rebuild-required`); a custom host gets its deploy hook. Until published, get_layout copy:'published' answers PUBLISHED_LAYOUT_UNAVAILABLE. Pass get_layout's `revision` as ifMatch; a stale one returns 409 \u2014 re-read, never retry blindly. A 422 lists validation issues (global, or per page override: pageSlug, copy) to fix with update_layout. The platform's Navigation/Footer Defaults publish themselves \u2014 never publish_component them first; the optional logo: skill components. Verify with get_layout copy:'published' \u2014 this response is the write's echo, not a receipt. Page Layout overrides go live with the page (update_page status:'published'). 403 PUBLISH_NOT_GRANTED: this connection may author drafts, not publish \u2014 say so; the user allows it or publishes from the dashboard.",
6343
6371
  inputSchema: publishLayoutInput.shape
6344
6372
  },
6345
6373
  handler: guard(async (args) => withClient(async (client) => {