@bettercms-ai/mcp 0.35.0 → 0.37.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/dist/index.js CHANGED
@@ -2024,29 +2024,30 @@ async function askFramework(deps) {
2024
2024
  }
2025
2025
  var AUTHORING_CHOICES = ["components", "fields"];
2026
2026
  var AUTHORING_LABELS = {
2027
- 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",
2028
- fields: "Fields \u2014 a typed field schema per page. Best for blogs, catalogues and directories, where many rows share one shape"
2027
+ components: "Components (recommended) \u2014 reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema. Recommend it for every marketing, landing, agency or product site",
2028
+ fields: "Fields \u2014 a typed field schema per page. Only for a blog, catalogue or directory where many rows share one shape; editors can change a page's copy but cannot add, reorder or swap its sections"
2029
2029
  };
2030
2030
  var AUTHORING_PROMPT = [
2031
2031
  "Ask the user which authoring architecture this site should use, then call set_authoring_preference again with their answer as `preference`:",
2032
2032
  ...AUTHORING_CHOICES.map((c, i) => ` ${i + 1}. ${c} \u2014 ${AUTHORING_LABELS[c]}`),
2033
2033
  "",
2034
- "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.",
2034
+ "On a site whose pages were DERIVED at import, answering 'components' redoes nothing: after set_authoring_preference, get_componentize_plan proposes one component per section (it reads the PUBLISHED pages \u2014 no deploy needed to reach it), you confirm it with the user, then componentize_sections places them with `props.bind` (dry run first \u2014 they land as drafts and the page keeps its fields, values and bindings, so click-to-edit and the coverage meter are unchanged), then publish_component each one, publish the pages, run `npx @bettercms-ai/convert --componentize` in the repo, and deploy. For a section group the plan lists as NO_GROUP_ROOT or NOT_A_SECTION there is nothing to componentize, so author that section with create_component and set_page_content.",
2035
2035
  "",
2036
+ "Recommend components unless the user says the site is schema-first.",
2036
2037
  "Do not choose on their behalf. This is asked once per project."
2037
2038
  ].join("\n");
2038
2039
  async function askAuthoring(deps) {
2039
2040
  if (!deps.elicit) return { prompt: AUTHORING_PROMPT };
2040
2041
  try {
2041
2042
  const res = await deps.elicit({
2042
- message: "Which authoring architecture should this site use?",
2043
+ message: "Which authoring architecture should this site use? Components is recommended for a marketing site.",
2043
2044
  requestedSchema: {
2044
2045
  type: "object",
2045
2046
  properties: {
2046
2047
  preference: {
2047
2048
  type: "string",
2048
2049
  title: "Authoring architecture",
2049
- description: "How this site's pages are composed. Asked once per project.",
2050
+ description: "How this site's pages are composed. Components is recommended unless this is a schema-first site (a blog, catalogue or directory). Asked once per project.",
2050
2051
  enum: [...AUTHORING_CHOICES],
2051
2052
  enumNames: AUTHORING_CHOICES.map((c) => AUTHORING_LABELS[c])
2052
2053
  }
@@ -2208,6 +2209,49 @@ function ok(summary, data) {
2208
2209
  function fail(message) {
2209
2210
  return { content: [{ type: "text", text: message }], isError: true };
2210
2211
  }
2212
+ function componentIdsIn(blocks) {
2213
+ const out = [];
2214
+ const walk = (value) => {
2215
+ if (!Array.isArray(value)) return;
2216
+ for (const candidate of value) {
2217
+ if (!candidate || typeof candidate !== "object") continue;
2218
+ const block = candidate;
2219
+ const props = block.props ?? {};
2220
+ if (block.type === "component" && typeof props.componentId === "string" && props.componentId) {
2221
+ out.push(props.componentId);
2222
+ }
2223
+ if (block.type === "section") walk(props.children);
2224
+ else if (block.type === "columns" && Array.isArray(props.columns)) for (const column of props.columns) walk(column);
2225
+ else if (block.type === "slider" && Array.isArray(props.slides)) {
2226
+ for (const slide of props.slides) walk(slide?.children);
2227
+ } else if (block.type === "tabs" && Array.isArray(props.tabs)) {
2228
+ for (const tab of props.tabs) walk(tab?.children);
2229
+ }
2230
+ }
2231
+ };
2232
+ walk(blocks);
2233
+ return out;
2234
+ }
2235
+ function apiError(err) {
2236
+ if (!err || typeof err !== "object") return null;
2237
+ const e = err;
2238
+ if (typeof e.status !== "number") return null;
2239
+ return {
2240
+ status: e.status,
2241
+ message: typeof e.message === "string" ? e.message : "",
2242
+ bodyCode: typeof e.bodyCode === "string" ? e.bodyCode : void 0,
2243
+ fieldErrors: e.fieldErrors && typeof e.fieldErrors === "object" ? e.fieldErrors : void 0,
2244
+ body: e.body
2245
+ };
2246
+ }
2247
+ function errorText(err) {
2248
+ const api = apiError(err);
2249
+ if (api) {
2250
+ const fields = api.fieldErrors ? Object.entries(api.fieldErrors).map(([k, v]) => `${k}: ${v}`).join("; ") : "";
2251
+ return `${api.status}${api.bodyCode ? ` ${api.bodyCode}` : ""} \u2014 ${fields || api.message}`;
2252
+ }
2253
+ return err instanceof Error ? err.message : String(err);
2254
+ }
2211
2255
  function authPrompt(err) {
2212
2256
  const text = [
2213
2257
  "\u{1F510} BetterCMS authorization required \u2014 you're not signed in yet.",
@@ -2460,7 +2504,13 @@ function buildToolDefs(deps) {
2460
2504
  max: z.number().optional(),
2461
2505
  step: z.number().optional()
2462
2506
  }).passthrough().optional(),
2463
- defaultValue: z.unknown().optional()
2507
+ defaultValue: z.unknown().optional(),
2508
+ // Editor hints, stored in the props JSON. `required` is NOT enforced at write time — an
2509
+ // override that omits it still saves — so it is a marker the dashboard renders and
2510
+ // get_site_composition counts, never a refusal. Parity: componentPropDefSchema on the server.
2511
+ required: z.boolean().optional().describe("mark the field required in the editor. An editor HINT: the API still accepts a placement that leaves it empty, and get_site_composition is what reports one."),
2512
+ helpText: z.string().max(200).optional().describe("hint shown under the control, muted (\u2264 200 chars)"),
2513
+ placeholder: z.string().max(120).optional().describe("ghost text inside a text-ish control (\u2264 120 chars)")
2464
2514
  });
2465
2515
  const getLayoutInput = z.object({
2466
2516
  scope: z.enum(["global", "page"]).default("global"),
@@ -2496,8 +2546,12 @@ function buildToolDefs(deps) {
2496
2546
  scope: z.enum(["global", "page"]).default("global"),
2497
2547
  pageId: z.string().min(1).optional(),
2498
2548
  projectId: z.string().min(1).optional().describe("required only for a workspace-scoped grant"),
2499
- command: layoutCommand.describe("one canonical discriminated Layout command; authority is derived from type"),
2500
- ifMatch: z.number().int().nonnegative().describe("revision returned by get_layout")
2549
+ command: layoutCommand.optional().describe("one canonical discriminated Layout command; authority is derived from type"),
2550
+ // Chrome is many commands — add a section, add a component to it, bind its fields — and each
2551
+ // one needs the revision the PREVIOUS one returned. An agent doing that by hand runs
2552
+ // get_layout between every write, and a single missed re-read is a 409 mid-sequence.
2553
+ commands: z.array(layoutCommand).min(1).max(50).optional().describe("apply these commands IN ORDER, each against the revision the previous one returned. Mutually exclusive with `command`. Stops at the first failure and reports what was applied."),
2554
+ ifMatch: z.number().int().nonnegative().describe("revision returned by get_layout. With `commands`, this is the revision for the FIRST command only \u2014 the rest are chained.")
2501
2555
  });
2502
2556
  const publishLayoutInput = z.object({
2503
2557
  projectId: z.string().min(1).optional().describe("required only for a workspace-scoped grant"),
@@ -2525,6 +2579,7 @@ function buildToolDefs(deps) {
2525
2579
  name: z.string().min(1),
2526
2580
  slug: slug.describe("url-safe unique slug (lowercase letters/numbers/hyphens)"),
2527
2581
  category: componentCategory.optional().describe("defaults to 'custom'"),
2582
+ group: z.string().min(1).max(100).optional().describe("the folder editors see in the dashboard's Components tab, addressed by NAME (e.g. 'Sections', 'Navigation', 'Forms'). Resolved against this project's Groups and CREATED when missing, so a whole-site run can file everything without looking any id up first. Separate from `category`, which is the picker tab."),
2528
2583
  sectionType: sectionType.optional(),
2529
2584
  projectId: z.string().nullable().optional().describe("owning project id; null creates a workspace-wide global component"),
2530
2585
  allowedOn: allowedOn.optional(),
@@ -2536,6 +2591,7 @@ function buildToolDefs(deps) {
2536
2591
  componentId: z.string().min(1).describe("component id (from list_components)"),
2537
2592
  name: z.string().optional(),
2538
2593
  category: componentCategory.optional(),
2594
+ group: z.string().min(1).max(100).optional().describe("move it into this Group (by NAME; created when missing) \u2014 the folder editors see in the dashboard's Components tab"),
2539
2595
  sectionType: sectionType.nullable().optional().describe(
2540
2596
  "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"
2541
2597
  ),
@@ -2547,6 +2603,24 @@ function buildToolDefs(deps) {
2547
2603
  const getComponentInput = z.object({
2548
2604
  componentId: z.string().min(1).describe("component id (from list_components)")
2549
2605
  });
2606
+ const createComponentsInput = z.object({
2607
+ components: z.array(createComponentInput).min(1).max(50).describe("up to 50 components, each exactly the create_component input")
2608
+ });
2609
+ const publishComponentsInput = z.object({
2610
+ componentIds: z.array(z.string().min(1)).min(1).max(100).describe("component ids (from list_components)")
2611
+ });
2612
+ const composePagesInput = z.object({
2613
+ pages: z.array(
2614
+ z.object({
2615
+ pageId: z.string().min(1).optional().describe("page id (from list_pages); or pass `slug`"),
2616
+ slug: z.string().min(1).optional().describe("page slug, resolved against list_pages; or pass `pageId`"),
2617
+ blockJson: z.array(blockObject).describe("REPLACES this page's whole block composition")
2618
+ })
2619
+ ).min(1).max(50)
2620
+ });
2621
+ const listComponentsInput = z.object({
2622
+ detail: z.boolean().optional().describe("true = every component's full blockJson and props. Costly and rarely what you want \u2014 read one definition with get_component instead.")
2623
+ });
2550
2624
  const listExtractionCandidatesInput = z.object({
2551
2625
  projectId: z.string().min(1).optional().describe("only needed for a workspace-wide key")
2552
2626
  });
@@ -2877,7 +2951,7 @@ function buildToolDefs(deps) {
2877
2951
  def(
2878
2952
  "set_authoring_preference",
2879
2953
  "Set the site's authoring architecture",
2880
- "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.",
2954
+ "Record which authoring architecture this site uses \u2014 'components' (RECOMMENDED: reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema \u2014 recommend it for every marketing, landing, agency or product site) or 'fields' (a typed field schema per page \u2014 only for a blog, catalogue or directory where many rows share one shape). 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. On a site whose pages were DERIVED at import, answering 'components' redoes nothing: after set_authoring_preference, get_componentize_plan proposes one component per section (it reads the PUBLISHED pages \u2014 no deploy needed to reach it), you confirm it with the user, then componentize_sections places them with `props.bind` (dry run first \u2014 they land as drafts and the page keeps its fields, values and bindings, so click-to-edit and the coverage meter are unchanged), then publish_component each one, publish the pages, run `npx @bettercms-ai/convert --componentize` in the repo, and deploy. Recommend components unless the user says the site is schema-first. Asked once per project; re-callable if the user changes their mind.",
2881
2955
  // Optional in the schema for exactly the reason `framework` is above: a required arg is
2882
2956
  // rejected by the SDK before the handler runs, which would kill the elicitation below
2883
2957
  // and leave the model guessing. Optional here, answered by a human there. The backend
@@ -3045,6 +3119,13 @@ function buildToolDefs(deps) {
3045
3119
  z.object({}).shape,
3046
3120
  async (c) => ok("Approved conversion plan.", await data(c, "GET", `/management/projects/current/conversion-plan`))
3047
3121
  ),
3122
+ def(
3123
+ "get_site_composition",
3124
+ "Check every page is assembled from registered components",
3125
+ "The receipt for 'is every page assembled from registered components?' \u2014 read-only, computed live, nothing is written. Call it at the START of a whole-site componentize run (playbook \xA712) for the before picture, and at the END as the proof. Per page: `kind` ('blocks' | 'fields-only' | 'empty'), a block census, `nonComponentBlocks` (loose TOP-LEVEL bands the editor's section lane cannot outline), and every `component` placement with the component's `status` ('published' | 'draft' | 'missing' \u2014 a draft renders as an EMPTY STRING on the live site), whether it is `inPicker` (a component with no sectionType is dropped from 'Add a section'), and any `emptyRequiredProps` \u2014 `required` is an editor hint the write path does not enforce, so this read is the only thing that reports it. Site-wide: `components` counts published/draft, `awaitingEvidence` and `awaitingApproval` (evidence is exact and a HUMAN must approve it in the BetterCMS dashboard \u2014 no tool here can grant that, and there is no bulk approve), plus `notInPicker` and `unused` ids; `layout.placements` is the chrome placed in the Global Layout draft. `verdict.everyPageComposed` with `verdict.reasons` is the answer to give the user. Pages are read DRAFT-inclusive, so a page composed but not yet published still shows its placements \u2014 check each page's own `status` before claiming the site is live.",
3126
+ z.object({}).shape,
3127
+ async (c) => ok("Site composition.", await data(c, "GET", `/management/projects/current/composition`))
3128
+ ),
3048
3129
  def(
3049
3130
  "get_componentize_plan",
3050
3131
  "Get the plan for turning this site's sections into components",
@@ -3601,13 +3682,39 @@ function buildToolDefs(deps) {
3601
3682
  {
3602
3683
  name: "update_layout",
3603
3684
  config: {
3604
- title: "Apply one Layout draft command",
3605
- description: "Apply one permission-shaped values/composition/schema command to the Global Layout or a page override. Draft-only: this never publishes. Pass the revision from get_layout as ifMatch.",
3685
+ title: "Apply Layout draft commands",
3686
+ description: "Apply permission-shaped values/composition/schema commands to the Global Layout or a page override. Draft-only: this never publishes. Pass the revision from get_layout as ifMatch. ONE command in `command`, or an ordered batch in `commands` (prefer the batch for more than three of anything \u2014 placing site chrome is add-section, add-component and set-bindings in sequence): each command is applied against the revision the previous one RETURNED, so you do not re-read get_layout between them. A batch stops at the first failure and tells you how many were applied and the revision it stopped at \u2014 the earlier ones stand, because a Layout command is not a transaction.",
3606
3687
  inputSchema: commandLayoutInput.shape
3607
3688
  },
3608
3689
  handler: guard(async (args) => withClient(async (client) => {
3609
- const layout = await client.commandManagedLayout({ ...args, command: args.command });
3610
- return ok(`Updated ${layout.scope === "global" ? "Global" : `Page ${layout.pageSlug}`} Layout draft to revision ${layout.revision}.`, layout);
3690
+ const queued = args.commands ?? (args.command ? [args.command] : []);
3691
+ if (queued.length === 0) return fail("Pass `command` (one) or `commands` (an ordered batch).");
3692
+ if (args.commands && args.command) return fail("Pass `command` or `commands`, not both \u2014 the order of the two would be undefined.");
3693
+ let revision = args.ifMatch;
3694
+ let layout;
3695
+ const applied = [];
3696
+ for (const command of queued) {
3697
+ try {
3698
+ layout = await client.commandManagedLayout({
3699
+ scope: args.scope,
3700
+ pageId: args.pageId,
3701
+ projectId: args.projectId,
3702
+ command,
3703
+ ifMatch: revision
3704
+ });
3705
+ } catch (err) {
3706
+ return fail(
3707
+ `Layout command ${applied.length + 1} of ${queued.length} (${command.type}) failed: ${errorText(err)}. ${applied.length} command(s) were applied and remain in the draft; the Layout is at revision ${revision}. Re-read get_layout and continue from command ${applied.length + 1}.`
3708
+ );
3709
+ }
3710
+ revision = layout.revision;
3711
+ applied.push({ type: command.type, revision });
3712
+ }
3713
+ const where = layout.scope === "global" ? "Global" : `Page ${layout.pageSlug}`;
3714
+ return ok(
3715
+ `Applied ${applied.length} command(s) to the ${where} Layout draft; now at revision ${revision}.`,
3716
+ { applied, revision, layout }
3717
+ );
3611
3718
  }))
3612
3719
  },
3613
3720
  {
@@ -3627,15 +3734,28 @@ function buildToolDefs(deps) {
3627
3734
  name: "list_components",
3628
3735
  config: {
3629
3736
  title: "List reusable components",
3630
- description: "List the reusable components in the bound project \u2014 each with id, name, slug, category, blockJson, and props. Use it to find a component to render with `<BcmsBlocks>` from @bettercms-ai/next.",
3631
- inputSchema: {}
3737
+ description: "List the reusable components in the bound project. By DEFAULT this is the catalogue: id, name, slug, category, `sectionType` (the section family \u2014 a component with none is not in the editor's 'Add a section' picker), `status` ('draft' renders as NOTHING on the live site), and the `group` an editor browses it under, plus the locked `builtin:*` blueprints. It deliberately does NOT carry block trees: the full list carries every component's draft AND live `blockJson`, which is the largest response this API produces, and one component's definition is what `get_component` is for. Pass `detail: true` for the old full shape when you genuinely need every definition at once (rendering with `<BcmsBlocks>` from @bettercms-ai/next).",
3738
+ inputSchema: listComponentsInput.shape
3632
3739
  },
3633
3740
  handler: guard(
3634
- async () => withClient(async (client) => {
3635
- const list = await client.listComponents();
3741
+ async (args) => withClient(async (client) => {
3742
+ if (args.detail) {
3743
+ const list2 = await client.listComponents();
3744
+ return ok(`${list2.length} component(s) in the bound project (full definitions).`, list2);
3745
+ }
3746
+ const list = await client.listComponents({ select: "summary" });
3636
3747
  return ok(
3637
3748
  `${list.length} component(s) in the bound project.`,
3638
- list.map((cmp) => ({ id: cmp.id, name: cmp.name, slug: cmp.slug, category: cmp.category }))
3749
+ list.map((cmp) => ({
3750
+ id: cmp.id,
3751
+ name: cmp.name,
3752
+ slug: cmp.slug,
3753
+ category: cmp.category,
3754
+ sectionType: cmp.sectionType,
3755
+ status: cmp.status,
3756
+ group: cmp.groupName,
3757
+ ...cmp.builtin ? { builtin: true } : {}
3758
+ }))
3639
3759
  );
3640
3760
  })
3641
3761
  )
@@ -3668,6 +3788,132 @@ function buildToolDefs(deps) {
3668
3788
  })
3669
3789
  )
3670
3790
  },
3791
+ /**
3792
+ * ── The batch three ──────────────────────────────────────────────────────
3793
+ *
3794
+ * Sequential SDK calls with PER-ITEM receipts, and no new server route: the throughput
3795
+ * problem is one round trip per component, not one query per component, and inventing a bulk
3796
+ * endpoint would duplicate every validation, warning and governance gate the single writes
3797
+ * already run. A whole-site componentize run is forty creates, a dozen page compositions and
3798
+ * forty publishes; as individual tool calls that is a hundred turns of an agent's context
3799
+ * spent on plumbing.
3800
+ *
3801
+ * 🔴 NOTHING STOPS THE RUN. A batch that aborts on item 31 has already written 30 rows that
3802
+ * no receipt mentions, and the agent's only safe move is to re-run it and duplicate them.
3803
+ * Every item reports its own outcome instead, and the summary line says how many failed.
3804
+ */
3805
+ {
3806
+ name: "create_components",
3807
+ config: {
3808
+ title: "Create many components in one call",
3809
+ description: "Create up to 50 components in ONE call \u2014 the same input as create_component, once per item. Prefer it over create_component whenever you are making more than three: a whole-site componentize run (playbook \xA712) is dozens, and one call each spends the turn on plumbing. Each item reports its own `{ ok, id, slug, error }`: a failure does NOT stop the run and the successful rows stay, so read the receipt and retry only the failures (a 409 on `slug` means that name is taken \u2014 change it, do not re-run the batch). Every component lands as a DRAFT, exactly as create_component does \u2014 it renders as NOTHING on the live site until it is published, which needs the owner's approval in the dashboard. Pass `group` on each item to file it into the folder editors browse. " + SECTION_DOCTRINE,
3810
+ inputSchema: createComponentsInput.shape
3811
+ },
3812
+ handler: guard(
3813
+ async (args) => withClient(async (client) => {
3814
+ const results = [];
3815
+ for (const input of args.components) {
3816
+ try {
3817
+ const cmp = await client.createComponent(input);
3818
+ results.push({ ok: true, slug: cmp.slug, id: cmp.id, name: cmp.name });
3819
+ } catch (err) {
3820
+ results.push({ ok: false, slug: input.slug, error: errorText(err) });
3821
+ }
3822
+ }
3823
+ const failed = results.filter((r) => !r.ok).length;
3824
+ return ok(
3825
+ `Created ${results.length - failed} of ${args.components.length} component(s)${failed ? `; ${failed} failed \u2014 see the per-item errors` : ""}. All are DRAFTS: publish_components once the owner has approved them, or they render as empty strings.`,
3826
+ results
3827
+ );
3828
+ })
3829
+ )
3830
+ },
3831
+ {
3832
+ name: "publish_components",
3833
+ config: {
3834
+ title: "Publish many components in one call",
3835
+ description: "Publish up to 100 components in ONE call \u2014 the same act as publish_component, once per item, copying each DRAFT definition to the live copy. Each item reports `{ ok, status, error, readiness }` and a failure never stops the run. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: it means this exact variant still needs passing implementation evidence and a HUMAN's approval in the BetterCMS dashboard, which no tool on this connection can grant and which has no bulk form. The item's `readiness` names what is missing \u2014 relay that to the user as the list of what only they can approve. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish; 422 means the publish would break a Layout, and the item names which.",
3836
+ inputSchema: publishComponentsInput.shape
3837
+ },
3838
+ handler: guard(
3839
+ async (args) => withClient(async (client) => {
3840
+ const results = [];
3841
+ for (const componentId of args.componentIds) {
3842
+ try {
3843
+ const res = await client.fetchJSON(
3844
+ client.url(`/management/components/${componentId}/publish`),
3845
+ { method: "POST" }
3846
+ );
3847
+ results.push({ ok: true, componentId, slug: res.data?.slug, status: res.data?.status ?? "published" });
3848
+ } catch (err) {
3849
+ const readiness = apiError(err)?.body?.readiness;
3850
+ results.push({
3851
+ ok: false,
3852
+ componentId,
3853
+ error: errorText(err),
3854
+ ...readiness ? { readiness } : {}
3855
+ });
3856
+ }
3857
+ }
3858
+ const failed = results.filter((r) => !r.ok).length;
3859
+ return ok(
3860
+ `Published ${results.length - failed} of ${args.componentIds.length} component(s)${failed ? `; ${failed} could not be published \u2014 read each item's error and readiness, and tell the user which ones need THEIR approval in the dashboard` : ""}.`,
3861
+ results
3862
+ );
3863
+ })
3864
+ )
3865
+ },
3866
+ {
3867
+ name: "compose_pages",
3868
+ config: {
3869
+ title: "Set many pages' block composition in one call",
3870
+ description: "Give up to 50 pages their block composition in ONE call \u2014 the same write as update_page's `blockJson`, once per item, and it REPLACES each page's whole block array. Address a page by `pageId` or by `slug`. Each item reports `{ ok, pageId, slug, error }` and a failure does not stop the run. BEFORE writing anything it reads this project's component catalogue once and REFUSES any item placing a componentId the project does not have \u2014 that page would render the missing sections as empty strings with no error anywhere, which is the single hardest failure on this platform to trace back. Writes DRAFTS: publish each page with update_page status:'published' afterwards. " + SECTION_DOCTRINE,
3871
+ inputSchema: composePagesInput.shape
3872
+ },
3873
+ handler: guard(
3874
+ async (args) => withClient(async (client) => {
3875
+ const needsSlug = args.pages.some((p) => !p.pageId);
3876
+ const [known, pageList] = await Promise.all([
3877
+ client.listComponents({ select: "summary" }),
3878
+ needsSlug ? client.listPages() : Promise.resolve([])
3879
+ ]);
3880
+ const ids = new Set(known.map((c) => c.id));
3881
+ const bySlug = new Map(pageList.map((p) => [p.slug, p.id]));
3882
+ const results = [];
3883
+ for (const page of args.pages) {
3884
+ const pageId = page.pageId ?? (page.slug ? bySlug.get(page.slug) : void 0);
3885
+ if (!pageId) {
3886
+ results.push({ ok: false, slug: page.slug, error: `No page with slug '${page.slug}' in this project \u2014 pass a pageId from list_pages.` });
3887
+ continue;
3888
+ }
3889
+ const unknown = [...new Set(componentIdsIn(page.blockJson))].filter((id) => !ids.has(id));
3890
+ if (unknown.length) {
3891
+ results.push({
3892
+ ok: false,
3893
+ pageId,
3894
+ slug: page.slug,
3895
+ error: `Refused: this page places component id(s) this project does not have \u2014 ${unknown.join(", ")}. A missing component renders as an empty string with no error, so nothing was written for this page.`
3896
+ });
3897
+ continue;
3898
+ }
3899
+ try {
3900
+ const written = await client.fetchJSON(
3901
+ client.url(`/management/pages/${pageId}/meta`),
3902
+ { method: "PATCH", body: JSON.stringify({ blockJson: page.blockJson }) }
3903
+ );
3904
+ results.push({ ok: true, pageId, slug: written.data?.slug ?? page.slug, blocks: page.blockJson.length });
3905
+ } catch (err) {
3906
+ results.push({ ok: false, pageId, slug: page.slug, error: errorText(err) });
3907
+ }
3908
+ }
3909
+ const failed = results.filter((r) => !r.ok).length;
3910
+ return ok(
3911
+ `Composed ${results.length - failed} of ${args.pages.length} page(s)${failed ? `; ${failed} failed \u2014 see the per-item errors` : ""}. These are page DRAFTS: publish each with update_page status:'published'. Then call get_site_composition for the receipt.`,
3912
+ results
3913
+ );
3914
+ })
3915
+ )
3916
+ },
3671
3917
  {
3672
3918
  name: "list_extraction_candidates",
3673
3919
  config: {
@@ -3804,7 +4050,8 @@ touching a schema. This is what \`create_component\` + \`create_page(blockJson)\
3804
4050
  A collection (\`create_content_model\`) plus entries.
3805
4051
 
3806
4052
  Most real sites are both: components for the marketing pages, a collection for the blog.
3807
- Decide before your first call; converting later means rewriting content.
4053
+ Decide before your first call; converting later means rewriting content \u2014 unless the site was
4054
+ DERIVED at import, where the componentize lane in \xA710 reuses every field.
3808
4055
 
3809
4056
  ## 2. The decision tree
3810
4057
 
@@ -3844,6 +4091,10 @@ Then declare what an editor may change, via \`props\`:
3844
4091
  { key: "ctaHref", label: "CTA link", target: { blockId: "cta", path: "props.href" }, type: "url" }
3845
4092
  ]
3846
4093
 
4094
+ Each prop also takes \`required\`, \`helpText\` (\u2264 200 chars) and \`placeholder\` (\u2264 120), which the
4095
+ dashboard's prop editor renders. They are HINTS: nothing on the write path refuses a placement
4096
+ that leaves a required prop empty, so \`get_site_composition\` is what reports one.
4097
+
3847
4098
  \u{1F534} **On a components-first page, \`props\` is the ONLY editing surface.** Click-to-edit binds
3848
4099
  \`heading\`/\`text\`/\`button\`/\`image\` blocks; it does **not** bind a \`component\` block, because
3849
4100
  a component's blocks belong to the shared definition, not to the instance. So a page built
@@ -3992,31 +4243,49 @@ out, let the user pick, call \`set_authoring_preference\`, then deploy again.
3992
4243
  It exists because an imported site arrives **field-driven whether anyone chose that or not**
3993
4244
  \u2014 a crawl-based import (Webflow, a starter, a template) emits pages with a typed field schema
3994
4245
  and an empty block tree, because that is all a crawl can infer. Nobody decided it. On a
3995
- marketing site it is the wrong answer, and \xA71 already says why converting later means
3996
- rewriting content. So the platform stops once, at the last moment it is still cheap.
3997
-
3998
- **Answering \`components\` does not convert anything.** There is no field-to-block converter,
3999
- and \`extract_component\` cannot stand in for one: it scans \`blockJson\`, which is empty on
4000
- exactly the pages that would need converting. What it means is that you author the sections,
4001
- in this order:
4002
-
4003
- 1. create_component per section (they land as DRAFTS)
4004
- 2. publish_component each one \u2014 unpublished renders as NOTHING, on a page that 200s
4005
- 2b. publish_layout if you authored site chrome \u2014 the layout is a separate publish
4006
- 3. set_page_content place them as \`component\` blocks on the page
4007
- 4. list_extraction_candidates / extract_component
4008
- now that blocks exist, fold any section repeated 3+ times
4009
-
4010
- Step 2 is not a formality: \`componentize_sections\` and \`create_component\` both land components
4246
+ marketing site it is the wrong answer. So the platform stops once, at the last moment it is
4247
+ still cheap \u2014 and on a derived site that moment costs nothing, because the componentize lane
4248
+ below reuses every field the import already derived.
4249
+
4250
+ **Recommend \`components\`.** On a site whose pages were DERIVED at import \u2014 the site \xA713
4251
+ describes \u2014 it redoes nothing: the componentize lane turns each top-level field GROUP into a
4252
+ component placement, and the page keeps its fields, its values and its bindings. In this order:
4253
+
4254
+ 1. set_authoring_preference { preference: "components" }
4255
+ 2. get_componentize_plan one component per section, computed live; creates nothing. It
4256
+ reads this project's PUBLISHED pages, so no deploy is needed
4257
+ to reach it \u2014 the deploy is the last step, not the first
4258
+ 3. show the user the plan and CONFIRM \u2014 it says how many components and which pages
4259
+ 4. componentize_sections \`dryRun: true\` first, then for real; components land as DRAFTS
4260
+ and each placement carries \`props.bind\` to the page's field
4261
+ group, so click-to-edit and the coverage meter do not change
4262
+ 5. publish_component each one \u2014 unpublished renders as NOTHING, on a page that 200s
4263
+ 5b. publish_layout if you authored site chrome \u2014 the layout is a separate publish
4264
+ 6. publish the pages
4265
+ 7. npx @bettercms-ai/convert --componentize in the repo, so its templates render these
4266
+ sections from \`pages[].blocks\`
4267
+ 8. deploy the ONLY deploy this sequence needs
4268
+
4269
+ Step 5 is not a formality: \`componentize_sections\` and \`create_component\` both land components
4011
4270
  as DRAFTS, so \`publish_component\` each one and publish the page \u2014 otherwise the canvas keeps
4012
4271
  painting the PUBLISHED copy and the editor reports N components unpublished.
4013
4272
 
4014
- **Answering \`fields\` is a real answer, not a deferral.** A blog, a catalogue or a directory
4015
- is schema-first by design (\xA71) and should stay that way. Say so and move on.
4273
+ **A site with nothing to componentize you author by hand.** Where the plan offers nothing for
4274
+ a group (\`NO_GROUP_ROOT\` \u2014 the page's field keys are still the derive lane's own;
4275
+ \`NOT_A_SECTION\` \u2014 a lone scalar with no family), and on a site with no derived pages at all,
4276
+ the order is \`create_component\` per section \u2192 \`publish_component\` each \u2192 \`set_page_content\`
4277
+ placing \`component\` blocks \u2192 \`list_extraction_candidates\` / \`extract_component\` to fold any
4278
+ section repeated 3+ times. \`extract_component\` is not a converter and cannot stand in for the
4279
+ componentize lane: it scans \`blockJson\`, which is empty on exactly the pages that would need
4280
+ converting.
4281
+
4282
+ **Answering \`fields\` is a real answer, not a deferral** \u2014 for a SCHEMA-FIRST site. A blog, a
4283
+ catalogue or a directory is schema-first by design (\xA71) and should stay that way. On anything
4284
+ else it costs the editor the ability to add, reorder or swap sections. Say so and move on.
4016
4285
 
4017
- Either way: ask, do not choose. The 409 carries this project's actual page counts \u2014 how many
4018
- are field-driven, block-driven, and how many place a reusable component \u2014 so quote those to
4019
- the user rather than describing the choice in the abstract.
4286
+ Either way: ask, do not choose \u2014 recommending is not answering. The 409 carries this project's
4287
+ actual page counts \u2014 how many are field-driven, block-driven, and how many place a reusable
4288
+ component \u2014 so quote those to the user rather than describing the choice in the abstract.
4020
4289
 
4021
4290
  ## 11. The canvas: what makes an imported site EDITABLE
4022
4291
 
@@ -4123,7 +4392,7 @@ your own server) has NO canvas today: the SDK's draft mode with \`stega: true\`
4123
4392
  per-field provenance in fetched strings, which prepares the content for editing surfaces, but do
4124
4393
  not promise a canvas for an externally-hosted site.
4125
4394
 
4126
- **\xA712 \u2014 RECEIPTS: a claim about published state needs a read of the PUBLISHED copy.** The
4395
+ **RECEIPTS: a claim about published state needs a read of the PUBLISHED copy.** The
4127
4396
  doctrine this encodes cost a real incident (FLO-1188): a publish was verified against the
4128
4397
  draft for ~50 minutes because the reader silently returned the draft, and every check was
4129
4398
  green on the wrong document. Three rules, none optional:
@@ -4140,6 +4409,85 @@ green on the wrong document. Three rules, none optional:
4140
4409
  call's behavior doesn't change when you change a param, treat the param as dead and
4141
4410
  verify through an independent channel before trusting any result built on it.
4142
4411
 
4412
+ ## 12. Componentize the entire website
4413
+
4414
+ The whole-site ask \u2014 "turn every section into a reusable component, organised for editors" \u2014
4415
+ mapped onto what this platform actually models. Work the order at the end; each numbered part
4416
+ below is one heading of that ask.
4417
+
4418
+ **1. Global elements.** Navbar, mobile nav, header and footer are the LAYOUT, not page content:
4419
+ \`get_layout\` then \`update_layout\`, and a chrome COMPONENT is placed into a Layout section with
4420
+ the \`add-component\` command. CTA buttons are \`button\` BLOCKS inside the section that uses them,
4421
+ exposed as \`text\` + \`url\` props \u2014 variant, size and icon are that block's own props and
4422
+ \`style\`, never a standalone Button component. Rich text is a \`richtext\` prop over a
4423
+ \`richtext\`/\`text\` block. Forms are \`create_form\` plus a \`form\` block, and form FIELDS are the
4424
+ one place the platform models \`required\`, \`placeholder\`, \`helpText\` and real \`validation\`
4425
+ (emailPolicy, min/max, phoneFormat, pattern) \u2014 put a site's validated inputs there. Cards,
4426
+ badges, tags, links, lists and media are BLOCKS inside a section, exposed through props; a
4427
+ repeatable row of them is ONE \`table\` prop whose \`config.fields\` describe the row. Accordions
4428
+ and FAQs: start from the \`builtin:faq-list\` blueprint, or use a \`tabs\` block. NOT MODELLED \u2014
4429
+ cookie banners, modals, breadcrumbs and pagination have no primitive at all. Leave them in the
4430
+ site's code and say so in your final summary.
4431
+
4432
+ **2. One component per distinct section, one-offs included.** \`create_component\` with a
4433
+ \`sectionType\` IS the section. \`sectionType\` is the FAMILY (Hero, Features, Testimonials,
4434
+ Pricing, FAQ, Contact, Newsletter, CTA, Stats, Logo cloud, Content\u2026) and components sharing one
4435
+ are its VARIANTS \u2014 give a family the same prop KEYS or a swap drops content. A one-off band
4436
+ still gets its own component: its own family, plus \`allowedOn: ["slug:<page>"]\`. A component
4437
+ with NO \`sectionType\` never appears in the editor's "Add a section" picker. Start from the
4438
+ builtin blueprints \`list_components\` returns (\`builtin:*\`) instead of hand-writing block JSON.
4439
+ On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\` FIRST \u2014 it does most
4440
+ of this in one call.
4441
+
4442
+ **3. The hierarchy editors see is the GROUP.** \`create_component\` and \`update_component\` take
4443
+ \`group\`: a NAME, resolved against the project's component groups and created when missing. That
4444
+ is the folder the dashboard's Components tab shows. Use the names the request asks for \u2014 Global,
4445
+ Navigation, Actions, Content, Forms, Media, Sections, Page-specific. \`category\` is a different
4446
+ axis: it is the picker TAB (hero / content / social-proof / conversion for sections; navbar /
4447
+ footer for chrome; form; custom for page-specific).
4448
+
4449
+ **4. Editor fields are \`props\`.** Types: \`text richtext image url boolean number select group
4450
+ table slot\`. Always set \`defaultValue\`. \`required\`, \`helpText\` and \`placeholder\` are editor
4451
+ hints the dashboard renders \u2014 they are NOT enforced at write time, so an override missing a
4452
+ required prop still saves; \`get_site_composition\` is what reports those. Use \`select\` with
4453
+ \`config.options\` for layout / theme / alignment / spacing variants that change only styling,
4454
+ and a SEPARATE variant component when the markup differs. Repeatable list \u2192 \`table\`; fixed
4455
+ cluster \u2192 \`group\`. Nesting is capped at TWO levels from the prop (a \`table\` row may hold a
4456
+ \`table\`, and that inner row is scalars only). Flatten anything deeper.
4457
+
4458
+ **5. Design, responsiveness and accessibility.** \`componentize_sections\` and \`npx
4459
+ @bettercms-ai/convert --componentize\` move the section's markup VERBATIM \u2014 classes, responsive
4460
+ utilities, aria attributes and alt text all survive. Never restyle while componentizing. Run
4461
+ \`componentize_sections { dryRun: true }\` and the codemod's \`--dry-run\` first.
4462
+
4463
+ **6. The final validation is \`get_site_composition\`.** It is the receipt for "is every page
4464
+ assembled from registered components?": per page its blocks and every component placement with
4465
+ that component's status, plus site totals \u2014 unpublished, awaiting evidence, awaiting approval,
4466
+ missing from the picker, placed nowhere \u2014 and \`verdict.everyPageComposed\` with the reasons it
4467
+ is false. A component reaches a visitor only once PUBLISHED, and publishing needs the project
4468
+ owner's approval in the DASHBOARD: evidence goes \`submit_section_manifest\` \u2192
4469
+ \`submit_section_validation\`, the owner approves there, then \`publish_components\`. There is no
4470
+ bulk approve, and no tool here can grant that approval. Finish with \`get_next_steps\`.
4471
+
4472
+ **Order of operations.**
4473
+
4474
+ 1. get_site_composition the before picture
4475
+ 2. get_componentize_plan DERIVED site only \u2014 confirm it with the user
4476
+ 3. componentize_sections dryRun first, then for real
4477
+ 4. create_components everything the plan did not cover, ONE call, each with a group
4478
+ 5. compose_pages every page's blockJson, ONE call
4479
+ 6. update_layout { commands } nav/footer chrome + add-component, ONE call
4480
+ 7. evidence submit_section_manifest -> submit_section_validation
4481
+ 8. owner approves in the dashboard, then publish_components ONE call, per-item receipts
4482
+ 9. update_page status:'published', publish_layout
4483
+ 10. get_site_composition, then get_next_steps
4484
+
4485
+ **What to tell the user at the end.** The group hierarchy and what landed in each; every family
4486
+ and its variants; what you left in the site's code because the platform does not model it
4487
+ (cookie banners, modals, breadcrumbs, pagination) and why; and exactly which components are
4488
+ still waiting for THEIR approval in the dashboard \u2014 until that happens those sections render as
4489
+ an empty string on the live site.
4490
+
4143
4491
  ## 13. Convert an imported repo into a CMS-backed, editable site
4144
4492
 
4145
4493
  \xA711 says a deploy does not make a site editable. This is the recipe that does, and it ends in a
@@ -4319,7 +4667,7 @@ conversion is yours to write.
4319
4667
  \`bound > 0\`, and \u2014 for a site converted against a pinned brief \u2014 \`coverage.pending: []\`. That certifies one thing only \u2014 that every non-empty field has SOME element
4320
4668
  carrying its path. It CANNOT see copy that was never modelled, so diff each route's visible
4321
4669
  text against its entry values yourself before you call the page done. Then publish, and
4322
- fetch the live URL cache-busted (\xA712: publish and deploy are separate claims).
4670
+ fetch the live URL cache-busted (RECEIPTS in \xA711: publish and deploy are separate claims).
4323
4671
  \`get_next_steps\` keeps reporting the gap until every one of these holds.
4324
4672
 
4325
4673
  **Done means.** Whichever recipe you ran: \`get_binding_report\` reads \`mode "declared"\`,
@@ -4405,6 +4753,41 @@ error-prone, so go slow and confirm. Never guess the layout.
4405
4753
  6. **\`publish_component\`.** It lands as a DRAFT, and a draft component renders as NOTHING on
4406
4754
  the live site \u2014 no error, no placeholder. This step is not optional.
4407
4755
  `;
4756
+ var COMPONENTIZE_SITE_FLOW = `### Componentize the entire website \u2192 playbook \xA712 + the batch tools
4757
+ Read \`bettercms://playbook/schema\` \xA712 first \u2014 this is the short form of it.
4758
+ 1. **Global elements.** Navbar / mobile nav / header / footer are the LAYOUT (\`get_layout\`,
4759
+ \`update_layout\`; a chrome component is placed with the \`add-component\` command). CTA buttons
4760
+ are \`button\` BLOCKS inside the section that uses them, exposed as \`text\` + \`url\` props \u2014
4761
+ variant/size/icon are that block's own props and \`style\`, never a Button component. Rich text
4762
+ is a \`richtext\` prop. Forms are \`create_form\` + a \`form\` block, and form FIELDS are the only
4763
+ place with real \`required\` / \`placeholder\` / \`helpText\` / \`validation\`. Cards, badges, tags,
4764
+ links, lists and media are blocks inside a section, repeatables as one \`table\` prop.
4765
+ Accordions/FAQ: \`builtin:faq-list\` or a \`tabs\` block. NOT MODELLED: cookie banners, modals,
4766
+ breadcrumbs, pagination \u2014 leave them in the site's code and say so at the end.
4767
+ 2. **One component per distinct section**, one-offs included. \`sectionType\` is the family;
4768
+ components sharing one are VARIANTS and must share prop KEYS. A one-off gets its own family
4769
+ plus \`allowedOn: ["slug:<page>"]\`. Start from the \`builtin:*\` blueprints in
4770
+ \`list_components\`. On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\`
4771
+ first (dryRun first, and confirm the plan with the user).
4772
+ 3. **Hierarchy** = \`group\` on create_component/update_component (a NAME; created when missing).
4773
+ Use Global, Navigation, Actions, Content, Forms, Media, Sections, Page-specific. \`category\`
4774
+ is the separate picker TAB.
4775
+ 4. **Fields** = \`props\`: text, richtext, image, url, boolean, number, select, group, table, slot.
4776
+ Always a \`defaultValue\`; \`required\` / \`helpText\` / \`placeholder\` are editor hints, not write
4777
+ -time enforcement. \`select\` for styling-only variants, a separate component when the markup
4778
+ differs. Nesting is capped at two levels \u2014 flatten anything deeper.
4779
+ 5. **Quality.** The componentize lane and \`npx @bettercms-ai/convert --componentize\` move markup
4780
+ VERBATIM \u2014 classes, responsive utilities, aria attributes, alt text all survive. Never
4781
+ restyle. Dry-run first.
4782
+ 6. **Validation** = \`get_site_composition\`: every page's placements, each component's status,
4783
+ and \`verdict.everyPageComposed\` with its reasons. Publishing needs the OWNER's approval in
4784
+ the dashboard (evidence via \`submit_section_manifest\` \u2192 \`submit_section_validation\`), then
4785
+ \`publish_components\`. No tool here can grant that approval, and there is no bulk approve.
4786
+
4787
+ **Order:** get_site_composition \u2192 (get_componentize_plan \u2192 componentize_sections) \u2192
4788
+ create_components \u2192 compose_pages \u2192 update_layout { commands } \u2192 evidence + owner approval \u2192
4789
+ publish_components \u2192 publish the pages / publish_layout \u2192 get_site_composition \u2192 get_next_steps.
4790
+ Prefer the batch tools whenever there is more than three of anything.`;
4408
4791
  var LAYOUT_FLOW = `### Project/page Layout authoring \u2192 \`get_layout\` / \`update_layout\` / \`publish_layout\`
4409
4792
  Edit the project's draft Global Layout or one page's draft override. \`update_layout\` never
4410
4793
  changes the live site; \`publish_layout\` takes the Global Layout draft live (page overrides go
@@ -4491,6 +4874,8 @@ ${FORM_FLOW}
4491
4874
 
4492
4875
  ${COMPONENT_FLOW}
4493
4876
 
4877
+ ${COMPONENTIZE_SITE_FLOW}
4878
+
4494
4879
  ${LAYOUT_FLOW}
4495
4880
 
4496
4881
  ${BUILD_SITE_FLOW}
@@ -4642,6 +5027,36 @@ On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs (re)
4642
5027
  ]
4643
5028
  })
4644
5029
  );
5030
+ server.registerPrompt(
5031
+ "componentize_site",
5032
+ {
5033
+ title: "Componentize the entire website (guided)",
5034
+ description: "Turn every section of a site into a reusable component, organised into groups editors can browse: audit with get_site_composition, create the components in one call, compose every page, then publish what the owner has approved.",
5035
+ argsSchema: {
5036
+ request: z2.string().optional().describe("scope or emphasis, e.g. 'marketing pages only'")
5037
+ }
5038
+ },
5039
+ ({ request }) => ({
5040
+ messages: [
5041
+ {
5042
+ role: "user",
5043
+ content: {
5044
+ type: "text",
5045
+ text: `Componentize this entire website in BetterCMS.${request ? ` Scope: "${request}".` : ""}
5046
+
5047
+ ${COMPONENTIZE_SITE_FLOW}
5048
+
5049
+ ${STRUCTURE_RULE}
5050
+
5051
+ Confirm the component library with the user before creating it. End with the summary \xA712 asks
5052
+ for: the group hierarchy, each family and its variants, what you left in the site's code because
5053
+ the platform does not model it, and which components are still waiting for the owner's approval
5054
+ in the dashboard \u2014 those render as an empty string until they are published.`
5055
+ }
5056
+ }
5057
+ ]
5058
+ })
5059
+ );
4645
5060
  server.registerPrompt(
4646
5061
  "build_site",
4647
5062
  {
@@ -4784,7 +5199,7 @@ function buildServer(deps) {
4784
5199
  // 🔴 The plain-language ask, routed — the stdio twin of the hosted connector's line
4785
5200
  // (src/routes/mcp/index.ts MCP_INSTRUCTIONS). "Make my site editable" has one recipe and
4786
5201
  // one definition of done; without this an agent converts the page it landed on and stops.
4787
- instructions: "When the user asks to make a site or all of its pages editable, to convert it, or to componentize the whole site: this is playbook \xA713. Read `bettercms://playbook/schema` \xA713, call get_binding_report and get_conversion_brief { complete: true }, convert EVERY route the brief lists, and finish only when get_binding_report shows coverage.pending empty on every route \u2014 not when the first page works. On a workspace-wide connection pass projectId on every call; never ask the user to re-scope the connection. BetterCMS never executes a customer's Section renderer or app code. An ordinary MCP connection is not a push runner: explicitly poll list_section_validation_requests, claim one request at an exact git commit, run implementation and responsive checks inside the user's own repository and real app shell, then submit manifest + validation with that requestId and complete it\u2014or truthfully fail it when implementation/evidence is missing. Never invent a manifest, a passing validation, or visual evidence; these tools cannot grant the separate human Visual Approval required for publication."
5202
+ instructions: "When the user asks to make a site or all of its pages editable, to convert it, or to bind its fields: this is playbook \xA713. Read `bettercms://playbook/schema` \xA713, call get_binding_report and get_conversion_brief { complete: true }, convert EVERY route the brief lists, and finish only when get_binding_report shows coverage.pending empty on every route \u2014 not when the first page works. On a workspace-wide connection pass projectId on every call; never ask the user to re-scope the connection. When the user asks to componentize the whole site, to turn every section into a component, or to build a component library from the site: this is playbook \xA712. Read `bettercms://playbook/schema` \xA712, start with get_site_composition, and use the batch tools \u2014 create_components, compose_pages, update_layout with `commands`, publish_components \u2014 rather than one call per component. Finish with get_site_composition and tell the user what the platform does not model (cookie banners, modals, breadcrumbs, pagination) and which components still need the owner's approval in the dashboard before they can be published. BetterCMS never executes a customer's Section renderer or app code. An ordinary MCP connection is not a push runner: explicitly poll list_section_validation_requests, claim one request at an exact git commit, run implementation and responsive checks inside the user's own repository and real app shell, then submit manifest + validation with that requestId and complete it\u2014or truthfully fail it when implementation/evidence is missing. Never invent a manifest, a passing validation, or visual evidence; these tools cannot grant the separate human Visual Approval required for publication."
4788
5203
  }
4789
5204
  );
4790
5205
  server.registerResource(