@bettercms-ai/mcp 0.20.0 → 0.21.0

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.
package/SKILL.md CHANGED
@@ -31,6 +31,65 @@ Within a page's own schema, destructure into a tree: plain fields, **group** (on
31
31
  object), **repeater** (a repeatable array). Never a 1-item repeater, never a repeater of a
32
32
  single text (that is `array`).
33
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
+
34
93
  The full decision tree, section anatomy, variant rules and build order live in the
35
94
  `propose_schema` prompt and in the `bettercms://playbook/schema` resource.
36
95
 
@@ -52,10 +111,18 @@ Tools: `list_forms`, `get_form`, `create_form`, `update_form`.
52
111
  ## Components (reusable Webflow-style symbols)
53
112
  Tools: `list_components`, `get_component`, `create_component`, `update_component`.
54
113
 
55
- 1. Design the `blockJson` tree — an array of blocks `{ type, id, props }`. Types:
56
- `heading {text, level}`, `text {text}`, `image {src, alt}`, `button {text, href}`,
57
- `spacer {height}`, `video {url}`, and `columns {columns: block[][], gap}` which nests
58
- child blocks. Every block needs a stable unique `id`.
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?}`, `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.
59
126
  2. Optional `props` — overridable fields `{ key, label, target: { blockId, path }, type,
60
127
  defaultValue? }` (type: `text|richtext|image|url|boolean`) so each instance can be
61
128
  customized.
package/dist/index.js CHANGED
@@ -314,7 +314,9 @@ import { BetterCMS } from "@bettercms-ai/sdk";
314
314
 
315
315
  // src/tools.ts
316
316
  import { z } from "zod";
317
- import { BetterCMSError } from "@bettercms-ai/sdk";
317
+
318
+ // ../types/src/component.ts
319
+ var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: a component's children render WITHOUT field bindings, so their text is NOT click-to-edit on the canvas \u2014 it is edited through the component's declared `props` in the section dock. A `section` block's children stay click-to-edit. So when you choose a component, DECLARE A PROP for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section component (list_components returns them with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and nine more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; REFERENCE it as a `component` when the band recurs or needs variants. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
318
320
 
319
321
  // ../types/src/layout-lucide-icons.ts
320
322
  var LAYOUT_SECTION_ICONS = Object.freeze([
@@ -2279,6 +2281,7 @@ var LAYOUT_SECTION_ICONS = Object.freeze([
2279
2281
  var LAYOUT_SECTION_ICON_SET = new Set(LAYOUT_SECTION_ICONS);
2280
2282
 
2281
2283
  // src/tools.ts
2284
+ import { BetterCMSError } from "@bettercms-ai/sdk";
2282
2285
  var FRAMEWORK_CHOICES = ["astro", "next", "react-ts", "other"];
2283
2286
  var FRAMEWORK_LABELS = {
2284
2287
  astro: "Astro \u2014 recommended default, static by default and fastest to publish",
@@ -2319,6 +2322,46 @@ async function askFramework(deps) {
2319
2322
  }
2320
2323
  return { prompt: FRAMEWORK_PROMPT };
2321
2324
  }
2325
+ var AUTHORING_CHOICES = ["components", "fields"];
2326
+ var AUTHORING_LABELS = {
2327
+ components: "Components \u2014 reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema. Best for marketing and landing sites",
2328
+ fields: "Fields \u2014 a typed field schema per page. Best for blogs, catalogues and directories, where many rows share one shape"
2329
+ };
2330
+ var AUTHORING_PROMPT = [
2331
+ "Ask the user which authoring architecture this site should use, then call set_authoring_preference again with their answer as `preference`:",
2332
+ ...AUTHORING_CHOICES.map((c, i) => ` ${i + 1}. ${c} \u2014 ${AUTHORING_LABELS[c]}`),
2333
+ "",
2334
+ "Answering 'components' does not convert anything \u2014 there is no field-to-block converter. It means you author the sections yourself: create_component, then publish_component (an unpublished component renders as NOTHING on the live site), then set_page_content placing `component` blocks. Once a page has blocks, list_extraction_candidates and extract_component fold the repeats.",
2335
+ "",
2336
+ "Do not choose on their behalf. This is asked once per project."
2337
+ ].join("\n");
2338
+ async function askAuthoring(deps) {
2339
+ if (!deps.elicit) return { prompt: AUTHORING_PROMPT };
2340
+ try {
2341
+ const res = await deps.elicit({
2342
+ message: "Which authoring architecture should this site use?",
2343
+ requestedSchema: {
2344
+ type: "object",
2345
+ properties: {
2346
+ preference: {
2347
+ type: "string",
2348
+ title: "Authoring architecture",
2349
+ description: "How this site's pages are composed. Asked once per project.",
2350
+ enum: [...AUTHORING_CHOICES],
2351
+ enumNames: AUTHORING_CHOICES.map((c) => AUTHORING_LABELS[c])
2352
+ }
2353
+ },
2354
+ required: ["preference"]
2355
+ }
2356
+ });
2357
+ const picked = res.action === "accept" ? res.content?.preference : void 0;
2358
+ if (typeof picked === "string" && AUTHORING_CHOICES.includes(picked)) {
2359
+ return { preference: picked };
2360
+ }
2361
+ } catch {
2362
+ }
2363
+ return { prompt: AUTHORING_PROMPT };
2364
+ }
2322
2365
  var fieldType = z.enum([
2323
2366
  "text",
2324
2367
  "richtext",
@@ -2619,7 +2662,14 @@ function buildToolDefs(deps) {
2619
2662
  options: z.array(z.string()).optional().describe("choices when type is 'select', 'radio' or 'checkboxes'"),
2620
2663
  hidden: z.boolean().optional().describe("not rendered; pairs with defaultValue to capture context"),
2621
2664
  defaultValue: z.string().optional(),
2622
- showIf: z.object({ field: z.string(), equals: z.string() }).optional().describe("show this field only when another field equals a value")
2665
+ showIf: z.object({ field: z.string(), equals: z.string() }).optional().describe("show this field only when another field equals a value"),
2666
+ validation: z.object({
2667
+ emailPolicy: z.enum(["any", "business"]).optional().describe("'email' fields only"),
2668
+ min: z.number().optional().describe("'number' fields only \u2014 inclusive floor"),
2669
+ max: z.number().optional().describe("'number' fields only \u2014 inclusive ceiling"),
2670
+ phoneFormat: z.enum(["any", "e164"]).optional().describe("'phone' fields only"),
2671
+ pattern: z.string().optional().describe("'text' / 'textarea' / 'url' fields only \u2014 a regex")
2672
+ }).optional().describe("per-field rules the API enforces on submit; each key is only valid on the field types listed")
2623
2673
  });
2624
2674
  const formSettingsShape = {
2625
2675
  description: z.string().optional(),
@@ -2642,11 +2692,26 @@ function buildToolDefs(deps) {
2642
2692
  key: z.string().min(1),
2643
2693
  label: z.string().min(1),
2644
2694
  target: z.object({ blockId: z.string().min(1), path: z.string().min(1) }),
2645
- type: z.enum(["text", "richtext", "image", "url", "boolean", "slot"]),
2695
+ // MUST stay at parity with componentPropDefSchema on the server. `update_component`
2696
+ // REPLACES the whole `props` array, so a type this enum omits cannot be echoed back: an
2697
+ // agent that reads a component and writes it back DESTROYS every prop of that type.
2698
+ type: z.enum(["text", "richtext", "image", "url", "boolean", "number", "select", "group", "table", "slot"]),
2646
2699
  // 'slot' holds ONE nested component instance; config.componentIds restricts what may
2647
2700
  // fill it. Absent here until now, so a slot allowlist was unreachable from stdio even
2648
2701
  // once the enum allowed the type.
2649
- config: z.object({ componentIds: z.array(z.string()).optional() }).optional(),
2702
+ config: z.object({
2703
+ componentIds: z.array(z.string()).optional(),
2704
+ // 'slot' allowlist
2705
+ options: z.array(z.string()).optional(),
2706
+ // 'select' — required, choices
2707
+ // 'group'/'table' sub-shape. Typed loosely here (the server validates the full
2708
+ // recursive shape) so a 3-level tree does not need a 3-level Zod mirror in the client.
2709
+ fields: z.array(z.record(z.string(), z.unknown())).optional(),
2710
+ min: z.number().optional(),
2711
+ // 'number'
2712
+ max: z.number().optional(),
2713
+ step: z.number().optional()
2714
+ }).passthrough().optional(),
2650
2715
  defaultValue: z.unknown().optional()
2651
2716
  });
2652
2717
  const getLayoutInput = z.object({
@@ -2718,7 +2783,9 @@ function buildToolDefs(deps) {
2718
2783
  componentId: z.string().min(1).describe("component id (from list_components)"),
2719
2784
  name: z.string().optional(),
2720
2785
  category: componentCategory.optional(),
2721
- sectionType: sectionType.nullable().optional().describe("null demotes it to an ordinary component"),
2786
+ sectionType: sectionType.nullable().optional().describe(
2787
+ "null demotes it to an ordinary component \u2014 it disappears from the 'Add a section' picker, and instances already placed keep rendering but lose their section chrome and variant switcher"
2788
+ ),
2722
2789
  allowedOn: allowedOn.optional().describe("REPLACES the placement allowlist"),
2723
2790
  description: z.string().optional(),
2724
2791
  blockJson: z.array(blockObject).optional().describe("REPLACES the block tree"),
@@ -2750,7 +2817,7 @@ function buildToolDefs(deps) {
2750
2817
  };
2751
2818
  const data = async (client, method, path, body) => (await client.fetchJSON(client.url(path), {
2752
2819
  method,
2753
- ...body !== void 0 ? { body: JSON.stringify(body), headers: { "content-type": "application/json" } } : {}
2820
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
2754
2821
  })).data;
2755
2822
  const s = (v) => v;
2756
2823
  const raw = async (client, path, base64, mimeType) => (await client.fetchJSON(client.url(path), {
@@ -2977,6 +3044,25 @@ function buildToolDefs(deps) {
2977
3044
  return ok("Created project.", await data(c, "POST", `/management/projects`, { ...a, framework }));
2978
3045
  }
2979
3046
  ),
3047
+ def(
3048
+ "set_authoring_preference",
3049
+ "Set the site's authoring architecture",
3050
+ "Record which authoring architecture this site uses \u2014 'components' (reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema \u2014 best for marketing and landing sites) or 'fields' (a typed field schema per page \u2014 best for blogs, catalogues and directories). ASK THE USER; do not pick for them. Called without `preference`, this tool asks them directly (or hands you the question to ask). deploy_project, deploy_from_upload and promote_project all refuse with 409 AUTHORING_DECISION_REQUIRED until it is set, and that refusal carries this project's real page counts to show the user. Answering 'components' does NOT convert anything \u2014 there is no field-to-block converter; it means you author the sections yourself: create_component, then publish_component (an unpublished component renders as NOTHING on the live site), then set_page_content placing `component` blocks. Asked once per project; re-callable if the user changes their mind.",
3051
+ // Optional in the schema for exactly the reason `framework` is above: a required arg is
3052
+ // rejected by the SDK before the handler runs, which would kill the elicitation below
3053
+ // and leave the model guessing. Optional here, answered by a human there. The backend
3054
+ // gate refuses the deploy regardless, so nothing ships on a guess.
3055
+ z.object({ preference: z.enum(AUTHORING_CHOICES).optional().describe("REQUIRED in effect \u2014 the architecture the USER chose. Ask them; never default it.") }).shape,
3056
+ async (c, a) => {
3057
+ let preference = a.preference;
3058
+ if (preference === void 0) {
3059
+ const asked = await askAuthoring(deps);
3060
+ if ("prompt" in asked) return fail(asked.prompt);
3061
+ preference = asked.preference;
3062
+ }
3063
+ return ok("Recorded the authoring architecture.", await data(c, "PATCH", `/management/projects/current/authoring-preference`, { preference }));
3064
+ }
3065
+ ),
2980
3066
  def(
2981
3067
  "clone_project",
2982
3068
  "Clone a project",
@@ -3031,7 +3117,7 @@ function buildToolDefs(deps) {
3031
3117
  def(
3032
3118
  "update_page",
3033
3119
  "Edit a page",
3034
- "Edit a page: title, slug, SEO metaTitle/metaDescription, publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first). It does NOT change the field SCHEMA \u2014 use add_page_field / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call.",
3120
+ "Edit a page: title, slug, SEO metaTitle/metaDescription, publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first). It does NOT change the field SCHEMA \u2014 use add_page_field / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call. " + SECTION_DOCTRINE,
3035
3121
  z.object({ pageId: z.string().min(1), title: z.string().optional(), slug: z.string().optional(), blockJson: z.array(blockObject).optional().describe("REPLACES the page's block composition"), metaTitle: z.string().optional(), metaDescription: z.string().optional(), status: z.enum(["draft", "published"]).optional() }).shape,
3036
3122
  async (c, a) => ok("Updated page.", await data(c, "PATCH", `/management/pages/${s(a.pageId)}/meta`, { title: a.title, slug: a.slug, blockJson: a.blockJson, metaTitle: a.metaTitle, metaDescription: a.metaDescription, status: a.status }))
3037
3123
  ),
@@ -3210,7 +3296,7 @@ function buildToolDefs(deps) {
3210
3296
  name: "create_page",
3211
3297
  config: {
3212
3298
  title: "Create a page",
3213
- description: "Create a page with its own typed schema. Supports pageType 'singleton' (exactly one entry \u2014 Home, About, Contact) and 'dynamic' (many entries sharing the schema \u2014 Blog posts, Products). Project-scoped from the key. Additive \u2014 does not delete or overwrite existing pages. FIRST read the page's real markup and DECOMPOSE it into a destructured tree: each visual section becomes a nested field \u2014 a fixed grouped block \u2192 type 'group', a repeating list of items (cards, testimonials, features, FAQs) \u2192 type 'repeater' \u2014 each carrying its own child `fields`. Do NOT flatten sections into many flat top-level fields. Build the full nested tree, then call this once.",
3299
+ description: "Create a page with its own typed schema. Supports pageType 'singleton' (exactly one entry \u2014 Home, About, Contact) and 'dynamic' (many entries sharing the schema \u2014 Blog posts, Products). Project-scoped from the key. Additive \u2014 does not delete or overwrite existing pages. FIRST read the page's real markup and DECOMPOSE it into a destructured tree: each visual section becomes a nested field \u2014 a fixed grouped block \u2192 type 'group', a repeating list of items (cards, testimonials, features, FAQs) \u2192 type 'repeater' \u2014 each carrying its own child `fields`. Do NOT flatten sections into many flat top-level fields. Build the full nested tree, then call this once. That tree is the page's SCHEMA \u2014 what it holds. " + SECTION_DOCTRINE,
3214
3300
  inputSchema: createPageInput.shape
3215
3301
  },
3216
3302
  handler: guard(
@@ -3567,7 +3653,7 @@ function buildToolDefs(deps) {
3567
3653
  name: "create_component",
3568
3654
  config: {
3569
3655
  title: "Create a reusable component",
3570
- description: "Create a reusable component from a blockJson tree. CONFIRM the structure with the user first. blockJson is an array of blocks (heading/text/image/button/spacer/video/columns); a 'columns' block nests child blocks in props.columns. `props` declares overridable fields. Returns the new id \u2014 render with `<BcmsBlocks>`. Always lands as a DRAFT: it is not on the live site until someone publishes it from the dashboard.",
3656
+ description: "Create a reusable component from a blockJson tree. THIS IS ALSO HOW A PAGE GETS ITS SECTIONS: set `sectionType` (e.g. 'Hero') and the component becomes a placeable section, selectable in the editor's 'Add a section' picker. Components sharing a `sectionType` are its layout VARIANTS \u2014 one Hero with a 'Centered' and a 'Two-column' variant, same prop keys, so a swap keeps the content. CONFIRM the structure with the user first. blockJson is an array of blocks \u2014 the same set create_page accepts (heading, text/richtext, image, button, spacer, video, columns, section, slider, tabs, navbar, footer, form, component, collection), NOT a narrower one; `section` nests child blocks in props.children and `columns` in props.columns. `props` declares overridable fields. Returns the new id \u2014 render with `<BcmsBlocks>`. Always lands as a DRAFT: it is not on the live site until someone publishes it from the dashboard. " + SECTION_DOCTRINE,
3571
3657
  inputSchema: createComponentInput.shape
3572
3658
  },
3573
3659
  handler: guard(
@@ -3750,8 +3836,13 @@ Then declare what an editor may change, via \`props\`:
3750
3836
  { key: "ctaHref", label: "CTA link", target: { blockId: "cta", path: "props.href" }, type: "url" }
3751
3837
  ]
3752
3838
 
3753
- Declare props only for what should really be editable. Every prop is a decision an editor
3754
- has to make.
3839
+ \u{1F534} **On a components-first page, \`props\` is the ONLY editing surface.** Click-to-edit binds
3840
+ \`heading\`/\`text\`/\`button\`/\`image\` blocks; it does **not** bind a \`component\` block, because
3841
+ a component's blocks belong to the shared definition, not to the instance. So a page built
3842
+ from \`component\` blocks renders correctly and emits ZERO fields on the canvas \u2014 editing goes
3843
+ through the declared props allowlist to per-instance overrides. **A component with no props
3844
+ is a section nobody can change.** Declare a prop for every string, link and image a marketer
3845
+ would ever reasonably want to touch; leave out only structure and styling.
3755
3846
 
3756
3847
  ## 4. Variants
3757
3848
 
@@ -3814,9 +3905,57 @@ nothing, on a page that returns 200. If a section is missing from the live site,
3814
3905
 
3815
3906
  **A workspace-level component (\`projectId: null\`) never reaches a live site.** It resolves
3816
3907
  in preview and is blank in production. Always pass the project's id.
3908
+
3909
+ ## 9. Check which project you are bound to, BEFORE you build
3910
+
3911
+ \`create_project\` succeeds on a project-scoped connection and hands back a real new project
3912
+ \u2014 but every write that follows still lands in the project your grant is bound to.
3913
+ \`projectId\` is forwarded as a header for a workspace-scoped grant; a **project-scoped grant
3914
+ accepts it and ignores it**, silently. No error, no warning, wrong project.
3915
+
3916
+ So call \`get_project\` (no arguments) first \u2014 it reports the project you are actually
3917
+ writing to. If that is not where the work belongs, stop and tell the user: only they can
3918
+ approve a grant for the other project, no tool can switch it.
3919
+
3920
+ If you must probe, probe with a \`create_content_model\` \u2014 models are deletable
3921
+ (\`delete_content_model\`, soft-delete) and **there is no \`delete_component\`**. A component
3922
+ written to the wrong project can only be demoted (\`sectionType: null\`) and left unpublished.
3923
+
3924
+ ## 10. You imported a site, or you are about to deploy one
3925
+
3926
+ \`deploy_project\`, \`deploy_from_upload\` and \`promote_project\` all answer **409
3927
+ AUTHORING_DECISION_REQUIRED** until a human has chosen this project's architecture. This is
3928
+ asked ONCE per project, ever. It is not an error to retry or route around: read the message
3929
+ out, let the user pick, call \`set_authoring_preference\`, then deploy again.
3930
+
3931
+ It exists because an imported site arrives **field-driven whether anyone chose that or not**
3932
+ \u2014 a crawl-based import (Webflow, a starter, a template) emits pages with a typed field schema
3933
+ and an empty block tree, because that is all a crawl can infer. Nobody decided it. On a
3934
+ marketing site it is the wrong answer, and \xA71 already says why converting later means
3935
+ rewriting content. So the platform stops once, at the last moment it is still cheap.
3936
+
3937
+ **Answering \`components\` does not convert anything.** There is no field-to-block converter,
3938
+ and \`extract_component\` cannot stand in for one: it scans \`blockJson\`, which is empty on
3939
+ exactly the pages that would need converting. What it means is that you author the sections,
3940
+ in this order:
3941
+
3942
+ 1. create_component per section (they land as DRAFTS)
3943
+ 2. publish_component each one \u2014 unpublished renders as NOTHING, on a page that 200s
3944
+ 3. set_page_content place them as \`component\` blocks on the page
3945
+ 4. list_extraction_candidates / extract_component
3946
+ now that blocks exist, fold any section repeated 3+ times
3947
+
3948
+ **Answering \`fields\` is a real answer, not a deferral.** A blog, a catalogue or a directory
3949
+ is schema-first by design (\xA71) and should stay that way. Say so and move on.
3950
+
3951
+ Either way: ask, do not choose. The 409 carries this project's actual page counts \u2014 how many
3952
+ are field-driven, block-driven, and how many place a reusable component \u2014 so quote those to
3953
+ the user rather than describing the choice in the abstract.
3817
3954
  `;
3818
3955
 
3819
3956
  // src/prompts.ts
3957
+ var STRUCTURE_RULE = `### Page structure (non-negotiable)
3958
+ ${SECTION_DOCTRINE}`;
3820
3959
  var SCHEMA_PROPOSAL_FLOW = `### Whole-project design (confirm-first) \u2192 \`create_component\` / \`create_page\` / \`create_content_model\`
3821
3960
  Design the WHOLE project from its brief or its code, and **confirm the shape with the user
3822
3961
  BEFORE creating anything**. Never silently guess.
@@ -4067,6 +4206,8 @@ the host has a stale cached MCP \u2014 tell the user to \`rm -rf ~/.npm/_npx\` a
4067
4206
 
4068
4207
  ${PAGE_FLOW}
4069
4208
 
4209
+ ${STRUCTURE_RULE}
4210
+
4070
4211
  After creating, report the page id, slug, type, field count, and the project it landed in.
4071
4212
  On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs (re)authorizing.`
4072
4213
  }
@@ -4143,8 +4284,12 @@ On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs (re)
4143
4284
  type: "text",
4144
4285
  text: `Build a BetterCMS site end-to-end.${request ? ` The user wants: "${request}".` : ""}
4145
4286
 
4287
+ ${SCHEMA_PROPOSAL_FLOW}
4288
+
4146
4289
  ${BUILD_SITE_FLOW}
4147
4290
 
4291
+ ${STRUCTURE_RULE}
4292
+
4148
4293
  ${SEO_FLOW}
4149
4294
 
4150
4295
  Confirm each stage with the user before writing. On 401/403, the MCP key needs (re)authorizing.`
@@ -4172,6 +4317,8 @@ Confirm each stage with the user before writing. On 401/403, the MCP key needs (
4172
4317
 
4173
4318
  ${LANDING_PAGES_FLOW}
4174
4319
 
4320
+ ${STRUCTURE_RULE}
4321
+
4175
4322
  On 401/403, the MCP key needs (re)authorizing.`
4176
4323
  }
4177
4324
  }