@bettercms-ai/mcp 0.42.0 → 0.45.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 +245 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1982,6 +1982,126 @@ var LAYOUT_SECTION_ICON_SET = new Set(LAYOUT_SECTION_ICONS);
|
|
|
1982
1982
|
// src/tools.ts
|
|
1983
1983
|
import { BetterCMSError } from "@bettercms-ai/sdk";
|
|
1984
1984
|
import { DeviceAuthPendingError } from "@bettercms-ai/device-auth";
|
|
1985
|
+
|
|
1986
|
+
// src/structure-playbook.ts
|
|
1987
|
+
var STRUCTURE_PLAYBOOK_URI = "bettercms://playbook/structure";
|
|
1988
|
+
var STRUCTURE_DEFAULT_INSTRUCTION = `After you create collections or pages, organise them per the structure playbook: read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure (read-only), show the user its outline, then apply it with set_content_structure and the version it returned (the If-Match). A project that already has a structure keeps it: file only what you created, with move_to_folder, unless the user asks for a full re-organisation.`;
|
|
1989
|
+
var STRUCTURE_EXAMPLE_PAYLOAD = {
|
|
1990
|
+
version: 0,
|
|
1991
|
+
doc: {
|
|
1992
|
+
schema: 1,
|
|
1993
|
+
folders: [
|
|
1994
|
+
{ id: "home:pages", parentId: null, name: "Static pages", icon: "files", sort: -3 },
|
|
1995
|
+
{ id: "home:collections", parentId: null, name: "Other content", icon: null, sort: -2 },
|
|
1996
|
+
{ id: "fld_std_blog", parentId: null, name: "Blog posts", icon: "newspaper", sort: 0 },
|
|
1997
|
+
{ id: "fld_std_products", parentId: null, name: "Products", icon: "shopping-bag", sort: 1 }
|
|
1998
|
+
],
|
|
1999
|
+
items: [
|
|
2000
|
+
{ kind: "page", id: "pg_home", folderId: "home:root", sort: 0, icon: null },
|
|
2001
|
+
{ kind: "page", id: "pg_blog", folderId: "home:root", sort: 1, icon: null },
|
|
2002
|
+
{ kind: "page", id: "pg_shop", folderId: "home:root", sort: 2, icon: null },
|
|
2003
|
+
{ kind: "collection", id: "cm_posts", folderId: "fld_std_blog", sort: 0, icon: null },
|
|
2004
|
+
{ kind: "collection", id: "cm_products", folderId: "fld_std_products", sort: 0, icon: null },
|
|
2005
|
+
{ kind: "collection", id: "cm_categories", folderId: "fld_std_products", sort: 1, icon: null },
|
|
2006
|
+
{ kind: "collection", id: "cm_colours", folderId: "fld_std_products", sort: 2, icon: null },
|
|
2007
|
+
{ kind: "collection", id: "cm_tags", folderId: "home:collections", sort: 0, icon: null },
|
|
2008
|
+
{ kind: "page", id: "pg_about", folderId: "home:pages", sort: 0, icon: null },
|
|
2009
|
+
{ kind: "page", id: "pg_contact", folderId: "home:pages", sort: 1, icon: null },
|
|
2010
|
+
{ kind: "page", id: "pg_404", folderId: "home:pages", sort: 2, icon: "file-code" }
|
|
2011
|
+
]
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
var STRUCTURE_PLAYBOOK = `# Organising a BetterCMS project: the Content structure standard
|
|
2015
|
+
|
|
2016
|
+
Read this BEFORE you organise the Content sidebar, and AFTER you create collections or pages:
|
|
2017
|
+
the default is to organise what you made by these rules. The sidebar is one document per
|
|
2018
|
+
project, \`{ schema: 1, folders: [{ id, parentId, name, icon, sort }], items: [{ kind: 'page' |
|
|
2019
|
+
'collection', id, folderId, sort, icon }] }\`, read with \`get_content_structure\` and written with
|
|
2020
|
+
\`set_content_structure\`. It is ORGANISATION ONLY: no URL, slug, schema, content or publish state
|
|
2021
|
+
ever changes.
|
|
2022
|
+
|
|
2023
|
+
## The rules
|
|
2024
|
+
|
|
2025
|
+
1. **One place per document.** Every page and every collection sits in exactly ONE place: pinned
|
|
2026
|
+
at the top level (\`folderId: "home:root"\`) or in one folder. A folder lists only its direct
|
|
2027
|
+
contents. Never list an id twice.
|
|
2028
|
+
2. **Pinned at the top:** the home page, then each collection's LIST page (the dynamic page that
|
|
2029
|
+
lists it, e.g. /blog). A single document opens straight into the editor, so it is a pin, not a
|
|
2030
|
+
folder with one page in it.
|
|
2031
|
+
3. **One folder per content family.**
|
|
2032
|
+
- Blog: a "Blog posts" folder holding the blog collection. A folder with exactly one
|
|
2033
|
+
collection opens that collection's table directly.
|
|
2034
|
+
- Commerce: a "Products" folder holding the product collection AND its taxonomies (product
|
|
2035
|
+
categories, product tags, materials, colours, sizes, merch collections, ...).
|
|
2036
|
+
- Decide the rest by the REFERENCE GRAPH: a collection that is referenced only by one family,
|
|
2037
|
+
or that references only one family (reviews \u2192 products), belongs in that family's folder.
|
|
2038
|
+
The family's main collection comes first, then the others in the order its fields reference
|
|
2039
|
+
them.
|
|
2040
|
+
4. **Other pages.** Standalone static pages go in the system folder \`home:pages\` ("Static
|
|
2041
|
+
pages"). Code-only routes (a 404, a page built by code with nothing to edit) stay there too,
|
|
2042
|
+
marked with the \`file-code\` icon. Pages that share a route (/legal/terms, /legal/privacy) get
|
|
2043
|
+
a subfolder of Static pages; a large route family or one named for what it is (/docs/...) gets
|
|
2044
|
+
its own top-level folder.
|
|
2045
|
+
5. **Shared taxonomies.** A vocabulary several families reference (Tags used by posts AND
|
|
2046
|
+
products) goes in the system folder \`home:collections\`, named "Other content". So does
|
|
2047
|
+
supporting content no family owns (testimonials, FAQs with no list page).
|
|
2048
|
+
6. **Icons** (lucide names): \`newspaper\` for posts, \`shopping-bag\` for products, \`book-open\`
|
|
2049
|
+
for docs, \`files\` for Static pages, \`folder\` by default.
|
|
2050
|
+
7. **Depth:** nest at most 2 levels by default. The hard limit is 4; do not use it without a
|
|
2051
|
+
reason the user gave you.
|
|
2052
|
+
8. **Organisation only.** Never rename a page, change a slug, touch a schema or move content to
|
|
2053
|
+
make a structure fit. The structure fits the site, not the other way round.
|
|
2054
|
+
9. **Page-bound models are not collections.** A page with fields has a content model bound to
|
|
2055
|
+
it; it is never filed (400 \`NAV_PAGE_BOUND_MODEL\`). Organise the PAGE.
|
|
2056
|
+
|
|
2057
|
+
## The workflow
|
|
2058
|
+
|
|
2059
|
+
1. \`get_content_structure\` \u2014 note \`version\` and whether a structure exists.
|
|
2060
|
+
2. \`suggest_content_structure\` \u2014 READ-ONLY. It runs the platform's own implementation of these
|
|
2061
|
+
rules over the project's pages, collections, references and routes and returns
|
|
2062
|
+
\`{ doc, outline, rationale, version, hasDocument }\`. It writes nothing.
|
|
2063
|
+
3. Show the user the \`outline\`. If \`hasDocument\` is true the project ALREADY has a structure
|
|
2064
|
+
someone made: never replace it without the user's explicit yes. Offer to file only what is
|
|
2065
|
+
new (\`move_to_folder\` per item) instead.
|
|
2066
|
+
4. Apply with \`set_content_structure { doc, version }\`, where \`version\` is the one you read \u2014
|
|
2067
|
+
it is the If-Match. A 412 \`VERSION_CONFLICT\` means someone changed the sidebar since: read it
|
|
2068
|
+
again and re-apply; never overwrite blind.
|
|
2069
|
+
5. For one or two new documents, prefer \`move_to_folder\` into the matching family folder.
|
|
2070
|
+
|
|
2071
|
+
Projects imported from GitHub get this structure automatically (the "Organising your content"
|
|
2072
|
+
step of the import), but only when they have none yet; a later import files NEW documents into
|
|
2073
|
+
the matching folder and changes nothing else.
|
|
2074
|
+
|
|
2075
|
+
## Worked example: a blog plus a shop
|
|
2076
|
+
|
|
2077
|
+
The site: a home page (\`pg_home\`), a /blog list page (\`pg_blog\`, dynamic) for the Blog
|
|
2078
|
+
collection (\`cm_posts\`), a /shop list page (\`pg_shop\`, dynamic) for Products
|
|
2079
|
+
(\`cm_products\`), About (\`pg_about\`), Contact (\`pg_contact\`) and a code-only 404
|
|
2080
|
+
(\`pg_404\`). Products references Product categories (\`cm_categories\`) and Colours
|
|
2081
|
+
(\`cm_colours\`); both Blog and Products reference Tags (\`cm_tags\`).
|
|
2082
|
+
|
|
2083
|
+
\`\`\`text
|
|
2084
|
+
[pinned page] Home
|
|
2085
|
+
[pinned page] Blog (/blog lists Blog)
|
|
2086
|
+
[pinned page] Shop (/shop lists Products)
|
|
2087
|
+
[system folder] Static pages icon=files
|
|
2088
|
+
About, Contact, Not found (icon=file-code)
|
|
2089
|
+
[system folder] Other content
|
|
2090
|
+
Tags (referenced by Blog AND Products: shared)
|
|
2091
|
+
[folder] Blog posts icon=newspaper
|
|
2092
|
+
Blog
|
|
2093
|
+
[folder] Products icon=shopping-bag
|
|
2094
|
+
Products, Product categories, Colours (referenced only by Products)
|
|
2095
|
+
\`\`\`
|
|
2096
|
+
|
|
2097
|
+
The exact \`set_content_structure\` payload (\`version: 0\` because the project had no structure):
|
|
2098
|
+
|
|
2099
|
+
\`\`\`json
|
|
2100
|
+
${JSON.stringify(STRUCTURE_EXAMPLE_PAYLOAD, null, 2)}
|
|
2101
|
+
\`\`\`
|
|
2102
|
+
`;
|
|
2103
|
+
|
|
2104
|
+
// src/tools.ts
|
|
1985
2105
|
var FRAMEWORK_CHOICES = ["astro", "next", "react-ts", "other"];
|
|
1986
2106
|
var FRAMEWORK_LABELS = {
|
|
1987
2107
|
astro: "Astro \u2014 recommended default, static by default and fastest to publish",
|
|
@@ -2273,6 +2393,11 @@ function toField(f) {
|
|
|
2273
2393
|
...f.config ? { config: f.config } : {}
|
|
2274
2394
|
};
|
|
2275
2395
|
}
|
|
2396
|
+
var STRUCTURE_NOTE = 'ORGANISATION ONLY: folders never change a URL, slug, content or publish state, and nothing is rebuilt or delivered. Folders nest at most 4 levels deep (a page or collection may sit in a level-4 folder). Icons are lucide icon names, e.g. "folder", "file-text", "rows-3", "house" (a-z, 0-9 and dashes); null clears one. SYSTEM FOLDERS (CPO-129): "home:pages" (Static pages), "home:collections" (Collections) and "home:globals" (Globals) always exist, even when the document has no record for them. They stay at the top level and can never be moved or deleted (400 NAV_SYSTEM_FOLDER), but rename_folder and set_icon work on them and create their record on first use. TYPE PURITY: anything inside Static pages, at any depth, must be a page, and anything inside Collections must be a collection, and Globals holds NO pages or collections at all, because globals are not sidebar items (subfolders under Globals are allowed but stay empty). Any of these is 400 NAV_TYPE_MISMATCH. Your own folders hold either kind. ACCESS (CPO-131): Requires Admin or Developer access to CHANGE the structure, the same access as editing the schema; anyone who can read content can call get_content_structure. A 403 SCHEMA_ACCESS_REQUIRED means the person behind this connection is not an Admin or Developer: stop and tell the user, do not retry. ROOT PINS (CPO-132): folderId \'root\' pins a page or collection at the top level; null returns it to its home (Static pages / Collections). Pins sit above the folders, in their own order, and may be pages or collections. In a document a pin is folderId "home:root", a virtual id with no folder record (a folder record with that id is 400 NAV_SYSTEM_FOLDER). KIND CHECK: an id is either a page or a collection, never both, and a custom folder holds either \u2014 so move_to_folder and set_icon VERIFY that the id really is the `kind` you named on this branch, and refuse a mismatch with 400 NAV_KIND_MISMATCH naming the kind to use; call it again with that kind, which also clears the wrong-kind entry. An id that resolves to nothing is allowed (it may exist on another branch) and the result says so. set_content_structure never refuses: it returns `warnings` and `kindMismatches`. get_content_structure marks such items `kindMismatch: true` and lists them in `summary.kindMismatches`. PAGE-BOUND MODELS (CPO-135): a page with fields has a content model bound to it. That model is NOT a collection \u2014 get_content_structure never lists it among collections or in the tree, and move_to_folder / set_icon / set_content_structure refuse it with 400 NAV_PAGE_BOUND_MODEL naming the owning page (\'This model belongs to the page "\u2026"; organise the page.\'). Organise the PAGE instead, by its page id. If a stored document still references one it appears in `summary.hiddenItems` with `boundToPageId`, and the next single write drops it.';
|
|
2397
|
+
function structureResult(d) {
|
|
2398
|
+
const message = d?.message;
|
|
2399
|
+
return ok(typeof message === "string" ? message : "Updated the Content structure.", d);
|
|
2400
|
+
}
|
|
2276
2401
|
function ok(summary, data) {
|
|
2277
2402
|
return {
|
|
2278
2403
|
content: [
|
|
@@ -2927,6 +3052,107 @@ function buildToolDefs(deps) {
|
|
|
2927
3052
|
z.object({ formId: z.string().min(1), submissionId: z.string().min(1) }).shape,
|
|
2928
3053
|
async (c, a) => ok("Deleted submission.", await data(c, "DELETE", `/management/forms/${s(a.formId)}/submissions/${s(a.submissionId)}`))
|
|
2929
3054
|
),
|
|
3055
|
+
// ── Content structure: the Content sidebar's folders and icons (CPO-127 f) ──
|
|
3056
|
+
def(
|
|
3057
|
+
"get_content_structure",
|
|
3058
|
+
"Get the Content sidebar structure",
|
|
3059
|
+
`Read the Content sidebar's structure for the connected project: its folders, which pages and collections sit in each, and their icons. Returns { doc, version, summary }: \`doc\` is the raw document ({ schema: 1, folders: [{ id, parentId, name, icon, sort }], items: [{ kind: 'page'|'collection', id, folderId, sort, icon }] }), \`version\` is what set_content_structure needs, and \`summary\` is the resolved tree with page and collection TITLES (\`outline\` is a readable version), ids that no longer resolve (\`missing\`), and the pages and collections not in any folder yet. Call it before changing the structure, and read the resource ${STRUCTURE_PLAYBOOK_URI} (the structure standard) before you organise anything. ${STRUCTURE_NOTE}`,
|
|
3060
|
+
z.object({}).shape,
|
|
3061
|
+
async (c) => {
|
|
3062
|
+
const d = await data(c, "GET", `/management/content-structure`);
|
|
3063
|
+
return ok(d?.summary?.outline ? `Content structure:
|
|
3064
|
+
${d.summary.outline}` : "Content structure.", d);
|
|
3065
|
+
}
|
|
3066
|
+
),
|
|
3067
|
+
def(
|
|
3068
|
+
"suggest_content_structure",
|
|
3069
|
+
"Propose the standard Content structure",
|
|
3070
|
+
`READ-ONLY: propose the standard Content sidebar structure for the connected project (${STRUCTURE_PLAYBOOK_URI}) and write NOTHING. Runs the platform's own implementation of the standard over the project's pages, collections, their references and the routes its build serves: the home page and each collection's list page pinned at the top, one folder per content family (Blog posts, Products with its taxonomies, grouped by the reference graph), shared taxonomies in Other content, standalone and code-only pages in Static 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.`,
|
|
3071
|
+
z.object({
|
|
3072
|
+
maxDepth: z.number().int().min(1).max(4).optional().describe("folder nesting to propose; default 2 (the standard), 4 is the hard limit")
|
|
3073
|
+
}).shape,
|
|
3074
|
+
async (c, a) => {
|
|
3075
|
+
const d = await data(c, "GET", `/management/content-structure/suggest${q({ maxDepth: a.maxDepth })}`);
|
|
3076
|
+
return ok(d?.outline ? `Proposed Content structure (nothing was written):
|
|
3077
|
+
${d.outline}` : "Proposed Content structure.", d);
|
|
3078
|
+
}
|
|
3079
|
+
),
|
|
3080
|
+
def(
|
|
3081
|
+
"set_content_structure",
|
|
3082
|
+
"Replace the Content sidebar structure",
|
|
3083
|
+
`Replace the whole Content sidebar document in one write. Follow the structure standard (read ${STRUCTURE_PLAYBOOK_URI} first; suggest_content_structure returns a ready doc). Prefer the single-action tools (create_folder, rename_folder, move_to_folder, set_icon, delete_folder) for small changes; use this to lay out a full structure at once. Send the complete \`doc\` and the \`version\` get_content_structure returned (0 when the project has none yet). Validated exactly like the dashboard: unique folder ids, every parentId/folderId must be a folder in the doc, no cycles, depth at most 4, names 1-80 chars, lucide icon names. A 412 VERSION_CONFLICT means someone changed it since you read it: call get_content_structure again and re-apply your change. Returns the new version and a diff (folders added, removed, renamed or moved; items moved; icons changed). ${STRUCTURE_NOTE}`,
|
|
3084
|
+
z.object({
|
|
3085
|
+
doc: z.object({
|
|
3086
|
+
schema: z.literal(1),
|
|
3087
|
+
folders: z.array(z.object({
|
|
3088
|
+
id: z.string().min(1).describe("your own stable id, up to 64 chars"),
|
|
3089
|
+
parentId: z.string().nullable().describe("parent folder id; null = top level"),
|
|
3090
|
+
name: z.string(),
|
|
3091
|
+
icon: z.string().nullable().describe("lucide icon name or null"),
|
|
3092
|
+
sort: z.number()
|
|
3093
|
+
})),
|
|
3094
|
+
items: z.array(z.object({
|
|
3095
|
+
kind: z.enum(["page", "collection"]),
|
|
3096
|
+
id: z.string().min(1).describe("page id or collection (content model) id"),
|
|
3097
|
+
folderId: z.string().nullable().describe('folder id; "home:root" = pinned at the top level; null = its home (Static pages for a page, the Collections list for a collection)'),
|
|
3098
|
+
sort: z.number(),
|
|
3099
|
+
icon: z.string().nullable()
|
|
3100
|
+
}))
|
|
3101
|
+
}).describe("the complete document"),
|
|
3102
|
+
version: z.number().int().min(0).describe("the version get_content_structure returned")
|
|
3103
|
+
}).shape,
|
|
3104
|
+
async (c, a) => structureResult(await data(c, "PUT", `/management/content-structure`, { doc: a.doc, version: a.version }))
|
|
3105
|
+
),
|
|
3106
|
+
def(
|
|
3107
|
+
"create_folder",
|
|
3108
|
+
"Create a Content sidebar folder",
|
|
3109
|
+
`Create a folder in the Content sidebar, at the top level or inside \`parentId\`, placed after what is already there. Returns the new \`folderId\`. Reads, applies and writes with the version check, retrying once on a conflict. ${STRUCTURE_NOTE}`,
|
|
3110
|
+
z.object({
|
|
3111
|
+
name: z.string().min(1).describe("the folder's label, 1-80 chars"),
|
|
3112
|
+
parentId: z.string().min(1).optional().describe("optional parent folder id (from get_content_structure); omit it, or send 'root', for the top level"),
|
|
3113
|
+
icon: z.string().min(1).optional().describe("optional lucide icon name, e.g. 'folder'")
|
|
3114
|
+
}).shape,
|
|
3115
|
+
async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/folders`, { name: a.name, parentId: a.parentId, icon: a.icon }))
|
|
3116
|
+
),
|
|
3117
|
+
def(
|
|
3118
|
+
"rename_folder",
|
|
3119
|
+
"Rename a Content sidebar folder",
|
|
3120
|
+
`Rename a Content sidebar folder. ${STRUCTURE_NOTE}`,
|
|
3121
|
+
z.object({
|
|
3122
|
+
folderId: z.string().min(1).describe("folder id (from get_content_structure)"),
|
|
3123
|
+
name: z.string().min(1).describe("the new label, 1-80 chars")
|
|
3124
|
+
}).shape,
|
|
3125
|
+
async (c, a) => structureResult(await data(c, "PATCH", `/management/content-structure/folders/${encodeURIComponent(s(a.folderId))}`, { name: a.name }))
|
|
3126
|
+
),
|
|
3127
|
+
def(
|
|
3128
|
+
"move_to_folder",
|
|
3129
|
+
"Move into a Content sidebar folder",
|
|
3130
|
+
`Move a page, a collection or a folder into a folder. folderId null moves it back to its home: Static pages for a page, the Collections 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_NOTE}`,
|
|
3131
|
+
z.object({
|
|
3132
|
+
kind: z.enum(["page", "collection", "folder"]),
|
|
3133
|
+
id: z.string().min(1).describe("the page id, collection (content model) id, or folder id"),
|
|
3134
|
+
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 (Static pages, the Collections list, or the top level for a folder)")
|
|
3135
|
+
}).shape,
|
|
3136
|
+
async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/move`, { kind: a.kind, id: a.id, folderId: a.folderId }))
|
|
3137
|
+
),
|
|
3138
|
+
def(
|
|
3139
|
+
"set_icon",
|
|
3140
|
+
"Set a Content sidebar icon",
|
|
3141
|
+
`Set or clear the sidebar icon of a page, a collection or a folder. ${STRUCTURE_NOTE}`,
|
|
3142
|
+
z.object({
|
|
3143
|
+
kind: z.enum(["page", "collection", "folder"]),
|
|
3144
|
+
id: z.string().min(1).describe("the page id, collection (content model) id, or folder id"),
|
|
3145
|
+
icon: z.string().min(1).nullable().describe("lucide icon name, e.g. 'file-text'; null clears it")
|
|
3146
|
+
}).shape,
|
|
3147
|
+
async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/icon`, { kind: a.kind, id: a.id, icon: a.icon }))
|
|
3148
|
+
),
|
|
3149
|
+
def(
|
|
3150
|
+
"delete_folder",
|
|
3151
|
+
"Delete a folder (its contents move up; nothing is deleted)",
|
|
3152
|
+
`Delete a folder (its contents move up; nothing is deleted). NOTHING ELSE IS DELETED: its pages, collections and sub-folders move up to the folder's parent, in the same order. ${STRUCTURE_NOTE}`,
|
|
3153
|
+
z.object({ folderId: z.string().min(1).describe("folder id (from get_content_structure)") }).shape,
|
|
3154
|
+
async (c, a) => structureResult(await data(c, "DELETE", `/management/content-structure/folders/${encodeURIComponent(s(a.folderId))}`))
|
|
3155
|
+
),
|
|
2930
3156
|
def(
|
|
2931
3157
|
"list_redirects",
|
|
2932
3158
|
"List redirects",
|
|
@@ -3169,14 +3395,14 @@ function buildToolDefs(deps) {
|
|
|
3169
3395
|
def(
|
|
3170
3396
|
"update_page",
|
|
3171
3397
|
"Edit a page",
|
|
3172
|
-
"Edit a page: title, slug, SEO metaTitle/metaDescription, publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first). It does NOT change the field SCHEMA \u2014 use add_page_field / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call. " + SECTION_DOCTRINE,
|
|
3173
|
-
z.object({ 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(), status: z.enum(["draft", "published"]).optional() }).shape,
|
|
3398
|
+
"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). It does NOT change the field SCHEMA \u2014 use add_page_field / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call. 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). " + SECTION_DOCTRINE,
|
|
3399
|
+
z.object({ 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() }).shape,
|
|
3174
3400
|
async (c, a) => {
|
|
3175
3401
|
const res = await c.fetchJSON(
|
|
3176
3402
|
c.url(`/management/pages/${s(a.pageId)}/meta`),
|
|
3177
3403
|
{
|
|
3178
3404
|
method: "PATCH",
|
|
3179
|
-
body: JSON.stringify({ title: a.title, slug: a.slug, blockJson: a.blockJson, metaTitle: a.metaTitle, metaDescription: a.metaDescription, status: a.status })
|
|
3405
|
+
body: JSON.stringify({ title: a.title, slug: a.slug, blockJson: a.blockJson, metaTitle: a.metaTitle, metaDescription: a.metaDescription, schemaType: a.schemaType, schema: a.schema, status: a.status })
|
|
3180
3406
|
}
|
|
3181
3407
|
);
|
|
3182
3408
|
const note = res.redirectNote;
|
|
@@ -3235,7 +3461,7 @@ function buildToolDefs(deps) {
|
|
|
3235
3461
|
def(
|
|
3236
3462
|
"get_next_steps",
|
|
3237
3463
|
"Get what to do next",
|
|
3238
|
-
|
|
3464
|
+
`What is still unfinished in the connected project, as the platform sees it \u2014 pages you created without a meta description, drafts never published, collections with no entries, forms nobody is notified about, writes waiting for human approval. Each item cites the count it reacted to. Call it AFTER a batch of edits to catch what you left behind, and before telling the user you are done. When it lists \`organise-content\`, the project's pages and collections are in no folder: read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure and apply it with set_content_structure once the user agrees.`,
|
|
3239
3465
|
z.object({}).shape,
|
|
3240
3466
|
async (c) => ok("Next steps.", await data(c, "GET", `/management/insights/next-steps`))
|
|
3241
3467
|
),
|
|
@@ -5797,9 +6023,23 @@ function buildServer(deps) {
|
|
|
5797
6023
|
// 🔴 The plain-language ask, routed — the stdio twin of the hosted connector's line
|
|
5798
6024
|
// (src/routes/mcp/index.ts MCP_INSTRUCTIONS). "Make my site editable" has one recipe and
|
|
5799
6025
|
// one definition of done; without this an agent converts the page it landed on and stops.
|
|
5800
|
-
instructions: "When the user asks to make a site or all of its pages editable, to convert it, or to bind its fields: this is playbook \xA713. Read `bettercms://playbook/schema` \xA713, call get_binding_report and get_conversion_brief { complete: true }, convert EVERY route the brief lists, and finish only when get_binding_report shows coverage.pending empty on every route \u2014 not when the first page works. On a workspace-wide connection pass projectId on every call; never ask the user to re-scope the connection. When the user asks to componentize the whole site, to turn every section into a component, or to build a component library from the site: this is playbook \xA712. Read `bettercms://playbook/schema` \xA712, start with get_site_composition, and use the batch tools \u2014 create_components, compose_pages, update_layout with `commands`, publish_components \u2014 rather than one call per component. Finish with get_site_composition and tell the user what the platform does not model (cookie banners, modals, breadcrumbs, pagination) and which components still need the owner's approval in the dashboard before they can be published. BetterCMS never executes a customer's Section renderer or app code. An ordinary MCP connection is not a push runner: explicitly poll list_section_validation_requests, claim one request at an exact git commit, run implementation and responsive checks inside the user's own repository and real app shell, then submit manifest + validation with that requestId and complete it\u2014or truthfully fail it when implementation/evidence is missing. Never invent a manifest, a passing validation, or visual evidence; these tools cannot grant the separate human Visual Approval required for publication."
|
|
6026
|
+
instructions: "When the user asks to make a site or all of its pages editable, to convert it, or to bind its fields: this is playbook \xA713. Read `bettercms://playbook/schema` \xA713, call get_binding_report and get_conversion_brief { complete: true }, convert EVERY route the brief lists, and finish only when get_binding_report shows coverage.pending empty on every route \u2014 not when the first page works. On a workspace-wide connection pass projectId on every call; never ask the user to re-scope the connection. When the user asks to componentize the whole site, to turn every section into a component, or to build a component library from the site: this is playbook \xA712. Read `bettercms://playbook/schema` \xA712, start with get_site_composition, and use the batch tools \u2014 create_components, compose_pages, update_layout with `commands`, publish_components \u2014 rather than one call per component. Finish with get_site_composition and tell the user what the platform does not model (cookie banners, modals, breadcrumbs, pagination) and which components still need the owner's approval in the dashboard before they can be published. BetterCMS never executes a customer's Section renderer or app code. An ordinary MCP connection is not a push runner: explicitly poll list_section_validation_requests, claim one request at an exact git commit, run implementation and responsive checks inside the user's own repository and real app shell, then submit manifest + validation with that requestId and complete it\u2014or truthfully fail it when implementation/evidence is missing. Never invent a manifest, a passing validation, or visual evidence; these tools cannot grant the separate human Visual Approval required for publication. " + // The default after authoring (the structure standard): organise what you made. Same
|
|
6027
|
+
// sentence as the hosted connector's MCP_INSTRUCTIONS.
|
|
6028
|
+
STRUCTURE_DEFAULT_INSTRUCTION
|
|
5801
6029
|
}
|
|
5802
6030
|
);
|
|
6031
|
+
server.registerResource(
|
|
6032
|
+
"structure-playbook",
|
|
6033
|
+
STRUCTURE_PLAYBOOK_URI,
|
|
6034
|
+
{
|
|
6035
|
+
title: "BetterCMS Content structure playbook",
|
|
6036
|
+
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 grouped by the reference graph, shared taxonomies in Other content, Static pages for the rest, at most 2 levels. With a worked set_content_structure payload.",
|
|
6037
|
+
mimeType: "text/markdown"
|
|
6038
|
+
},
|
|
6039
|
+
() => ({
|
|
6040
|
+
contents: [{ uri: STRUCTURE_PLAYBOOK_URI, mimeType: "text/markdown", text: STRUCTURE_PLAYBOOK }]
|
|
6041
|
+
})
|
|
6042
|
+
);
|
|
5803
6043
|
server.registerResource(
|
|
5804
6044
|
"schema-playbook",
|
|
5805
6045
|
PLAYBOOK_URI,
|