@bettercms-ai/mcp 0.66.4 → 0.67.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 +59 -30
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/references/bindings.md +10 -5
- package/dist/skills/bettercms/references/components.md +8 -3
- package/dist/skills/bettercms/references/getting-started.md +2 -1
- package/dist/skills/bettercms/references/localization.md +5 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2692,7 +2692,10 @@ var NEW_TOOL_TEXT = {
|
|
|
2692
2692
|
editor_setup: "Make the live build editable. A build imports only SEO/meta, media, brand and the models bcms-content.json declares; this derives the rest from the live release: pages, collections, forms, bindings, button and section components (when the authoring preference is components), Component Groups and the Content sidebar. 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` or get_deploy_status `editorSetup`). `state`: never or stale (run it), running, ready, failed. 409 = nothing is live yet: deploy first. Then get_next_steps.",
|
|
2693
2693
|
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)."
|
|
2694
2694
|
};
|
|
2695
|
+
var DELETE_PAGE_RUNTIME_BUNDLE = " Or, with no pageId, `runtimeBundle`: 'report' lists the duplicate pages derived from a server app's own bundle (`app/` folders); 'apply' retires them and their empty folders.";
|
|
2695
2696
|
var PARAM_TEXT = {
|
|
2697
|
+
runtimeBundle: "instead of pageId: the server-app bundle cleanup",
|
|
2698
|
+
pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
|
|
2696
2699
|
editorSetupForce: "re-run a setup already ready for the live build",
|
|
2697
2700
|
editorSetupCheck: "true = only report the state; starts nothing",
|
|
2698
2701
|
pageDynamic: "a dynamic (collection template) page only",
|
|
@@ -2703,7 +2706,7 @@ var PARAM_TEXT = {
|
|
|
2703
2706
|
where: "field filters: {field: value} is equality, {field: {op: value}} with op eq|ne|gt|gte|lt|lte|contains|in, and an array means `in`. System fields slug, status, publishedAt, locale work anywhere; a data field needs modelId. Compares the served copy (live for published, working for draft)",
|
|
2704
2707
|
sort: "comma list, a leading - sorts descending, e.g. '-publishedAt,title'",
|
|
2705
2708
|
page: "1-based page of `limit` rows; the response's meta says totalPages",
|
|
2706
|
-
locale: "BCP-47
|
|
2709
|
+
locale: "language, a BCP-47 tag (zh-CN \u2192 zh-cn), one of the project's locales; null = the default",
|
|
2707
2710
|
translationOf: "the SOURCE's id this translates (never another translation); null unlinks. One per locale: set locale on the translation and translationOf to the source",
|
|
2708
2711
|
pageScheduledAt: "ISO datetime to publish this page's draft at (needs publish permission); null cancels. An edit that omits scheduledAt CANCELS a pending schedule",
|
|
2709
2712
|
pageUnpublishAt: "ISO datetime at which the published page returns to draft; null cancels; omitting it never cancels it",
|
|
@@ -2714,7 +2717,7 @@ var PARAM_TEXT = {
|
|
|
2714
2717
|
confirmedInput: "true only after the user explicitly approved adding this custom input (`ui.input`); else setting or changing one is refused input_needs_confirmation. Removing one needs no approval"
|
|
2715
2718
|
};
|
|
2716
2719
|
var INPUT_CONFIRMED_HEADER = "x-bcms-confirmed-input";
|
|
2717
|
-
var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant?, size?, arrow?, icon?, newTab?}: never a text field beside a link field.";
|
|
2720
|
+
var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant?, size?, arrow?, icon?, newTab?} (arrow:false, icon:null turn one off): never a text field beside a link field.";
|
|
2718
2721
|
var UPDATE_FIELD_DESCRIPTION = "Change ONE existing field of a content model (`modelId`) or of a page's schema (`pageId`), addressed by `path`: its key, or dot-separated keys through groups and repeaters ('hero.title'). Set its label, helpText, required, validation `config` (merged by key; null deletes one), showIf, ui or placement (null clears those). The rest of the schema is written back unchanged at the version read; a 409 means it changed since, so read again. `type` changes how every stored value is read and needs allowRetype: true. `key` renames it: stored values stay under the old key, so the route answers 409 with the entry count unless allowFieldRemoval: true discards them \u2014 migrate the values first. Ask the user before a retype or a rename. A page's label + link pair (primaryCtaText + primaryCtaHref) becomes one button with `mergeHref`.";
|
|
2719
2722
|
var GET_SEO_DESCRIPTION = "Get the connected project's site-wide SEO settings \u2014 every key update_seo writes: seoDefaults (metaTitle, metaDescription, ogImage, twitterHandle, og, twitter, collectionSchemaTypes), siteSchema (the site's JSON-LD), robotsConfig, sitemapConfig, rssConfig, indexnowConfig and searchConfig.";
|
|
2720
2723
|
var UPDATE_SEO_DESCRIPTION = "Set the connected project's site-wide SEO defaults and config. Each key you send is REPLACED whole (not deep-merged), so read get_seo and send the complete object. `seoDefaults` applies to every page that does not override it, and its `collectionSchemaTypes` maps a collection's model id to the schema.org type of its entries. `siteSchema` is the site's own JSON-LD (Organization/WebSite, emitted on the home page). `robotsConfig`, `sitemapConfig` and `rssConfig` shape robots.txt, sitemap.xml and the feed; `indexnowConfig` pings search engines on publish; `searchConfig` is site search. A key the plan does not include answers 403 FEATURE_LOCKED. Rebuilds the site.";
|
|
@@ -2894,7 +2897,7 @@ var fieldType = z.enum([
|
|
|
2894
2897
|
"sections"
|
|
2895
2898
|
]);
|
|
2896
2899
|
var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
|
|
2897
|
-
var fieldKey = z.string().regex(/^[a-zA-Z0-9_]+$/, "letters, numbers, and
|
|
2900
|
+
var fieldKey = z.string().regex(/^[a-zA-Z0-9_-]+$/, "letters, numbers, underscores, and hyphens only");
|
|
2898
2901
|
var uiObject = z.strictObject({
|
|
2899
2902
|
collapsed: z.boolean().optional().describe("start this panel collapsed. Use it for long optional sections."),
|
|
2900
2903
|
preview: z.strictObject({
|
|
@@ -2935,7 +2938,7 @@ var propUiObject = uiObject.extend({
|
|
|
2935
2938
|
});
|
|
2936
2939
|
var fieldShape = {
|
|
2937
2940
|
key: fieldKey.describe(
|
|
2938
|
-
"machine field key. \u{1F534} UNIQUE ACROSS THE WHOLE MODEL \u2014 API IDs share ONE FLAT NAMESPACE, so a field nested inside a group must NOT reuse a key used by another field or group. Every section's heading cannot be 'title'. Prefix with the section: 'hero_title', 'pricing_title', 'faq_title'. Reusing a key is refused, and if it slips through it leaves permanent errors in the editor and breaks conditional-visibility rules, which reference fields by bare key."
|
|
2941
|
+
"machine field key. \u{1F534} UNIQUE ACROSS THE WHOLE MODEL \u2014 API IDs share ONE FLAT NAMESPACE, so a field nested inside a group must NOT reuse a key used by another field or group. Every section's heading cannot be 'title'. Prefix with the section: 'hero_title', 'pricing_title', 'faq_title'. Reusing a key is refused, and if it slips through it leaves permanent errors in the editor and breaks conditional-visibility rules, which reference fields by bare key. Keys derived from an imported site may contain hyphens ('hero-h1-welcome'): address them exactly as returned."
|
|
2939
2942
|
),
|
|
2940
2943
|
label: z.string().min(1).describe("human label shown in the editor"),
|
|
2941
2944
|
type: fieldType.describe(`field type. ${BUTTON_FIELD_NOTE}`),
|
|
@@ -3297,6 +3300,7 @@ function buildToolDefs(deps) {
|
|
|
3297
3300
|
throw err;
|
|
3298
3301
|
}
|
|
3299
3302
|
}
|
|
3303
|
+
const pageIdOf = async (client, ref) => UUID.test(ref) ? ref : (await client.fetchJSON(client.url(`/management/pages/resolve?ref=${encodeURIComponent(ref)}`))).data.id;
|
|
3300
3304
|
function guard(fn) {
|
|
3301
3305
|
return async (args) => {
|
|
3302
3306
|
try {
|
|
@@ -3417,10 +3421,10 @@ function buildToolDefs(deps) {
|
|
|
3417
3421
|
translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf)
|
|
3418
3422
|
});
|
|
3419
3423
|
const getPageInput = z.object({
|
|
3420
|
-
pageId: z.string().min(1).describe(
|
|
3424
|
+
pageId: z.string().min(1).describe(PARAM_TEXT.pageRef)
|
|
3421
3425
|
});
|
|
3422
3426
|
const setPageContentInput = z.object({
|
|
3423
|
-
pageId: z.string().min(1).describe(
|
|
3427
|
+
pageId: z.string().min(1).describe(PARAM_TEXT.pageRef),
|
|
3424
3428
|
data: z.record(z.string(), z.unknown()).describe(
|
|
3425
3429
|
"field values keyed by field key. A nested 'array' (zone) value is an OBJECT { nonRepeatable: { childKey: value, \u2026 }, repeatable: [ { childKey: value }, \u2026 ] } \u2014 nonRepeatable holds the fixed-block values, repeatable is the list of item objects (omit a zone you didn't define). A primitive 'array' (itemType) is a plain list. An 'image' value is an asset URL or asset id (from upload_asset) \u2014 the server resolves it to { id, url, name, altText }. Read get_page first to see each field's zones."
|
|
3426
3430
|
),
|
|
@@ -3468,7 +3472,8 @@ function buildToolDefs(deps) {
|
|
|
3468
3472
|
translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf)
|
|
3469
3473
|
});
|
|
3470
3474
|
const deletePageInput = z.object({
|
|
3471
|
-
pageId: z.string().min(1).describe("id of the page to delete (from list_pages)")
|
|
3475
|
+
pageId: z.string().min(1).optional().describe("id of the page to delete (from list_pages)"),
|
|
3476
|
+
runtimeBundle: z.enum(["report", "apply"]).optional().describe(PARAM_TEXT.runtimeBundle)
|
|
3472
3477
|
});
|
|
3473
3478
|
const deleteEntryInput = z.object({
|
|
3474
3479
|
entryId: z.string().min(1).describe("id of the content entry to delete (from list_content_entries)")
|
|
@@ -3719,8 +3724,8 @@ function buildToolDefs(deps) {
|
|
|
3719
3724
|
const composePagesInput = z.object({
|
|
3720
3725
|
pages: z.array(
|
|
3721
3726
|
z.object({
|
|
3722
|
-
pageId: z.string().min(1).optional().describe(
|
|
3723
|
-
slug: z.string().min(1).optional().describe("page slug,
|
|
3727
|
+
pageId: z.string().min(1).optional().describe(PARAM_TEXT.pageRef),
|
|
3728
|
+
slug: z.string().min(1).optional().describe("a page slug, when one page has it; or pass `pageId`"),
|
|
3724
3729
|
blockJson: z.array(blockObject).describe("REPLACES this page's whole block composition")
|
|
3725
3730
|
})
|
|
3726
3731
|
).min(1).max(50)
|
|
@@ -4330,7 +4335,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4330
4335
|
"update_page",
|
|
4331
4336
|
"Edit a page",
|
|
4332
4337
|
"Edit a page: title, slug, SEO metaTitle/metaDescription, structured data (`schemaType`, `schema`), publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first and send its `optimisticVersion` as `ifMatch`). A `blockJson` that swaps a component block to another component comes back with `warnings` (COMPONENT_SWAP_DROPS_OVERRIDES) naming the overrides the new component does not render; they stay on the block, so swapping back restores them. 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 \u2014 and on a singleton it publishes the page's content entry too; if the entry's workflow stage refuses, the page still goes live and `warnings` carries ENTRY_STILL_DRAFT with the entry id and the reason. STRUCTURED DATA is native SEO, never a field: leave it out and the page gets Automatic JSON-LD from its content (WebSite on the home page, Blog/CollectionPage on a collection's list page, WebPage otherwise); `schemaType` picks an explicit schema.org type whose properties are filled from the content; `schema` is pasted JSON-LD and wins over both (null clears it). " + PAGE_HEAD_NOTE + " " + SECTION_DOCTRINE,
|
|
4333
|
-
z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1), title: z.string().optional(), slug: z.string().optional(), blockJson: z.array(blockObject).optional().describe("REPLACES the page's block composition"), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", "WebPage", "AboutPage", "ContactPage", "CollectionPage", "Blog", "BlogPosting", "Article", "Product", "FAQPage", "Event", "Organization", "LocalBusiness"]).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,
|
|
4338
|
+
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.array(blockObject).optional().describe("REPLACES the page's block composition"), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", "WebPage", "AboutPage", "ContactPage", "CollectionPage", "Blog", "BlogPosting", "Article", "Product", "FAQPage", "Event", "Organization", "LocalBusiness"]).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,
|
|
4334
4339
|
async (c, a) => {
|
|
4335
4340
|
const res = await c.fetchJSON(
|
|
4336
4341
|
c.url(`/management/pages/${s(a.pageId)}/meta`),
|
|
@@ -4711,14 +4716,16 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4711
4716
|
"get_next_steps",
|
|
4712
4717
|
"Get what to do next",
|
|
4713
4718
|
NEXT_STEPS_DESCRIPTION,
|
|
4714
|
-
z.object({
|
|
4719
|
+
z.object({
|
|
4720
|
+
importJunk: z.boolean().optional().describe("true = add rule import-junk: what an old import left, with removal calls")
|
|
4721
|
+
}).shape,
|
|
4715
4722
|
// The whole body: `receipt` rides beside `data`, and `data()` would drop it.
|
|
4716
|
-
async (c) => ok("Next steps.", await c.fetchJSON(c.url(`/management/insights/next-steps`), { method: "GET" }))
|
|
4723
|
+
async (c, a) => ok("Next steps.", await c.fetchJSON(c.url(`/management/insights/next-steps${a.importJunk === true ? "?importJunk=true" : ""}`), { method: "GET" }))
|
|
4717
4724
|
),
|
|
4718
4725
|
def(
|
|
4719
4726
|
"get_binding_report",
|
|
4720
4727
|
"Check what on the live site is editable",
|
|
4721
|
-
"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 \u2014 not-declared, ambiguous-text, no-element, declared-only, needs-wrap, refused, layout-owned, page-owned, layout-locator-stale, locale-unvarianted, svg-unavailable, partial and unstamped (a page list the content apply left as built: `kind` `list <bound>/<total>`), mismatch (a text-match stamp whose element shows MORE than the value, or no longer the text it was matched on (`data-bcms-from`): never written), report-outdated. DEPLOY FIRST: before any release there is no report (pages 0, mode null, refreshRequired true). `refreshReason`: `never-generated` \u2014 only a release BetterCMS builds and serves writes a report, so a HEADLESS site never gets one, redeploy or not; `outdated` \u2014 the next release rewrites it. It certifies only that every non-empty field has SOME element carrying its path. `unaddressable` is the copy it cannot see: visible live text no field owns, by route and bucket, biggest first (null = unmeasured: diff visible text against entry values). Over `count / visible > 0.02`, fix it in the SOURCE (wrap the run in an element the codemod can bind, or declare `<BcmsField path=\u2026>`), deploy, re-read \u2014 playbook \xA711. `canvas.lane`: the editor's live-preview lane (`bridge`, `draft-route`; `none` \u2192 get_next_steps). `coverage.error` `nothing-bound`: no element of this build is bound, so its percentage describes another site.",
|
|
4728
|
+
"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 \u2014 not-declared, ambiguous-text, no-element, declared-only, needs-wrap, refused, layout-owned, page-owned, layout-locator-stale, locale-unvarianted, svg-unavailable, partial and unstamped (a page list the content apply left as built: `kind` `list <bound>/<total>`), mismatch (a text-match stamp whose element shows MORE than the value, or no longer the text it was matched on (`data-bcms-from`): never written), report-outdated. DEPLOY FIRST: before any release there is no report (pages 0, mode null, refreshRequired true). `refreshReason`: `never-generated` \u2014 only a release BetterCMS builds and serves writes a report, so a HEADLESS site never gets one, redeploy or not; `outdated` \u2014 the next release rewrites it. It certifies only that every non-empty field has SOME element carrying its path. Global Sections count only on routes that render them (`layoutRoutes`). `unaddressable` is the copy it cannot see: visible live text no field owns, by route and bucket, biggest first (null = unmeasured: diff visible text against entry values). Over `count / visible > 0.02`, fix it in the SOURCE (wrap the run in an element the codemod can bind, or declare `<BcmsField path=\u2026>`), deploy, re-read \u2014 playbook \xA711. `canvas.lane`: the editor's live-preview lane (`bridge`, `draft-route`; `none` \u2192 get_next_steps). `coverage.error` `nothing-bound`: no element of this build is bound, so its percentage describes another site.",
|
|
4722
4729
|
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. Also per slot: `builtRoutes`: routes the live build has HTML for (null = cannot say). `layoutIncomplete`: Layout paths the last content apply could not write (their publish waits); `layoutUnread`: Layout fields no route reads (`layoutUnreadJudged` false = not judged).") }).shape,
|
|
4723
4730
|
async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
|
|
4724
4731
|
),
|
|
@@ -4989,7 +4996,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4989
4996
|
def(
|
|
4990
4997
|
"get_deploy_status",
|
|
4991
4998
|
"Get deploy/build status",
|
|
4992
|
-
"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` is the newest release (id, sha, status, phaseMarks, error, createdAt): while an uploaded build ingests or releases, state is `building`, `release.sha` the new sha and `lastLiveSha` still the old one; a failed deploy shows `release.status` failed with its `error`, and `held` state (`held.by`: staff, archived or no-subdomain) means the release waits until that is cleared. Also returns `canvas.lane` (the editor's live-preview lane for this site: `bridge` / `draft-route` / `none`) and `editorUrl`, the visual editor to hand the user.",
|
|
4999
|
+
"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` is the newest release (id, sha, status, phaseMarks, error, createdAt): while an uploaded build ingests or releases, state is `building`, `release.sha` the new sha and `lastLiveSha` still the old one (re-sending the live sha is a no-op); a failed deploy shows `release.status` failed with its `error`, and `held` state (`held.by`: staff, archived or no-subdomain) means the release waits until that is cleared. `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` (the editor's live-preview lane for this site: `bridge` / `draft-route` / `none`) and `editorUrl`, the visual editor to hand the user.",
|
|
4993
5000
|
z.object({}).shape,
|
|
4994
5001
|
// Flat body, no `data` envelope: `data()` read it as undefined.
|
|
4995
5002
|
async (c) => ok("Deploy status.", await c.fetchJSON(c.url(`/management/projects/deploy-status`), { method: "GET" }))
|
|
@@ -5047,7 +5054,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
5047
5054
|
name: "list_pages",
|
|
5048
5055
|
config: {
|
|
5049
5056
|
title: "List pages in the current project",
|
|
5050
|
-
description: "List the pages in the project this MCP key is bound to, each with its full field SCHEMA, pageType (singleton|dynamic), and status. Use it to verify WHERE content lands and to read field keys/types before set_page_content / add_field.",
|
|
5057
|
+
description: "List the pages in the project this MCP key is bound to, each with its id, `path`, `language`, full field SCHEMA, pageType (singleton|dynamic), and status. Use it to verify WHERE content lands and to read field keys/types before set_page_content / add_field.",
|
|
5051
5058
|
inputSchema: {}
|
|
5052
5059
|
},
|
|
5053
5060
|
handler: guard(
|
|
@@ -5059,6 +5066,9 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
5059
5066
|
id: p.id,
|
|
5060
5067
|
title: p.title,
|
|
5061
5068
|
slug: p.slug,
|
|
5069
|
+
// Its URL and language: slugs are unique per folder, so `about` can be seven pages.
|
|
5070
|
+
path: p.path,
|
|
5071
|
+
language: p.language,
|
|
5062
5072
|
pageType: p.pageType,
|
|
5063
5073
|
status: p.status,
|
|
5064
5074
|
fields: p.fields
|
|
@@ -5071,7 +5081,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
5071
5081
|
name: "get_page",
|
|
5072
5082
|
config: {
|
|
5073
5083
|
title: "Get a page (with its field schema)",
|
|
5074
|
-
description: "Get one page
|
|
5084
|
+
description: "Get one page INCLUDING its full field schema (keys, types, nested group/repeater children), pageType, `path` and `language`. Read this before set_page_content so you write correctly-keyed values, or before add_field so you know the existing keys.",
|
|
5075
5085
|
inputSchema: getPageInput.shape
|
|
5076
5086
|
},
|
|
5077
5087
|
handler: guard(
|
|
@@ -5334,11 +5344,19 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5334
5344
|
name: "delete_page",
|
|
5335
5345
|
config: {
|
|
5336
5346
|
title: "Delete a page",
|
|
5337
|
-
description: "DELETE a page and its content. Destructive but REVERSIBLE \u2014 it soft-deletes (can be restored from the dashboard) and is audit-logged. Use it to remove a page you created by mistake. Confirm with the user before deleting content they may want.",
|
|
5347
|
+
description: "DELETE a page and its content. Destructive but REVERSIBLE \u2014 it soft-deletes (can be restored from the dashboard) and is audit-logged. Use it to remove a page you created by mistake. Confirm with the user before deleting content they may want." + DELETE_PAGE_RUNTIME_BUNDLE,
|
|
5338
5348
|
inputSchema: deletePageInput.shape
|
|
5339
5349
|
},
|
|
5340
5350
|
handler: guard(
|
|
5341
5351
|
async (args) => withClient(async (client) => {
|
|
5352
|
+
if (!args.pageId === !args.runtimeBundle) return fail("Pass exactly one of pageId or runtimeBundle.");
|
|
5353
|
+
if (args.runtimeBundle) {
|
|
5354
|
+
const res2 = await client.fetchJSON(client.url("/management/pages/retire-runtime-bundle"), {
|
|
5355
|
+
method: "POST",
|
|
5356
|
+
body: JSON.stringify({ dryRun: args.runtimeBundle !== "apply" })
|
|
5357
|
+
});
|
|
5358
|
+
return ok(args.runtimeBundle === "apply" ? "Bundle pages retired." : "Bundle pages (dry run, nothing written).", res2.data);
|
|
5359
|
+
}
|
|
5342
5360
|
const res = await client.deletePage(args.pageId);
|
|
5343
5361
|
return ok(`Deleted page ${res.id} (soft-delete \u2014 restorable from the dashboard).`, res);
|
|
5344
5362
|
})
|
|
@@ -5630,18 +5648,20 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5630
5648
|
},
|
|
5631
5649
|
handler: guard(
|
|
5632
5650
|
async (args) => withClient(async (client) => {
|
|
5633
|
-
const
|
|
5634
|
-
const [known, pageList] = await Promise.all([
|
|
5635
|
-
client.listComponents({ select: "summary" }),
|
|
5636
|
-
needsSlug ? client.listPages() : Promise.resolve([])
|
|
5637
|
-
]);
|
|
5651
|
+
const known = await client.listComponents({ select: "summary" });
|
|
5638
5652
|
const ids = new Set(known.map((c) => c.id));
|
|
5639
|
-
const bySlug = new Map(pageList.map((p) => [p.slug, p.id]));
|
|
5640
5653
|
const results = [];
|
|
5641
5654
|
for (const page of args.pages) {
|
|
5642
|
-
const
|
|
5643
|
-
if (!
|
|
5644
|
-
results.push({ ok: false,
|
|
5655
|
+
const ref = page.pageId ?? page.slug;
|
|
5656
|
+
if (!ref) {
|
|
5657
|
+
results.push({ ok: false, error: "Pass a pageId or path from list_pages." });
|
|
5658
|
+
continue;
|
|
5659
|
+
}
|
|
5660
|
+
let pageId;
|
|
5661
|
+
try {
|
|
5662
|
+
pageId = await pageIdOf(client, ref);
|
|
5663
|
+
} catch (err) {
|
|
5664
|
+
results.push({ ok: false, slug: page.slug, error: errorText(err) });
|
|
5645
5665
|
continue;
|
|
5646
5666
|
}
|
|
5647
5667
|
const unknown = [...new Set(componentIdsIn(page.blockJson))].filter((id) => !ids.has(id));
|
|
@@ -5659,7 +5679,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5659
5679
|
client.url(`/management/pages/${pageId}/meta`),
|
|
5660
5680
|
{ method: "PATCH", body: JSON.stringify({ blockJson: page.blockJson }) }
|
|
5661
5681
|
);
|
|
5662
|
-
results.push({ ok: true, pageId, slug: written.data?.slug ?? page.slug, blocks: page.blockJson.length });
|
|
5682
|
+
results.push({ ok: true, pageId, slug: written.data?.slug ?? page.slug, path: written.data?.path, blocks: page.blockJson.length });
|
|
5663
5683
|
} catch (err) {
|
|
5664
5684
|
results.push({ ok: false, pageId, slug: page.slug, error: errorText(err) });
|
|
5665
5685
|
}
|
|
@@ -5911,11 +5931,20 @@ ${lines.join("\n")}`, found);
|
|
|
5911
5931
|
// through the client's request plumbing — no bespoke SDK method per endpoint.
|
|
5912
5932
|
...lifecycleTools()
|
|
5913
5933
|
];
|
|
5934
|
+
const withPageRef = (def) => ({
|
|
5935
|
+
...def,
|
|
5936
|
+
handler: (args) => {
|
|
5937
|
+
const ref = args?.pageId;
|
|
5938
|
+
if (typeof ref !== "string" || !ref || UUID.test(ref)) return def.handler(args);
|
|
5939
|
+
return guard(async () => def.handler({ ...args, pageId: await withClient((c) => pageIdOf(c, ref)) }))(void 0);
|
|
5940
|
+
}
|
|
5941
|
+
});
|
|
5914
5942
|
return [
|
|
5915
5943
|
...defs.filter((d) => !LISTING_TAIL.has(d.name)),
|
|
5916
5944
|
...defs.filter((d) => LISTING_TAIL.has(d.name))
|
|
5917
|
-
].map(withProjectId);
|
|
5945
|
+
].map(withPageRef).map(withProjectId);
|
|
5918
5946
|
}
|
|
5947
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5919
5948
|
function registerTools(server, deps, tools = process.env.BETTERCMS_MCP_TOOLS) {
|
|
5920
5949
|
const withElicit = {
|
|
5921
5950
|
...deps,
|
|
@@ -7322,7 +7351,7 @@ the host has a stale cached MCP \u2014 tell the user to \`rm -rf ~/.npm/_npx\` a
|
|
|
7322
7351
|
|
|
7323
7352
|
${SCHEMA_PROPOSAL_FLOW}
|
|
7324
7353
|
|
|
7325
|
-
After creating, report each page's id,
|
|
7354
|
+
After creating, report each page's id, path, type, and field count, and the project it
|
|
7326
7355
|
landed in. On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs
|
|
7327
7356
|
(re)authorizing.`
|
|
7328
7357
|
}
|
|
@@ -7354,7 +7383,7 @@ ${PAGE_FLOW}
|
|
|
7354
7383
|
|
|
7355
7384
|
${STRUCTURE_RULE}
|
|
7356
7385
|
|
|
7357
|
-
After creating, report the page id,
|
|
7386
|
+
After creating, report the page id, path, type, field count, and the project it landed in.
|
|
7358
7387
|
On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs (re)authorizing.`
|
|
7359
7388
|
}
|
|
7360
7389
|
}
|