@bettercms-ai/mcp 0.58.0 → 0.59.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 +35 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2661,7 +2661,7 @@ function buildToolDefs(deps) {
|
|
|
2661
2661
|
"per-type props: heading {text, level}; text/richtext {html} (NOT {text}), and both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only) \u2014 use it for a title that carries a styled span; image {src, alt}; button {text, href}; spacer {height}; video {url}; form {formId}; component {componentId, overrides?}; navbar {links:[{label,href}], logo?, cta?}; footer {columns, copyright?}; section {children: block[]}; columns {columns: block[][], gap} \u2014 a column may NOT hold columns/section/slider/tabs; slider {slides:[{id,children}]}; tabs {tabs:[{id,label,children}]}; collection {cardComponentId?, detailComponentId?, titleField?, excerptField?, limit?, order?, emptyText?} \u2014 lists this page's published entries as cards, and renders ONE entry on /<page>/<entrySlug>"
|
|
2662
2662
|
),
|
|
2663
2663
|
style: z.record(z.string(), z.unknown()).optional().describe(
|
|
2664
|
-
"design tokens: theme, bg (none|surface|muted|accent|dark|custom), bgCustom hex, paddingTop/paddingBottom/paddingSides px, contentWidth (narrow|default|wide|full), align, corner, shadow, borderTop/borderBottom. A real marketing band is a `section` block carrying bg + padding + contentWidth."
|
|
2664
|
+
"design tokens: theme, bg (none|surface|muted|accent|dark|custom), bgCustom hex, paddingTop/paddingBottom/paddingSides px, contentWidth (narrow|default|wide|full), align, corner, shadow, borderTop/borderBottom, border (all four sides, in the text colour). A real marketing band is a `section` block carrying bg + padding + contentWidth."
|
|
2665
2665
|
)
|
|
2666
2666
|
});
|
|
2667
2667
|
const createPageInput = z.object({
|
|
@@ -3021,6 +3021,11 @@ function buildToolDefs(deps) {
|
|
|
3021
3021
|
const getComponentInput = z.object({
|
|
3022
3022
|
componentId: z.string().min(1).describe("component id (from list_components)")
|
|
3023
3023
|
});
|
|
3024
|
+
const duplicateComponentInput = z.object({
|
|
3025
|
+
componentId: z.string().min(1).describe("the look to copy (from list_components)"),
|
|
3026
|
+
name: z.string().min(1).max(255).describe("the new look's name, unique in its family, e.g. 'Ghost'"),
|
|
3027
|
+
preset: z.enum(["primary", "secondary", "outline", "ghost", "link"]).optional().describe("restyle the copy's button (see above); omit for a plain copy")
|
|
3028
|
+
});
|
|
3024
3029
|
const createComponentsInput = z.object({
|
|
3025
3030
|
components: z.array(createComponentInput).min(1).max(50).describe("up to 50 components, each exactly the create_component input")
|
|
3026
3031
|
});
|
|
@@ -3829,14 +3834,18 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
3829
3834
|
def(
|
|
3830
3835
|
"get_componentize_plan",
|
|
3831
3836
|
"Get the plan for turning this site's sections into components",
|
|
3832
|
-
"What this site's SECTIONS would become as components \u2014 a proposal that creates nothing, changes nothing and is computed live on every call. For a site whose pages were DERIVED at import (the site get_conversion_brief describes), each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import already folded (`group-*`). Per page it returns each section's `groupKey`, its `sectionType` family (Hero, FAQ, CTA, Features, Social proof\u2026), its leaf `fields` (key, path, type, the value the CMS holds), a `shapeHash`, and either the component that already renders it (`reuse.componentId`) or the one this plan proposes (`reuse.proposedSlug`) \u2014 and the components themselves under `components`: a NEW one carries the exact `props` and `blockJson` create_component would take, while a row for a component that ALREADY EXISTS carries its `componentId`, `slug` and `name` and no definition, because nothing will be written for it.
|
|
3833
|
-
z.object({
|
|
3834
|
-
|
|
3837
|
+
"What this site's SECTIONS would become as components \u2014 a proposal that creates nothing, changes nothing and is computed live on every call. For a site whose pages were DERIVED at import (the site get_conversion_brief describes), each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import already folded (`group-*`). Per page it returns each section's `groupKey`, its `sectionType` family (Hero, FAQ, CTA, Features, Social proof\u2026), its leaf `fields` (key, path, type, the value the CMS holds), a `shapeHash`, and either the component that already renders it (`reuse.componentId`) or the one this plan proposes (`reuse.proposedSlug`) \u2014 and the components themselves under `components`: a NEW one carries the exact `props` and `blockJson` create_component would take, while a row for a component that ALREADY EXISTS carries its `componentId`, `slug` and `name` and no definition, because nothing will be written for it. Every page gets its OWN components (`home-hero`, `about-hero`), never one shared across pages; a locale copy (`/fr`) reuses its default-locale page's. A COLLECTION TEMPLATE (a dynamic page, `pattern: \"{slug}\"`) is planned ONCE, from one entry, and every prop reads the entry being rendered: only its prose body and lists of object rows are bands, while its frontmatter (author, images, tags, dates) stays the entry's own fields. `pending` says why a group is not offered: `NO_GROUP_ROOT` (the page's field keys are still the derive lane's own \u2014 `h1-welcome`, `p-we-build-things` \u2014 so there is no family to group by; rename them into families first), `NOT_A_SECTION` (a lone scalar with no family, or the page's own metadata \u2014 a section is a group field, a repeater, or a family two or more leaves share, so a legal page of `title`/`metaDescription`/`intro` proposes nothing; `detail: ENTRY_METADATA` is a collection template's frontmatter, `detail: DECLINED` a group somebody declined with componentize_sections { decline: true }), `IMAGE_AS_TEXT` (a text field holding an image path \u2014 it plans itself once the field is an image), `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER` (a repeater THREE deep; TWO levels are expressed exactly \u2014 the group's `table` prop gains a nested `table` sub-field, and each row's nested column is an array of row objects), `PAGE_NOT_EMPTY` (the page holds blocks this lane does not own and will not overwrite). Chrome is NEVER a section: `nav-`/`footer-` keys and everything promoted into the project Layout are edited through the Layout. READ `summary: true` FIRST \u2014 groups, outcomes and slugs, no values (the full plan runs to hundreds of KB). `paged: true` returns 10 pages at a time with a `cursor` for the next; a 409 `PLAN_CHANGED` means the plan moved, start again. The unpaged full read is what `--plan plan.json` takes. Keep the `digest` \u2014 componentize_sections refuses any other.",
|
|
3838
|
+
z.object({
|
|
3839
|
+
summary: z.boolean().optional().describe("true = no values: each section's groupKey, family, pending and component slug. Read this first."),
|
|
3840
|
+
paged: z.boolean().optional().describe("true = 10 pages at a time; the response carries `page`, `pageCount` and a `cursor` for the next."),
|
|
3841
|
+
cursor: z.string().optional().describe("The `cursor` from the previous page. Implies paged. A 409 `PLAN_CHANGED` means start again with no cursor.")
|
|
3842
|
+
}).shape,
|
|
3843
|
+
async (c, a) => ok("Componentize plan.", await data(c, "GET", `/management/projects/current/componentize-plan${q({ summary: a.summary, paged: a.paged, cursor: a.cursor })}`))
|
|
3835
3844
|
),
|
|
3836
3845
|
def(
|
|
3837
3846
|
"componentize_sections",
|
|
3838
3847
|
"Turn this site's derived sections into components",
|
|
3839
|
-
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections and say how many components it will create and which pages it will rewrite. It creates each proposed component as a DRAFT and replaces each page's DRAFT blocks with an ordered list of `component` instances \u2014 one per group, each carrying `props.bind: \"<groupKey>\"`, which points at the page field group that already holds the copy. So nothing is copied and nothing moves: the page keeps its `fields`, its values and its bindings, click-to-edit keeps working and the coverage meter does not change. Pass the plan's `digest`; a 409 `stale-plan` means the site changed since you read that plan, so read it again, show the user what changed and confirm again. Read its `components.wouldDuplicate` before applying: each id is an existing component of the same family this run would sit a NEW one beside, because reuse is by identity and slug, never by family. Running it twice is safe: a group that already has a placement comes back in `sections.pending` as ALREADY_COMPONENTIZED and no second component is created. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages before this reaches a visitor. Then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`. The default `bind` is the above: the words stay in the page's field group. `copy: \"instance\"` moves the words onto each placement and marks the page's fields `origin: \"componentized\"` \u2014 kept, never deleted (see `copy`). Every component it CREATES is filed into a Component Group (the folders of the dashboard's Components tab): the one you name in `group` (found or created), else its section family \u2014 nav, header, footer or menu \u2192 Layout, an unnamed section \u2192 Sections, otherwise the family name (Hero, FAQ). A component it reuses keeps the Group it has; the receipt's `components.groups` lists the Groups it filed into.",
|
|
3848
|
+
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections and say how many components it will create and which pages it will rewrite. It creates each proposed component as a DRAFT and replaces each page's DRAFT blocks with an ordered list of `component` instances \u2014 one per group, each carrying `props.bind: \"<groupKey>\"`, which points at the page field group that already holds the copy. So nothing is copied and nothing moves: the page keeps its `fields`, its values and its bindings, click-to-edit keeps working and the coverage meter does not change. Pass the plan's `digest`; a 409 `stale-plan` means the site changed since you read that plan, so read it again, show the user what changed and confirm again. Read its `components.wouldDuplicate` before applying: each id is an existing component of the same family this run would sit a NEW one beside, because reuse is by identity and slug, never by family. Running it twice is safe: a group that already has a placement comes back in `sections.pending` as ALREADY_COMPONENTIZED and no second component is created. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages before this reaches a visitor. Then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`. The default `bind` is the above: the words stay in the page's field group. `copy: \"instance\"` moves the words onto each placement and marks the page's fields `origin: \"componentized\"` \u2014 kept, never deleted (see `copy`). Every component it CREATES is filed into a Component Group (the folders of the dashboard's Components tab): the one you name in `group` (found or created), else its section family \u2014 nav, header, footer or menu \u2192 Layout, an unnamed section \u2192 Sections, otherwise the family name (Hero, FAQ). A component it reuses keeps the Group it has; the receipt's `components.groups` lists the Groups it filed into. When the user says a proposed group is NOT a section, pass `decline: true` with those `sections` (and `pageIds`): nothing is componentized, the plan reports them `NOT_A_SECTION` / `DECLINED` and stops proposing them, and a plan that proposes nothing that was not declined is done; `decline: false` offers them again.",
|
|
3840
3849
|
z.object({
|
|
3841
3850
|
digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
|
|
3842
3851
|
pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
|
|
@@ -3844,9 +3853,10 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
3844
3853
|
sections: z.array(z.string().min(1)).optional().describe("Act only on these section groupKeys (from the plan). Every other section on the page keeps the placement it already has."),
|
|
3845
3854
|
copy: z.enum(["bind", "instance"]).optional().describe("Who owns each section's copy. 'bind' (default) leaves it in the page fields. 'instance' is the page-builder model: each placement takes that group's CURRENT draft values into its own `props.overrides` (a repeater becomes one table prop holding every row), records which page path each came from in `props.source`, and drops `props.bind`; the page's fields are KEPT and marked `origin: \"componentized\"`, so nothing is lost and the editor stops showing a second place to type the same words."),
|
|
3846
3855
|
dryRun: z.boolean().optional().describe("true = return the receipt without writing anything. Do this first."),
|
|
3847
|
-
group: z.string().min(1).max(100).optional().describe("File the components this run creates into this Component Group (by NAME; created when missing). Omit to file each under its section family.")
|
|
3856
|
+
group: z.string().min(1).max(100).optional().describe("File the components this run creates into this Component Group (by NAME; created when missing). Omit to file each under its section family."),
|
|
3857
|
+
decline: z.boolean().optional().describe("true = record `sections` as NOT sections (nothing is componentized; the plan stops proposing them). false = offer them again. Needs `sections`.")
|
|
3848
3858
|
}).shape,
|
|
3849
|
-
async (c, a) => ok("Componentized the sections.", await data(c, "POST", `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, pages: a.pages, sections: a.sections, copy: a.copy, dryRun: a.dryRun, group: a.group }))
|
|
3859
|
+
async (c, a) => ok("Componentized the sections.", await data(c, "POST", `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, pages: a.pages, sections: a.sections, copy: a.copy, dryRun: a.dryRun, group: a.group, decline: a.decline }))
|
|
3850
3860
|
),
|
|
3851
3861
|
def(
|
|
3852
3862
|
"get_analytics_overview",
|
|
@@ -5016,6 +5026,24 @@ ${lines.join("\n")}`, found);
|
|
|
5016
5026
|
})
|
|
5017
5027
|
)
|
|
5018
5028
|
},
|
|
5029
|
+
{
|
|
5030
|
+
name: "duplicate_component",
|
|
5031
|
+
config: {
|
|
5032
|
+
title: "Add a look to a component family",
|
|
5033
|
+
description: "Add a new LOOK to a component's variant family (\"+ New variant\"), e.g. a Ghost button the site does not have: copy the component into the same family and folder under `name`, with a new unique slug. `preset` restyles its button block: primary / secondary fill it with the brand kit's primary / accent colour; ghost drops the fill and border, keeps the padding and colours the text with the old fill; link is a ghost with no padding. Omit `preset` for a plain copy. outline drops the fill, keeps the padding and draws a border all round in the old fill's colour, which becomes the text colour. A look the style vocabulary cannot express is refused with 422 and the reason \u2014 relay it, never approximate it with update_component. 409 means that name is already a look in the family. When the family's contract is published and this connection may publish, the new look is published at once and is choosable in every section's style select; otherwise it is a draft: call publish_component.",
|
|
5034
|
+
inputSchema: duplicateComponentInput.shape
|
|
5035
|
+
},
|
|
5036
|
+
handler: guard(
|
|
5037
|
+
async (args) => withClient(async (client) => {
|
|
5038
|
+
const { componentId, ...body } = args;
|
|
5039
|
+
const res = await client.fetchJSON(
|
|
5040
|
+
client.url(`/management/components/${encodeURIComponent(componentId)}/duplicate`),
|
|
5041
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
5042
|
+
);
|
|
5043
|
+
return ok(`Added look '${res.data.name}' (id ${res.data.id}, ${res.data.status}).`, res.data);
|
|
5044
|
+
})
|
|
5045
|
+
)
|
|
5046
|
+
},
|
|
5019
5047
|
// ── AI content + SEO actions (Option B) ────────────────────────────────────
|
|
5020
5048
|
{
|
|
5021
5049
|
name: "write_content",
|