@bettercms-ai/mcp 0.66.1 → 0.66.3
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 +37 -26
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/references/bindings.md +3 -3
- package/dist/skills/bettercms/references/content-modeling.md +2 -1
- package/dist/skills/bettercms/references/localization.md +5 -5
- package/dist/skills/bettercms/references/publishing.md +12 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2277,7 +2277,7 @@ var STRUCTURE_EXAMPLE_PAYLOAD = {
|
|
|
2277
2277
|
doc: {
|
|
2278
2278
|
schema: 1,
|
|
2279
2279
|
folders: [
|
|
2280
|
-
{ id: "home:pages", parentId: null, name: "
|
|
2280
|
+
{ id: "home:pages", parentId: null, name: "Pages", icon: "documents", sort: 1e3 },
|
|
2281
2281
|
{ id: "home:collections", parentId: null, name: "Shared", icon: null, sort: 1001 },
|
|
2282
2282
|
{ id: "fld_std_blog", parentId: null, name: "Blog posts", icon: "document-text", sort: 0 },
|
|
2283
2283
|
{ id: "fld_std_products", parentId: null, name: "Products", icon: "basket", sort: 1 }
|
|
@@ -2327,18 +2327,20 @@ ever changes.
|
|
|
2327
2327
|
The family's main collection comes first, then the others in the order its fields reference
|
|
2328
2328
|
them.
|
|
2329
2329
|
4. **Family order:** the site's navigation order; failing that, entry count, then name.
|
|
2330
|
-
5. **
|
|
2331
|
-
("
|
|
2330
|
+
5. **Pages.** EVERY remaining standalone page goes in the system folder \`home:pages\`
|
|
2331
|
+
("Pages") \u2014 no standalone page ever gets a top-level folder. Code-only routes (a 404, a
|
|
2332
2332
|
page built by code with nothing to edit) stay there too, marked with the \`file-code\` icon.
|
|
2333
2333
|
Pages that share a route (/legal/terms, /legal/privacy, /docs/...) get a subfolder of it.
|
|
2334
|
+
A site in more than one language files every page, home included, under Pages \u203A <language>
|
|
2335
|
+
(named in itself: English, Fran\xE7ais, Deutsch), default language first.
|
|
2334
2336
|
6. **Shared taxonomies.** A vocabulary several families reference (Tags used by posts AND
|
|
2335
2337
|
products) goes in the system folder \`home:collections\`, named "Shared". So does supporting
|
|
2336
2338
|
content no family owns (testimonials, FAQs with no list page).
|
|
2337
2339
|
7. **Settings closes the sidebar.** \`home:globals\` ("Settings") holds NO pages or collections \u2014
|
|
2338
2340
|
navigation, header, footer, site settings and SEO defaults open from it in the dashboard. The
|
|
2339
|
-
three system folders always sort BELOW the folders you make: \u2026
|
|
2341
|
+
three system folders always sort BELOW the folders you make: \u2026 Pages, Shared, Settings.
|
|
2340
2342
|
8. **Icons** (kit names, bettercms://icons/dock): \`document-text\` for posts, \`basket\` for products,
|
|
2341
|
-
\`book\` for docs, \`documents\` for
|
|
2343
|
+
\`book\` for docs, \`documents\` for Pages, \`folder\` by default. Lucide names already stored
|
|
2342
2344
|
still draw.
|
|
2343
2345
|
9. **Depth:** nest at most 2 levels by default. The hard limit is 4; do not use it without a
|
|
2344
2346
|
reason the user gave you.
|
|
@@ -2354,15 +2356,15 @@ ever changes.
|
|
|
2354
2356
|
|
|
2355
2357
|
What the structure writes refuse, and the code each refusal carries:
|
|
2356
2358
|
|
|
2357
|
-
- **System folders.** \`home:pages\` (
|
|
2359
|
+
- **System folders.** \`home:pages\` (Pages), \`home:collections\` (Shared) and
|
|
2358
2360
|
\`home:globals\` (Settings) always exist and can never be moved or deleted (400
|
|
2359
2361
|
\`NAV_SYSTEM_FOLDER\`); \`rename_folder\` and \`set_icon\` work on them.
|
|
2360
|
-
- **Type purity** (400 \`NAV_TYPE_MISMATCH\`):
|
|
2362
|
+
- **Type purity** (400 \`NAV_TYPE_MISMATCH\`): Pages holds only pages at any depth, Shared
|
|
2361
2363
|
only collections, Settings neither \u2014 globals are not sidebar items.
|
|
2362
2364
|
- **Pins** sit above the folders in their own order, pages or collections. In a document a pin is
|
|
2363
2365
|
\`folderId: "home:root"\`, a virtual id with no folder record; a folder record with that id is
|
|
2364
2366
|
400 \`NAV_SYSTEM_FOLDER\`. In \`move_to_folder\`, \`folderId: 'root'\` pins and \`null\` returns
|
|
2365
|
-
the item to its home (
|
|
2367
|
+
the item to its home (Pages / Shared).
|
|
2366
2368
|
- **Kind.** \`move_to_folder\` and \`set_icon\` VERIFY what an id is: a page id sent as a
|
|
2367
2369
|
collection is 400 \`NAV_KIND_MISMATCH\` naming the kind to use. \`set_content_structure\` never
|
|
2368
2370
|
refuses a kind mismatch: it returns \`warnings\` and \`kindMismatches\` instead.
|
|
@@ -2407,7 +2409,7 @@ collection (\`cm_posts\`), a /shop list page (\`pg_shop\`, dynamic) for Products
|
|
|
2407
2409
|
Blog
|
|
2408
2410
|
[folder] Products icon=shopping-bag
|
|
2409
2411
|
Products, Product categories, Colours (referenced only by Products)
|
|
2410
|
-
[system folder]
|
|
2412
|
+
[system folder] Pages icon=files
|
|
2411
2413
|
About, Contact, Not found (icon=file-code)
|
|
2412
2414
|
[system folder] Shared
|
|
2413
2415
|
Tags (referenced by Blog AND Products: shared)
|
|
@@ -2518,6 +2520,14 @@ function mergeConfig(current, patch) {
|
|
|
2518
2520
|
}
|
|
2519
2521
|
return out;
|
|
2520
2522
|
}
|
|
2523
|
+
var wordsOf = (v) => (typeof v === "string" ? v : typeof v?.label === "string" ? v.label : "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
2524
|
+
function helpTextEcho(helpText, field) {
|
|
2525
|
+
if (typeof helpText !== "string" || !helpText.trim()) return null;
|
|
2526
|
+
const said = wordsOf(helpText.replace(/^\s*e\.?g\.?[:,]?\s*/i, "")).replace(/^["'“”‘’]+|["'“”‘’.…]+$/g, "");
|
|
2527
|
+
const value = wordsOf(field.defaultValue);
|
|
2528
|
+
const echoes = said === wordsOf(field.label) || value !== "" && (said === value || /…["”]?$/.test(helpText.trim()) && value.startsWith(said));
|
|
2529
|
+
return echoes ? `helpText repeats the field's label or value, which the editor already shows. Say what belongs here or its format in under 60 characters ("Whole number, no currency symbol."), or send null for none.` : null;
|
|
2530
|
+
}
|
|
2521
2531
|
function planFieldEdit(model, edit, callerIfMatch) {
|
|
2522
2532
|
const fields = model.fields ?? [];
|
|
2523
2533
|
const keys = typeof edit.path === "string" && edit.path ? edit.path.split(".") : [];
|
|
@@ -2540,6 +2550,8 @@ function planFieldEdit(model, edit, callerIfMatch) {
|
|
|
2540
2550
|
if (!retype && rename === void 0 && edit.config === void 0 && changes.length === 0) {
|
|
2541
2551
|
return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, ui, placement, type or key." };
|
|
2542
2552
|
}
|
|
2553
|
+
const echo = helpTextEcho(edit.helpText, { label: edit.label ?? current.label, defaultValue: current.defaultValue });
|
|
2554
|
+
if (echo) return { ok: false, error: echo };
|
|
2543
2555
|
const next = patchAt(fields, keys, (f) => {
|
|
2544
2556
|
let out = { ...f };
|
|
2545
2557
|
for (const k of changes) {
|
|
@@ -2737,6 +2749,7 @@ function entryListSearch(args) {
|
|
|
2737
2749
|
for (const key of ["modelId", "pageId", "status"]) if (args[key]) q.set(key, String(args[key]));
|
|
2738
2750
|
for (const [k, v] of whereToQuery(args.where)) q.append(k, v);
|
|
2739
2751
|
for (const key of ["sort", "page", "limit"]) if (args[key] !== void 0 && args[key] !== null) q.set(key, String(args[key]));
|
|
2752
|
+
if (args.detail !== true) q.set("select", "summary");
|
|
2740
2753
|
const s = q.toString();
|
|
2741
2754
|
return s ? `?${s}` : "";
|
|
2742
2755
|
}
|
|
@@ -3128,6 +3141,7 @@ var ENTRY_MERGE_NOTE = "true = keep the fields you leave out: the entry is read,
|
|
|
3128
3141
|
var ENTRY_IF_MATCH_NOTE = "optimisticVersion from get_content_entry: the write applies only at that version, and a 409 means it changed since: read it again and re-apply. With merge it defaults to the version merge read.";
|
|
3129
3142
|
var ENTRY_SCHEDULED_AT_NOTE = "ISO datetime to publish this draft at (needs publish permission); null cancels. An edit that omits scheduledAt CANCELS a pending publish schedule, so send it again with every edit made before it fires.";
|
|
3130
3143
|
var ENTRY_UNPUBLISH_AT_NOTE = "ISO datetime at which the published entry returns to draft; null cancels. Omitting it never cancels it.";
|
|
3144
|
+
var ENTRY_LIST_DETAIL_NOTE = "true = full rows with every field value and the published copy. Large: read one entry with get_content_entry instead.";
|
|
3131
3145
|
var ENTRY_LIST_LIMIT_NOTE = "entries per page, 1-200; 50 when omitted. Read `meta.totalPages` and ask for the next `page`.";
|
|
3132
3146
|
var PAGE_HEAD_NOTE = "noindex, canonical, og and twitter set the page's head extras; each MERGES into what is stored (a key you leave out is kept, null or an empty string clears it).";
|
|
3133
3147
|
var STRUCTURE_FENCE = "ORGANISATION ONLY: Content sidebar folders never change a URL, slug, content or publish state, and nothing is rebuilt or delivered; URL folders (page folders) are set in the dashboard. Read the resource bettercms://playbook/structure before you organise anything: it holds the rules this refuses on \u2014 system folders, type purity, the Admin/Developer access check, root pins and page-bound models.";
|
|
@@ -3412,7 +3426,8 @@ function buildToolDefs(deps) {
|
|
|
3412
3426
|
where: z.record(z.string(), z.unknown()).optional().describe(PARAM_TEXT.where),
|
|
3413
3427
|
sort: z.string().max(100).optional().describe(PARAM_TEXT.sort),
|
|
3414
3428
|
page: z.number().int().min(1).optional().describe(PARAM_TEXT.page),
|
|
3415
|
-
limit: z.number().int().min(1).max(200).optional().describe(ENTRY_LIST_LIMIT_NOTE)
|
|
3429
|
+
limit: z.number().int().min(1).max(200).optional().describe(ENTRY_LIST_LIMIT_NOTE),
|
|
3430
|
+
detail: z.boolean().optional().describe(ENTRY_LIST_DETAIL_NOTE)
|
|
3416
3431
|
});
|
|
3417
3432
|
const clearableText = (max) => z.string().max(max).nullable().optional();
|
|
3418
3433
|
const headPatchShape = {
|
|
@@ -3830,7 +3845,7 @@ ${d.summary.outline}` : "Content structure.", d);
|
|
|
3830
3845
|
def(
|
|
3831
3846
|
"suggest_content_structure",
|
|
3832
3847
|
"Propose the standard Content structure",
|
|
3833
|
-
`READ-ONLY: propose the standard Content sidebar structure for the connected project (${STRUCTURE_PLAYBOOK_URI}) and write NOTHING. Runs the standard over the project's pages, collections, references and built routes: the home page and each collection's list page pinned, one folder per content family named for its detail type (Blog posts, Case studies, Products with its taxonomies and an Attributes subfolder), ordered by entry count (or the caller's nav order), shared taxonomies in Shared, every remaining page in
|
|
3848
|
+
`READ-ONLY: propose the standard Content sidebar structure for the connected project (${STRUCTURE_PLAYBOOK_URI}) and write NOTHING. Runs the standard over the project's pages, collections, references and built routes: the home page and each collection's list page pinned, one folder per content family named for its detail type (Blog posts, Case studies, Products with its taxonomies and an Attributes subfolder), ordered by entry count (or the caller's nav order), shared taxonomies in Shared, every remaining page in Pages, at most 2 levels deep. Page-bound models are never filed. Returns { doc, version, hasDocument, outline, rationale }. To apply it: show the user the outline, then call set_content_structure with this doc and this version (the If-Match; a 412 means someone changed the sidebar, so suggest again). When hasDocument is true the project already has a structure somebody made: never replace it without the user's yes; file only what is new with move_to_folder instead.`,
|
|
3834
3849
|
z.object({
|
|
3835
3850
|
maxDepth: z.number().int().min(1).max(4).optional().describe("folder nesting to propose; default 2 (the standard), 4 is the hard limit")
|
|
3836
3851
|
}).shape,
|
|
@@ -3857,7 +3872,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3857
3872
|
items: z.array(z.object({
|
|
3858
3873
|
kind: z.enum(["page", "collection"]),
|
|
3859
3874
|
id: z.string().min(1).describe("page id or collection (content model) id"),
|
|
3860
|
-
folderId: z.string().nullable().describe('folder id; "home:root" = pinned at the top level; null = its home (
|
|
3875
|
+
folderId: z.string().nullable().describe('folder id; "home:root" = pinned at the top level; null = its home (Pages for a page, the Shared list for a collection)'),
|
|
3861
3876
|
sort: z.number(),
|
|
3862
3877
|
icon: z.string().nullable()
|
|
3863
3878
|
}))
|
|
@@ -3890,13 +3905,13 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3890
3905
|
def(
|
|
3891
3906
|
"move_to_folder",
|
|
3892
3907
|
"Move into a Content sidebar folder",
|
|
3893
|
-
`Move a page, a collection or a folder into a folder. folderId null moves it back to its home:
|
|
3908
|
+
`Move a page, a collection or a folder into a folder. folderId null moves it back to its home: Pages for a page, the Shared list for a collection, the top level for a folder. It lands after what is already there. Moving a folder takes its contents with it and is refused if the result would nest deeper than 4 levels or put a folder inside itself. ${STRUCTURE_POINTER}`,
|
|
3894
3909
|
z.object({
|
|
3895
3910
|
kind: z.enum(["page", "collection", "folder"]).describe(
|
|
3896
3911
|
"what the id IS on this branch \u2014 VERIFIED, not assumed: an id is either a page or a collection, never both, and a mismatch is refused with 400 NAV_KIND_MISMATCH naming the kind to use. Call it again with that kind, which also clears the wrong-kind entry."
|
|
3897
3912
|
),
|
|
3898
3913
|
id: z.string().min(1).describe("the page id, collection (content model) id, or folder id"),
|
|
3899
|
-
folderId: z.string().min(1).nullable().describe("target folder id; 'root' pins a page or collection at the top level (for a folder, 'root' is the top level); null moves it back to its home (
|
|
3914
|
+
folderId: z.string().min(1).nullable().describe("target folder id; 'root' pins a page or collection at the top level (for a folder, 'root' is the top level); null moves it back to its home (Pages, the Shared list, or the top level for a folder)")
|
|
3900
3915
|
}).shape,
|
|
3901
3916
|
async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/move`, { kind: a.kind, id: a.id, folderId: a.folderId }))
|
|
3902
3917
|
),
|
|
@@ -5217,20 +5232,11 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5217
5232
|
name: "list_content_entries",
|
|
5218
5233
|
config: {
|
|
5219
5234
|
title: "List content entries (incl. drafts)",
|
|
5220
|
-
description: "List content entries \u2014 including drafts \u2014 filtered by model and/or page, by field values (`where`), sorted and paged. Use it to SEE existing content before editing. For a singleton page, pass its pageId to get its single entry. A project-scoped connection sees its own project's entries plus workspace-level ones, which it can read but not change. Returns `limit` entries per `page` (50 by default); the summary says which page of how many.",
|
|
5235
|
+
description: "List content entries \u2014 including drafts \u2014 filtered by model and/or page, by field values (`where`), sorted and paged. Use it to SEE existing content before editing. For a singleton page, pass its pageId to get its single entry. A project-scoped connection sees its own project's entries plus workspace-level ones, which it can read but not change. Returns `limit` entries per `page` (50 by default); the summary says which page of how many. Each entry is a SUMMARY (id, slug, title, status, model, page, dates, optimisticVersion) \u2014 read values with get_content_entry, or pass `detail: true` for full rows.",
|
|
5221
5236
|
inputSchema: listEntriesInput.shape
|
|
5222
5237
|
},
|
|
5223
5238
|
handler: guard(
|
|
5224
5239
|
async (args) => withClient(async (client) => {
|
|
5225
|
-
const filter = {
|
|
5226
|
-
...args.modelId ? { modelId: args.modelId } : {},
|
|
5227
|
-
...args.pageId ? { pageId: args.pageId } : {},
|
|
5228
|
-
...args.status ? { status: args.status } : {}
|
|
5229
|
-
};
|
|
5230
|
-
if ([args.limit, args.where, args.sort, args.page].every((v) => v === void 0)) {
|
|
5231
|
-
const entries = await client.listEntries(filter);
|
|
5232
|
-
return ok(`${entries.length} entr(y/ies).`, entries);
|
|
5233
|
-
}
|
|
5234
5240
|
const res = await client.fetchJSON(
|
|
5235
5241
|
client.url(`/management/content/entries${entryListSearch(args)}`),
|
|
5236
5242
|
{ method: "GET" }
|
|
@@ -6132,6 +6138,10 @@ components \u2014 is \`editor_setup\`, run on demand against the live build. The
|
|
|
6132
6138
|
\`get_deploy_status\` \`editorSetup\` (and get_next_steps) say when it has not run for the live
|
|
6133
6139
|
build: \`never\` or \`stale\` \u2192 run it before anything below.
|
|
6134
6140
|
|
|
6141
|
+
**Builds run \`build:bcms\` instead of \`build\`** when package.json declares one: the build alone,
|
|
6142
|
+
without CI checks (\`"build:bcms": "astro build"\` beside \`"build": "astro check && astro build"\`),
|
|
6143
|
+
keeping any step the build needs. The convert codemod adds it; never edit \`build\` for BetterCMS.
|
|
6144
|
+
|
|
6135
6145
|
\`deploy_project\` and \`promote_project\` both answer **409
|
|
6136
6146
|
AUTHORING_DECISION_REQUIRED** until a human has chosen this project's architecture. This is
|
|
6137
6147
|
asked ONCE per project, ever. It is not an error to retry or route around: read the message
|
|
@@ -6854,7 +6864,8 @@ Importance orders a screen: required fields first, then array order.
|
|
|
6854
6864
|
schema: the same call with \`pageId\` instead of \`modelId\`, and the version from \`get_page\`'s
|
|
6855
6865
|
\`backingModel\`.
|
|
6856
6866
|
- A component's props: \`update_component\` with \`propUi\`, \`propGroup\`, \`propLabel\`, \`propHelpText\` (keyed by prop key).
|
|
6857
|
-
Name props from the code (\`plan.cents\` \u2192 Cents)
|
|
6867
|
+
Name props from the code (\`plan.cents\` \u2192 Cents); help text guides ("Whole number, no currency symbol."), never repeats
|
|
6868
|
+
the value; repeated cards are one \`table\` prop. An icon (\`config.media: "icon"\`) is content: group it, never prune it.
|
|
6858
6869
|
They write the DRAFT props; no publish is needed for the editor to show them.
|
|
6859
6870
|
- Always send \`ifMatch\`, the \`optimisticVersion\` you read.
|
|
6860
6871
|
- A NEW field carries its chrome in the same \`add_field\` call.
|
|
@@ -7649,7 +7660,7 @@ function buildServer(deps) {
|
|
|
7649
7660
|
STRUCTURE_PLAYBOOK_URI,
|
|
7650
7661
|
{
|
|
7651
7662
|
title: "BetterCMS Content structure playbook",
|
|
7652
|
-
description: "How to organise a project's Content sidebar: one place per document, the home page and list pages pinned, one folder per content family named for the detail type its route holds and ordered by entry count, shared taxonomies in Shared,
|
|
7663
|
+
description: "How to organise a project's Content sidebar: one place per document, the home page and list pages pinned, one folder per content family named for the detail type its route holds and ordered by entry count, shared taxonomies in Shared, Pages for the rest, Settings last, at most 2 levels. With a worked set_content_structure payload.",
|
|
7653
7664
|
mimeType: "text/markdown"
|
|
7654
7665
|
},
|
|
7655
7666
|
() => ({
|