@bettercms-ai/mcp 0.43.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 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,7 +2393,7 @@ function toField(f) {
2273
2393
  ...f.config ? { config: f.config } : {}
2274
2394
  };
2275
2395
  }
2276
- 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.';
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.';
2277
2397
  function structureResult(d) {
2278
2398
  const message = d?.message;
2279
2399
  return ok(typeof message === "string" ? message : "Updated the Content structure.", d);
@@ -2936,7 +3056,7 @@ function buildToolDefs(deps) {
2936
3056
  def(
2937
3057
  "get_content_structure",
2938
3058
  "Get the Content sidebar structure",
2939
- `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. ${STRUCTURE_NOTE}`,
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}`,
2940
3060
  z.object({}).shape,
2941
3061
  async (c) => {
2942
3062
  const d = await data(c, "GET", `/management/content-structure`);
@@ -2944,10 +3064,23 @@ function buildToolDefs(deps) {
2944
3064
  ${d.summary.outline}` : "Content structure.", d);
2945
3065
  }
2946
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
+ ),
2947
3080
  def(
2948
3081
  "set_content_structure",
2949
3082
  "Replace the Content sidebar structure",
2950
- `Replace the whole Content sidebar document in one write. 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}`,
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}`,
2951
3084
  z.object({
2952
3085
  doc: z.object({
2953
3086
  schema: z.literal(1),
@@ -2961,7 +3094,7 @@ ${d.summary.outline}` : "Content structure.", d);
2961
3094
  items: z.array(z.object({
2962
3095
  kind: z.enum(["page", "collection"]),
2963
3096
  id: z.string().min(1).describe("page id or collection (content model) id"),
2964
- folderId: z.string().nullable().describe("folder id; null = its home (Static pages for a page, the Collections list for a collection)"),
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)'),
2965
3098
  sort: z.number(),
2966
3099
  icon: z.string().nullable()
2967
3100
  }))
@@ -2976,7 +3109,7 @@ ${d.summary.outline}` : "Content structure.", d);
2976
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}`,
2977
3110
  z.object({
2978
3111
  name: z.string().min(1).describe("the folder's label, 1-80 chars"),
2979
- parentId: z.string().min(1).optional().describe("optional parent folder id (from get_content_structure); omit for the top level"),
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"),
2980
3113
  icon: z.string().min(1).optional().describe("optional lucide icon name, e.g. 'folder'")
2981
3114
  }).shape,
2982
3115
  async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/folders`, { name: a.name, parentId: a.parentId, icon: a.icon }))
@@ -2998,7 +3131,7 @@ ${d.summary.outline}` : "Content structure.", d);
2998
3131
  z.object({
2999
3132
  kind: z.enum(["page", "collection", "folder"]),
3000
3133
  id: z.string().min(1).describe("the page id, collection (content model) id, or folder id"),
3001
- folderId: z.string().min(1).nullable().describe("target folder id, or null to move it back to its home (Static pages, the Collections list, or the top level for a folder)")
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)")
3002
3135
  }).shape,
3003
3136
  async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/move`, { kind: a.kind, id: a.id, folderId: a.folderId }))
3004
3137
  ),
@@ -3262,14 +3395,14 @@ ${d.summary.outline}` : "Content structure.", d);
3262
3395
  def(
3263
3396
  "update_page",
3264
3397
  "Edit a page",
3265
- "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,
3266
- 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,
3267
3400
  async (c, a) => {
3268
3401
  const res = await c.fetchJSON(
3269
3402
  c.url(`/management/pages/${s(a.pageId)}/meta`),
3270
3403
  {
3271
3404
  method: "PATCH",
3272
- 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 })
3273
3406
  }
3274
3407
  );
3275
3408
  const note = res.redirectNote;
@@ -3328,7 +3461,7 @@ ${d.summary.outline}` : "Content structure.", d);
3328
3461
  def(
3329
3462
  "get_next_steps",
3330
3463
  "Get what to do next",
3331
- "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.",
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.`,
3332
3465
  z.object({}).shape,
3333
3466
  async (c) => ok("Next steps.", await data(c, "GET", `/management/insights/next-steps`))
3334
3467
  ),
@@ -5890,9 +6023,23 @@ function buildServer(deps) {
5890
6023
  // 🔴 The plain-language ask, routed — the stdio twin of the hosted connector's line
5891
6024
  // (src/routes/mcp/index.ts MCP_INSTRUCTIONS). "Make my site editable" has one recipe and
5892
6025
  // one definition of done; without this an agent converts the page it landed on and stops.
5893
- 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
5894
6029
  }
5895
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
+ );
5896
6043
  server.registerResource(
5897
6044
  "schema-playbook",
5898
6045
  PLAYBOOK_URI,