@bettercms-ai/mcp 0.83.0 → 0.83.2

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 +38 -9
  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/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.",
@@ -3047,6 +3052,7 @@ var PARAM_TEXT = {
3047
3052
  editorSetupCheck: "true = only report the state; starts nothing",
3048
3053
  waitSeconds: "wait up to N seconds (max 50) for it to finish; call again while it runs",
3049
3054
  editorSetupExempt: "instead of running: record setup-gate exceptions [{ route, check, reason, note }]; reason null removes one. Reasons: get_next_steps `receipt.gate`",
3055
+ pageBodySchema: `the body's allowed blocks {allowed:[{kind}]}: source (pageId, "self"), component (id), family (sectionType), builtins, workspace; null = any`,
3050
3056
  editorSetupDeclare: "instead of running: record binding decisions [{op, id, ...}], max 100, a result each; with `check: true` a dry run. Ops: skill bindings",
3051
3057
  pageBlockJson: "an ARRAY REPLACES the page's block composition (a `component` block per section). An OBJECT edits ONE section of the stored page instead: `insertSection` adds a component (its id, or a builtin:* id: copied into the project once) at index `at`, or `before` / `after` a section's block id (none = last), with its `style` and `overrides`; `sectionStyle` sets a section's block style (a key replaces the stored one, null unsets it). `section` in the answer names the block (skill publishing)",
3052
3058
  pageDynamic: "a dynamic (collection template) page only",
@@ -3710,10 +3716,15 @@ var HOSTING_TEXT = {
3710
3716
  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
3717
  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
3718
  };
3713
- async function builtinCopyId(client, builtinId) {
3719
+ async function builtinCopyId(client, builtinId, verdict) {
3714
3720
  const listed = await client.fetchJSON(client.url(`/management/components?select=summary`));
3715
3721
  const copy = (listed.data ?? []).find((row) => row.sourceKey === builtinId && !row.builtin);
3716
3722
  if (copy) return copy.id;
3723
+ try {
3724
+ await verdict();
3725
+ } catch (err) {
3726
+ if (apiError(err)?.bodyCode !== "BUILTIN_NOT_COPIED") throw err;
3727
+ }
3717
3728
  const made = await client.fetchJSON(client.url(`/management/components`), { method: "POST", body: JSON.stringify({ from: builtinId }) });
3718
3729
  return made.data.id;
3719
3730
  }
@@ -4847,7 +4858,7 @@ ${d.outline}` : "Proposed Content structure.", d);
4847
4858
  def(
4848
4859
  "submit_conversion_receipt",
4849
4860
  "Record what the conversion codemod could and could not do",
4850
- "Hand BetterCMS the codemod's own account of a run, so the meter can say WHY a path is undeclared. Submit the receipt `npx @bettercms-ai/convert --receipt out.json` wrote as `{ briefDigest, receipt }`, for the SAME `briefDigest` get_conversion_brief { complete: true } returned (its shape: see `receipt`). \u{1F534} A RECEIPT WITH PENDING PATHS IS A PROGRESS REPORT, NOT A FINISH LINE: the response answers `complete` and `pendingTotal`, and echoes the first 40 pending rows with their `fix` (`action` one of `wrap-span`, `declare-attr`, `bind-expression`, `declare-richtext`, `bind-data`, `manual`; the row shape: skill bindings; a 404 `unknown-brief` means that digest was never issued here). Apply every fix, rerun, resubmit: only `complete: true` ends a conversion \u2014 never report a site converted on less. It is a RECORD, not a release: nothing on the site changes; the meter reads it on the get_binding_report after the next deploy. \u{1F534} The same run owes two more things (playbook \xA713 steps 2b and 3b): the `--forms` receipt (a SECOND call, same `briefDigest`) and the `styleEdits` of `convert --verify-styles` inside this receipt; any changed class or style is refused with 409 `style-edits`. Requires artifact:write.",
4861
+ "Hand BetterCMS the codemod's own account of a run, so the meter can say WHY a path is undeclared. Submit the receipt `npx @bettercms-ai/convert --receipt out.json` wrote as `{ briefDigest, receipt }`, for the SAME `briefDigest` get_conversion_brief { complete: true } returned (its shape: see `receipt`). \u{1F534} A RECEIPT WITH PENDING PATHS IS A PROGRESS REPORT, NOT A FINISH LINE: the response answers `complete` and `pendingTotal`, and echoes the first 40 pending rows with their `fix` (`action` one of `wrap-span`, `declare-attr`, `bind-expression`, `declare-richtext`, `bind-data`, `manual`; the row shape: skill bindings; a 404 `unknown-brief` means that digest was never issued here). Apply every fix, rerun, resubmit: only `complete: true` ends a conversion \u2014 never report a site converted on less. Apply what you can; the site stays text-match for the rest and flips to declared only when nothing is left for the annotator (never set `declaredBindings` by hand). It is a RECORD, not a release: nothing on the site changes; the meter reads it on the get_binding_report after the next deploy. \u{1F534} The same run owes two more things (playbook \xA713 steps 2b and 3b): the `--forms` receipt (a SECOND call, same `briefDigest`) and the `styleEdits` of `convert --verify-styles` inside this receipt; any changed class or style is refused with 409 `style-edits`. Requires artifact:write.",
4851
4862
  z.object({
4852
4863
  briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
4853
4864
  receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim: `paths: { declared, rewritten, alreadyDeclared, pending[{ route, scope, path, kind, file, reason, message, fix }] }` plus `styleEdits`. `paths.declared` must equal rewritten + alreadyDeclared + pending.length; each pending `reason` is the converter's own (IN_EXPRESSION, AMBIGUOUS_LITERAL, REPEATER_FIXED_LENGTH, PARSE_ERROR, \u2026); a path with no row reads `not-declared`.")
@@ -4958,17 +4969,20 @@ ${d.outline}` : "Proposed Content structure.", d);
4958
4969
  "update_page",
4959
4970
  "Edit a page",
4960
4971
  "Edit a page: title, slug, SEO metaTitle/metaDescription, structured data (`schemaType`, `schema`), publish status (draft|published), and `blockJson` (its block composition \u2014 passing an array REPLACES the whole array, so read get_page first and send its `optimisticVersion` as `ifMatch`; an object adds or styles ONE section). An answer that starts with `held` is a draft the live page will NOT show until each of `holds` has its `action` taken, and a publish answers with `outcome`: `partial` lists the values not live. Never report either as done (skill publishing). `warnings`: COMPONENT_SWAP_DROPS_OVERRIDES (skill components), ENTRY_STILL_DRAFT and COMPONENT_DRAFT (skill publishing). It does NOT change the field SCHEMA \u2014 use add_field (pageId) / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call, and a singleton's content entry with them. STRUCTURED DATA is native SEO, never a field: Automatic JSON-LD from the content, unless `schemaType` picks a schema.org type or pasted `schema` (wins over both, null clears it) is set (skill seo). " + PAGE_HEAD_NOTE + " " + SECTION_DOCTRINE,
4961
- z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1).describe(PARAM_TEXT.pageRef), title: z.string().optional(), slug: z.string().optional(), blockJson: z.union([z.array(blockObject), z.object({ insertSection: z.object({ componentId: z.string().min(1), at: z.number().int().min(0).optional(), before: z.string().optional(), after: z.string().optional(), style: z.record(z.string(), z.unknown()).optional(), overrides: z.record(z.string(), z.unknown()).optional() }).strict().optional(), sectionStyle: z.object({ blockId: z.string().min(1), style: z.record(z.string(), z.unknown()) }).strict().optional() }).strict()]).optional().describe(PARAM_TEXT.pageBlockJson), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", ...SCHEMA_ORG_TYPES]).optional().describe("structured data type; 'auto' (the default) derives it from the content"), schema: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).nullable().optional().describe("custom JSON-LD; wins over schemaType; null clears it"), status: z.enum(["draft", "published"]).optional(), scheduledAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageScheduledAt), unpublishAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageUnpublishAt), locale: z.string().nullable().optional().describe(PARAM_TEXT.locale), translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf), ifMatch: z.number().int().min(0).optional().describe(UPDATE_PAGE_IF_MATCH_NOTE), dynamic: z.object({ previewPath: z.string().nullable().optional().describe(PARAM_TEXT.previewPath), modelId: z.string().min(1).optional().describe(PARAM_TEXT.pageModelId) }).optional().describe(PARAM_TEXT.pageDynamic) }).shape,
4972
+ z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1).describe(PARAM_TEXT.pageRef), title: z.string().optional(), slug: z.string().optional(), blockJson: z.union([z.array(blockObject), z.object({ insertSection: z.object({ componentId: z.string().min(1), at: z.number().int().min(0).optional(), before: z.string().optional(), after: z.string().optional(), style: z.record(z.string(), z.unknown()).optional(), overrides: z.record(z.string(), z.unknown()).optional() }).strict().optional(), sectionStyle: z.object({ blockId: z.string().min(1), style: z.record(z.string(), z.unknown()) }).strict().optional(), bodySchema: z.record(z.string(), z.unknown()).nullable().optional().describe(PARAM_TEXT.pageBodySchema) }).strict()]).optional().describe(PARAM_TEXT.pageBlockJson), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", ...SCHEMA_ORG_TYPES]).optional().describe("structured data type; 'auto' (the default) derives it from the content"), schema: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).nullable().optional().describe("custom JSON-LD; wins over schemaType; null clears it"), status: z.enum(["draft", "published"]).optional(), scheduledAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageScheduledAt), unpublishAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageUnpublishAt), locale: z.string().nullable().optional().describe(PARAM_TEXT.locale), translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf), ifMatch: z.number().int().min(0).optional().describe(UPDATE_PAGE_IF_MATCH_NOTE), dynamic: z.object({ previewPath: z.string().nullable().optional().describe(PARAM_TEXT.previewPath), modelId: z.string().min(1).optional().describe(PARAM_TEXT.pageModelId) }).optional().describe(PARAM_TEXT.pageDynamic) }).shape,
4962
4973
  async (c, a) => {
4963
4974
  const edit = Array.isArray(a.blockJson) ? void 0 : a.blockJson;
4964
4975
  const insert = edit?.insertSection;
4965
- const insertSection = insert?.componentId.startsWith("builtin:") ? { ...insert, componentId: await builtinCopyId(c, insert.componentId) } : insert;
4976
+ const pageBody = (sectionInsert) => ({ title: a.title, slug: a.slug, blockJson: Array.isArray(a.blockJson) ? a.blockJson : void 0, insertSection: sectionInsert, sectionStyle: edit?.sectionStyle, bodySchema: edit?.bodySchema, 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 });
4977
+ const ifMatchHeaders = a.ifMatch !== void 0 ? { headers: { "If-Match": `W/"${a.ifMatch}"` } } : {};
4978
+ const verdict = () => c.fetchJSON(c.url(`/management/pages/${s(a.pageId)}/meta`), { method: "PATCH", ...ifMatchHeaders, body: JSON.stringify({ ...pageBody(insert), dryRun: true }) });
4979
+ const insertSection = insert?.componentId.startsWith("builtin:") ? { ...insert, componentId: await builtinCopyId(c, insert.componentId, verdict) } : insert;
4966
4980
  const res = await c.fetchJSON(
4967
4981
  c.url(`/management/pages/${s(a.pageId)}/meta`),
4968
4982
  {
4969
4983
  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 })
4984
+ ...ifMatchHeaders,
4985
+ body: JSON.stringify(pageBody(insertSection))
4972
4986
  }
4973
4987
  );
4974
4988
  const note = res.redirectNote;
@@ -5512,7 +5526,7 @@ ${d.outline}` : "Proposed Content structure.", d);
5512
5526
  def(
5513
5527
  "get_binding_report",
5514
5528
  "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.",
5529
+ "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
5530
  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
5531
  async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
5518
5532
  ),
@@ -5783,7 +5797,7 @@ ${d.outline}` : "Proposed Content structure.", d);
5783
5797
  def(
5784
5798
  "get_deploy_status",
5785
5799
  "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.",
5800
+ "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
5801
  z.object({ waitSeconds: z.number().optional().describe(PARAM_TEXT.waitSeconds) }).shape,
5788
5802
  // Flat body, no `data` envelope: `data()` read it as undefined.
5789
5803
  async (c, a) => ok("Deploy status.", await c.fetchJSON(c.url(withWait(`/management/projects/deploy-status`, a.waitSeconds)), { method: "GET" }))
@@ -5811,6 +5825,21 @@ ${d.outline}` : "Proposed Content structure.", d);
5811
5825
  // `declare` records binding decisions; with `check: true` it is a dry run (the body's `dryRun`).
5812
5826
  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
5827
  ),
5828
+ // Ask, don't guess: the plan, and its answers through editor_setup's declare route.
5829
+ def(
5830
+ "get_editable_plan",
5831
+ "Get the owner's open questions",
5832
+ NEW_TOOL_TEXT.get_editable_plan,
5833
+ z.object({}).shape,
5834
+ async (c) => ok("Editable plan.", await c.fetchJSON(c.url(`/management/projects/editable-plan`), { method: "GET" }))
5835
+ ),
5836
+ def(
5837
+ "confirm_editable_plan",
5838
+ "Record the owner's answers",
5839
+ NEW_TOOL_TEXT.confirm_editable_plan,
5840
+ z.object({ decisions: z.array(z.record(z.string(), z.unknown())), check: z.boolean().optional() }).shape,
5841
+ 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 } : {} }) }))
5842
+ ),
5814
5843
  // ── Deploy to the user's own host (parity with remote /mcp) ──
5815
5844
  // A person connects Vercel or Netlify in the dashboard; nothing here touches OAuth or a host
5816
5845
  // token. deploy_to_host takes artifact:write like deploy_project. @see management/deploy-targets.ts
@@ -6339,7 +6368,7 @@ ${notes.join("\n")}` : summary, created);
6339
6368
  name: "publish_layout",
6340
6369
  config: {
6341
6370
  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.",
6371
+ 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
6372
  inputSchema: publishLayoutInput.shape
6344
6373
  },
6345
6374
  handler: guard(async (args) => withClient(async (client) => {