@bettercms-ai/mcp 0.73.2 → 0.75.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
@@ -2,12 +2,12 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { realpathSync } from "fs";
5
- import { fileURLToPath as fileURLToPath2 } from "url";
5
+ import { fileURLToPath as fileURLToPath3 } from "url";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { loadConfig, FileTokenStore, DeviceAuthClient } from "@bettercms-ai/device-auth";
8
8
 
9
9
  // src/server.ts
10
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
  import { BetterCMS } from "@bettercms-ai/sdk";
12
12
 
13
13
  // src/tools.ts
@@ -2758,11 +2758,11 @@ function patchAt(fields, [key, ...rest], edit) {
2758
2758
  let next = null;
2759
2759
  if (rest.length === 0) next = edit(field);
2760
2760
  else {
2761
- for (const list of CHILD_LISTS) {
2762
- const kids = getIn(field, list);
2761
+ for (const list2 of CHILD_LISTS) {
2762
+ const kids = getIn(field, list2);
2763
2763
  const patched = Array.isArray(kids) ? patchAt(kids, rest, edit) : null;
2764
2764
  if (patched) {
2765
- next = setIn(field, list, patched);
2765
+ next = setIn(field, list2, patched);
2766
2766
  break;
2767
2767
  }
2768
2768
  }
@@ -2777,8 +2777,8 @@ function fieldAt(fields, keys) {
2777
2777
  function allKeys(fields) {
2778
2778
  return fields.flatMap((f) => [
2779
2779
  String(f.key),
2780
- ...CHILD_LISTS.flatMap((list) => {
2781
- const kids = getIn(f, list);
2780
+ ...CHILD_LISTS.flatMap((list2) => {
2781
+ const kids = getIn(f, list2);
2782
2782
  return Array.isArray(kids) ? allKeys(kids) : [];
2783
2783
  })
2784
2784
  ]);
@@ -2936,6 +2936,19 @@ function listsTool(name, sets) {
2936
2936
  if (ABM_TOOLS.has(name)) return sets.has("abm");
2937
2937
  return !sets.has("core") || !LISTING_TAIL.has(name);
2938
2938
  }
2939
+ var ALWAYS_LOADED = /* @__PURE__ */ new Set([
2940
+ "get_next_steps",
2941
+ "editor_setup",
2942
+ "get_deploy_status",
2943
+ "get_conversion_brief",
2944
+ "get_binding_report",
2945
+ "list_projects",
2946
+ "list_pages",
2947
+ "get_page",
2948
+ "list_content_models"
2949
+ ]);
2950
+ var toolMeta = (name) => ALWAYS_LOADED.has(name) ? { "anthropic/alwaysLoad": true } : void 0;
2951
+ var TOOL_LOADING_INSTRUCTION = `TOOL SCHEMAS: a client with tool search lists most BetterCMS tools by name only. Before the first call to a tool whose schema you have not been shown, load it with the client's tool search, and load every tool the task needs in ONE search (for a renaming pass: update_field, update_content_model, update_component, list_components). A call made without the schema is refused by the client before it reaches BetterCMS ("Invalid tool parameters"): that is this, never a BetterCMS error, and never a reason to change the arguments.`;
2939
2952
 
2940
2953
  // src/tool-text.ts
2941
2954
  var ADD_FIELD_DESCRIPTION = "Append ONE field to a content model (`modelId`) or to a page's schema (`pageId`) \u2014 additive, never overwrites; a key already in use is refused. For a section/zone add ONE 'group' (fixed block) or 'repeater' (repeating list) carrying its child `fields`, not the zone's inner fields as separate top-level fields. Pass `after` to insert it after an existing top-level field instead of at the end; `ui`, `helpText`, `showIf`, `placement` and `fieldsetId` declare how it presents in the editor \u2014 read them back with get_content_model or get_page. On a page a 'group' is stored as type 'array' with `config.zones.nonRepeatable`: entry and delivery data carry `<key>.nonRepeatable.<sub>`, while data-bcms-field paths are `<key>.<sub>`. Schema only \u2014 never places a section; use componentize_sections to turn a group into a section. `multiselect` stores a string[] of `config.options`; `icon` is a BetterCMS icon name or an uploaded icon. To change a field that exists, use update_field. V2 keys (object, openMode, advanced, helpMore, recommendedMax, config.scale): \xA714 of bettercms://playbook/schema.";
@@ -2991,6 +3004,7 @@ var PARAM_TEXT = {
2991
3004
  fromBuiltin: "a builtin:* id from list_components: copies it into the project; fields you send win",
2992
3005
  componentPreview: "a slot row's look: {title?, subtitle?, media?} TOP-LEVEL prop keys, fallback?, icon?; null clears. \xA73 of bettercms://playbook/schema",
2993
3006
  componentIcon: "the block icon, a dock icon name; null clears. \xA73 of bettercms://playbook/schema",
3007
+ componentTabs: "/kit tabs no top-level prop sits in: [{id,label,icon?,order?}]. \xA73 of bettercms://playbook/schema",
2994
3008
  describes: "for editors: what it is and holds; omitted: written for you",
2995
3009
  runtimeBundle: "instead of pageId: the server-app bundle cleanup",
2996
3010
  pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
@@ -3308,8 +3322,8 @@ var fieldShape = {
3308
3322
  rules: z.array(
3309
3323
  z.object({
3310
3324
  field: z.string().describe("the BARE KEY of another field on this same model"),
3311
- op: z.enum(["eq", "neq", "in", "isSet", "notEmpty", "empty"]),
3312
- value: z.union([z.string(), z.array(z.string())]).optional().describe("omit for 'isSet'/'notEmpty' (the source holds any value) and 'empty' (it holds none); required for every other op")
3325
+ op: z.enum(["eq", "neq", "in", "notIn", "gt", "lt", "isSet", "notEmpty", "empty"]),
3326
+ value: z.union([z.string(), z.array(z.string())]).optional().describe(`omit for 'isSet'/'notEmpty' (the source holds any value) and 'empty' (it holds none); 'gt'/'lt' take one number as a string ("10"); required for every other op`)
3313
3327
  })
3314
3328
  )
3315
3329
  }).optional().describe(
@@ -3565,6 +3579,7 @@ function publishedLines(outcome, summary) {
3565
3579
  if (outcome.status !== "live") return [`${summary} Publish status: ${outcome.status}.`];
3566
3580
  return [`${summary} Published: ${outcome.live > 0 ? `${outcome.live} ${outcome.live === 1 ? "value" : "values"} live on the served page` : "live"}.`];
3567
3581
  }
3582
+ var DOCK_UI_NEXT = "Give every prop of this component its V2 ui (control, tab, group, collapsed, layout, preview, itemLabel, icon) by playbook \xA714 and the closest bettercms://kit entry: get_component, then ONE update_component with propUi, propTab and propHelpText (the field-ui prompt). The setup gate's dock row stays open until it is done.";
3568
3583
  function ok(summary, data) {
3569
3584
  return {
3570
3585
  content: [
@@ -3995,14 +4010,18 @@ function buildToolDefs(deps) {
3995
4010
  match: z.enum(["all", "any"]),
3996
4011
  rules: z.array(z.object({
3997
4012
  field: z.string().min(1),
3998
- op: z.enum(["eq", "neq", "in", "isSet", "notEmpty", "empty"]),
4013
+ op: z.enum(["eq", "neq", "in", "notIn", "gt", "lt", "isSet", "notEmpty", "empty"]),
3999
4014
  value: z.union([z.string(), z.array(z.string())]).optional()
4000
4015
  })).min(1).max(20)
4001
- }).optional().describe("show this prop only while SIBLING props (by key) match: {match:'all'|'any', rules:[{field, op:'eq'|'neq'|'in'|'isSet'|'notEmpty'|'empty', value?}]}. 'isSet', 'notEmpty' and 'empty' take no value. Editor-only."),
4016
+ }).optional().describe("show this prop only while SIBLING props (by key) match: {match:'all'|'any', rules:[{field, op:'eq'|'neq'|'in'|'notIn'|'gt'|'lt'|'isSet'|'notEmpty'|'empty', value?}]}. 'isSet', 'notEmpty' and 'empty' take no value; 'gt'/'lt' one number as a string. Editor-only."),
4002
4017
  // V2 editor-only keys, the model field's (playbook §14). Declared: this is a strip site.
4003
4018
  helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
4004
4019
  advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
4005
4020
  recommendedMax: z.number().int().positive().optional().describe(PARAM_TEXT.recommendedMax),
4021
+ // F1.x `unique` (a list's items / a table's rows distinct) and the server's `stale` marker:
4022
+ // undeclared here, an agent's `unique` and an echoed `stale` were dropped with a 200.
4023
+ unique: z.boolean().optional(),
4024
+ stale: z.boolean().optional(),
4006
4025
  openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).optional().describe("object/group/table props: how the value (or each row) opens"),
4007
4026
  // ⚠ A STRIP SITE, exactly like OutField above: this is a `z.object`, so a key the model
4008
4027
  // sends and this shape does not declare is dropped BEFORE the request leaves — a 200 with
@@ -4094,6 +4113,7 @@ function buildToolDefs(deps) {
4094
4113
  description: z.string().optional().describe(PARAM_TEXT.describes),
4095
4114
  preview: componentPreviewArg.optional(),
4096
4115
  icon: dockIconArg.nullable().optional().describe(PARAM_TEXT.componentIcon),
4116
+ tabs: z.array(z.object({ id: z.string(), label: z.string(), icon: z.string().optional(), order: z.number().int().optional() })).nullable().optional().describe(PARAM_TEXT.componentTabs),
4097
4117
  blockJson: z.array(blockObject).default([]).describe("the component's block tree"),
4098
4118
  props: z.array(componentPropObject).default([]).describe(
4099
4119
  "overridable fields. DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch: only the leaves a prop targets are click-to-edit. Each one may carry `ui` to say how its control presents in the inspector \u2014 a collapsed group, what titles and thumbnails a table's rows, whether that table's rows can be dragged at all. 'button' is ONE unit {label, href, variant?, size?, arrow?, icon?, newTab?} on a button block's 'props', never a text prop plus a url prop. 'link' an href, 'date' YYYY-MM-DD, 'color' a hex, 'file' {url, name?}, 'icon' an icon name or an image, 'multiselect' a string[] from config.options (required). " + SLOT_MULTIPLE_NOTE + " " + PROP_V2_NOTE
@@ -6039,13 +6059,13 @@ ${notes.join("\n")}` : summary, created);
6039
6059
  handler: guard(
6040
6060
  async (args) => withClient(async (client) => {
6041
6061
  if (args.detail) {
6042
- const list2 = await client.listComponents();
6043
- return ok(`${list2.length} component(s) in the bound project (full definitions).`, list2);
6062
+ const list3 = await client.listComponents();
6063
+ return ok(`${list3.length} component(s) in the bound project (full definitions).`, list3);
6044
6064
  }
6045
- const list = await client.listComponents({ select: "summary" });
6065
+ const list2 = await client.listComponents({ select: "summary" });
6046
6066
  return ok(
6047
- `${list.length} component(s) in the bound project.`,
6048
- list.map((cmp) => ({
6067
+ `${list2.length} component(s) in the bound project.`,
6068
+ list2.map((cmp) => ({
6049
6069
  id: cmp.id,
6050
6070
  name: cmp.name,
6051
6071
  slug: cmp.slug,
@@ -6235,7 +6255,7 @@ ${lines.join("\n")}`, found);
6235
6255
  const { component, replaced } = await client.extractComponent(args);
6236
6256
  return ok(
6237
6257
  `Created component '${component.name}' (id ${component.id}) and repointed ${replaced} sections to it. Page drafts changed; publish those pages to make it live.`,
6238
- component
6258
+ { ...component, nextAction: DOCK_UI_NEXT }
6239
6259
  );
6240
6260
  })
6241
6261
  )
@@ -6462,7 +6482,8 @@ function registerTools(server, deps, tools = process.env.BETTERCMS_MCP_TOOLS) {
6462
6482
  };
6463
6483
  const sets = toolsetsOf(tools);
6464
6484
  for (const def of buildToolDefs(withElicit).filter((d) => listsTool(d.name, sets))) {
6465
- server.registerTool(def.name, def.config, def.handler);
6485
+ const _meta = toolMeta(def.name);
6486
+ server.registerTool(def.name, _meta ? { ...def.config, _meta } : def.config, def.handler);
6466
6487
  }
6467
6488
  }
6468
6489
 
@@ -6490,6 +6511,11 @@ DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every
6490
6511
 
6491
6512
  ## 2. The decision tree
6492
6513
 
6514
+ Designing ANY section? -> FIRST find its closest kit entry in bettercms://kit
6515
+ (by category, then edge tag); follow that entry's
6516
+ mcp notes and avoid its pitfalls (tag rules below).
6517
+ Its props carry NO target: give each one a target
6518
+ into YOUR blockJson (\xA73) before create_component
6493
6519
  Repeated visual region on a page? -> a COMPONENT with sectionType (a Section)
6494
6520
  ...and it comes in more than one look? -> siblings sharing that sectionType = VARIANTS
6495
6521
  A stack of mixed, reorderable content? -> kind:'block' models + a \`modular\` field
@@ -6510,6 +6536,29 @@ DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every
6510
6536
  Site-wide nav/footer/modal? -> the LAYOUT (get_layout/update_layout), never a
6511
6537
  page section and never one component per page
6512
6538
 
6539
+ Kit edge tags, one rule each (long form with entry ids: the skill's kit-scenarios reference):
6540
+
6541
+ deep-nesting levels = content kinds, not wrappers; past 6, split the deepest subtree out
6542
+ repeater one band holds the list; never store what order says (step no., totals)
6543
+ polymorphic-array each item names its _type; merge consecutive prose into one text item
6544
+ slots children differ in shape; span/surface/phone order go in slotSettings
6545
+ variants column count, side, carousel = a variant, never a free number or boolean
6546
+ conditional a select reveals one branch; fill only it (hidden values still publish)
6547
+ references reference the entry/product/page; store only what differs as overrides
6548
+ dynamic-query store collection + filters + sort + limit; import the items as entries first
6549
+ global-synced same on ~80% of pages = one global; an instance overrides only what differs
6550
+ localized match locales by position, never text; never translate on import; no hreflang
6551
+ rich-text one body; callouts, tables, media, instances are islands; no h1, no chrome
6552
+ media-heavy largest source, real alt, video poster + caption file, positions in %
6553
+ embed store provider + id or canonical URL, never the script; iframes need a title
6554
+ form a Form reference by id; never the inputs copied into the section
6555
+ commerce the store owns price, stock, SKU, options, cart: reference, never copy
6556
+ personalization only with evidence; complete default, audiences hold only the differences
6557
+ validation numbers as numbers, datetimes with offset; check cross-field rules on import
6558
+ a11y every visual-only element gets text: logo name, pin label, iframe title
6559
+ seo-structured JSON-LD is emitted from content; never model meta or JSON-LD fields (\xA75b)
6560
+ responsive-layout per-breakpoint values as settings that inherit desktop; spacing as tokens
6561
+
6513
6562
  ## 3. Anatomy of a real section component
6514
6563
 
6515
6564
  A component that is only headings and text renders as an unstyled stack \u2014 plain text on white
@@ -6547,8 +6596,9 @@ dashboard's prop editor renders. \`helpText\` is three-state: a string is yours,
6547
6596
  purpose (send it for a prop whose /kit schema has no description; nothing refills it), and omitted
6548
6597
  lets the platform write one. They are HINTS: nothing on the write path refuses a placement
6549
6598
  that leaves a required prop empty, so \`get_site_composition\` is what reports one.
6550
- A /kit schema's document-level \`preview\` \u2192 the component's \`preview\`: how ONE instance reads as a ROW in a parent's slot list. {title?, subtitle?, media?} each name a TOP-LEVEL prop key (422 \`preview_key_unknown\` otherwise), \`fallback?\` is a literal row label (\u2264 120) and \`icon?\` a dock icon name; null clears it.
6599
+ A /kit schema's document-level \`preview\` \u2192 the component's \`preview\`: how ONE instance reads as a ROW in a parent's slot list. {title?, subtitle?, media?} each name a TOP-LEVEL prop key or a dotted path into one (\`slides.0.heading\`; 422 \`preview_key_unknown\` otherwise), \`fallback?\` is a literal row label (\u2264 120) and \`icon?\` a dock icon name; null clears it.
6551
6600
  A /kit schema's document-level \`icon\` \u2192 the component's \`icon\` (a dock icon name from bettercms://icons/dock, null clears): the block icon on the root crumb, slot rows and slot child frames.
6601
+ A /kit schema's \`groups\` \u2192 each prop's \`ui.tab\` (defined on the first top-level prop in it; a nested field's \`group\` \u2192 its \`ui.tab: {id}\`); a group NO top-level prop sits in \u2192 the component's \`tabs\` ([{id, label, icon?, order?}], 422 \`component_tab_invalid\` for a prop's own tab). A slot's \`of.allow\` \u2192 \`config.allow\` (block names); a scalar list's item settings (\`of[0]\` validation, description, localized) \u2192 \`config.item\`.
6552
6602
 
6553
6603
  **Prop types, one rule each.** 'select' REQUIRES \`config.options\` (a default must be one of them). 'group'/'table'/'object' REQUIRE \`config.fields\` (\`[{key,label,type,options?,fields?}]\`); a group writes an object, a table an array of them (\`config.mode: 'data'\`: ONE V2 table value, no fields). 'number' takes optional \`config.min/max/step\`. 'slot' holds ONE nested component instance: \`target.path: 'props'\` on an EMPTY component block (\`props.componentId: ""\`), \`config.componentIds\` restricts fills, defaultValue \`{componentId, overrides?}\`. 'form' holds ONE Form document: \`target.path: 'props'\` on an EMPTY form block (\`props.formId: ""\`), defaultValue \`{formId}\`, never the hydrated \`{formId, form}\` (400); do NOT re-declare the form's FIELDS as props, the Form document owns them. 'button' is ONE unit \`{label, href, variant?, size?, arrow?, icon?, newTab?}\` on a button block's 'props', never a text prop plus a url prop. 'link' an href, 'date' YYYY-MM-DD, 'color' a hex, 'file' \`{url, name?}\`, 'icon' an icon name or an image, 'multiselect' a string[] from \`config.options\` (required).
6554
6604
 
@@ -7263,7 +7313,7 @@ conversion is yours to write.
7263
7313
  markup inside a \`text\` field.
7264
7314
  **Prose MIGRATED from another CMS needs one extra check.** Every block's \`_type\` must be one
7265
7315
  this platform stores \u2014 \`block\`, \`table\` (\`{_type:"table", rows:[{_key, cells:[{_key,
7266
- content:[...blocks], header?, group?, highlight?, sortable?}], group?, footer?, highlight?}], caption?, notes?, rowGroups?, columnGroups?, filterable?, jsonLd?: {type: "ItemList"|"Dataset"|"Offers", axis?, priceRow?}, design?: {responsive?: "stack"|"cards", sticky?}}\`,
7316
+ content:[...blocks], header?, group?, highlight?, sortable?, note?, column?: {type?: "text"|"number"|"money"|"boolean"|"rating"|"link", unit?, currency?, decimals?, align?, trueLabel?, falseLabel?, max?, description?} (first row only), type?, value?: number|boolean (content keeps its words)}], group?, footer?, highlight?, type?}], caption?, notes?, rowGroups?, columnGroups?, filterable?, jsonLd?: {type: "ItemList"|"Dataset"|"Offers", axis?, priceRow?}, design?: {responsive?: "stack"|"cards", sticky?}, footnotes?: [{_key, text}]}\`,
7267
7317
  groups \`[{_key, label}]\`; Sanity's row of plain strings, \`rows:[{cells:["a","b"]}]\`,
7268
7318
  is admitted too), \`bcmsCode\`, \`bcmsDivider\`, \`bcmsMention\`, \`pullQuote\`
7269
7319
  (\`{quote, name, role?, link?}\`), \`keyTakeaways\` (\`{title?, points:[...strings]}\`,
@@ -7502,7 +7552,7 @@ Importance orders a screen: required fields first, then array order.
7502
7552
  | Calm defaults for rare settings | \`advanced: true\` |
7503
7553
  | A soft length warning (amber, never a refusal) | \`recommendedMax: 60\` |
7504
7554
  | A marketer cannot change it | \`readOnly: 'developer'\` plus \`lockReason\` |
7505
- | A field that shows only sometimes | \`showIf\` (it SHOWS the field): ops \`eq\`, \`neq\`, \`in\`, \`isSet\`, \`notEmpty\`, \`empty\` |
7555
+ | A field that shows only sometimes | \`showIf\` (it SHOWS the field): ops \`eq\`, \`neq\`, \`in\`, \`notIn\`, \`gt\`, \`lt\` (one number as a string), \`isSet\`, \`notEmpty\`, \`empty\` |
7506
7556
  | A good row title and thumbnail | \`ui.preview: { title, subtitle, media, fallback }\` (\`fallback\` is a literal label) |
7507
7557
  | Labels that classify or filter (topics, keywords), never a feature list | \`type: 'tags'\`: a string[], trimmed, de-duplicated, each \u226464 characters; \`config.options\` are suggestions, \`config.max\`, \`config.allowCustom: false\` closes the list (\`tags_value_invalid\`) |
7508
7558
  | A star rating | \`type: 'rating'\`: a number from 0 to \`config.max\` (5), \`config.step\` 1 or 0.5 (\`rating_value_invalid\`) |
@@ -7620,6 +7670,7 @@ disagrees with the array the moment anyone drags a row in the builder.
7620
7670
  // src/prompts.ts
7621
7671
  var STRUCTURE_RULE = `### Page structure (non-negotiable)
7622
7672
  ${SECTION_DOCTRINE}`;
7673
+ var DOCK_UI_STEP = "EDITOR CHROME: for every component, run the field-ui lane (the `field-ui` prompt, playbook \xA714): `get_component`, then one `update_component` with `propUi`, `propTab` and `propHelpText` giving each prop its V2 ui (control, tab, group, collapsed, layout, preview, itemLabel, icon) after the closest `bettercms://kit` entry. The gate's `dock` row stays open until each has it, or a true `default-controls` exception.";
7623
7674
  var CONVERT_SITE_TEXT = (projectId) => `Convert my site so every visible element is editable in BetterCMS${projectId ? `, project ${projectId}` : ""}.
7624
7675
  Do not stop at the plan or at a partial receipt \u2014 finish the whole site, never editing a class or style.
7625
7676
 
@@ -7650,8 +7701,8 @@ Do not stop at the plan or at a partial receipt \u2014 finish the whole site, ne
7650
7701
  do the same with \`npx @bettercms-ai/convert@latest --upgrade-element-styles --root . --dry-run\`. Run \`--verify-styles\` again
7651
7702
  and restore what it lists (the marker, and the inline style from \`block.style\` or \`fieldAttrs\`, are allowed; changing a class
7652
7703
  or an existing style is not). Deploy, poll, call \`get_next_steps\` again and run \`receipt.gate.nextAction\` until \`receipt.complete\` is true; what else it lists, tell me by name. Not listed: say so; invent no work. Its \`receipt.gate\` fails a route on \`sections\` or \`style\` while I could not add a section to that page, reorder it or space it: run each such row's \`fix\` too. A write that answers \`held\`, or a publish whose \`outcome\` is \`partial\`, is not done until its action is taken.
7653
- 11. Ask me whether to commit and push to my repository, commit only, or leave it uncommitted (unpushed, the
7654
- next rebuild from my repo drops it). Touch git only as I answer.`;
7704
+ 11. ${DOCK_UI_STEP}
7705
+ 12. Ask me whether to commit and push to my repository, commit only, or leave it uncommitted (unpushed, the next rebuild from my repo drops it). Touch git only as I answer.`;
7655
7706
  var FIELD_UI_PROMPT = {
7656
7707
  title: "Declare how fields LOOK in the editor (guided)",
7657
7708
  description: "Set the editor chrome of an existing schema and its component props: groups, tabs, rows, icons, item names, what titles and thumbnails a list's rows, help text, conditional visibility. Derives it from your source, then from the field shapes, and asks only what it cannot know.",
@@ -8083,6 +8134,8 @@ ${COMPONENTIZE_SITE_FLOW}
8083
8134
 
8084
8135
  ${STRUCTURE_RULE}
8085
8136
 
8137
+ Last, before the summary: ${DOCK_UI_STEP}
8138
+
8086
8139
  Confirm the component library with the user before creating it. End with the summary \xA712 asks
8087
8140
  for: the group hierarchy, each family and its variants, what you left in the site's code because
8088
8141
  the platform does not model it, and which components are still waiting for the owner's approval
@@ -8221,7 +8274,8 @@ Read bettercms://playbook/schema section 11 first, then work this order:
8221
8274
  4. PRESENTATION MANIFEST \u2014 add bcms-presentation.json to public/ (container width, type
8222
8275
  scale, nav position/background, footer surface as DTCG {"$type","$value"} entries) and
8223
8276
  redeploy, or structural draft previews render in a generic theme, not this site's design.
8224
- 5. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
8277
+ 5. ${DOCK_UI_STEP}
8278
+ 6. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
8225
8279
  a setup that has not run, and placed-but-unpublished components). You are finished only
8226
8280
  when its receipt.complete is true: run receipt.gate.nextAction, read it again after each
8227
8281
  batch, and record an item you cannot close with editor_setup { exempt } only under an allowed
@@ -8307,6 +8361,93 @@ function loadSkillDocs(dir = defaultSkillDir()) {
8307
8361
  return docs;
8308
8362
  }
8309
8363
 
8364
+ // src/kit.ts
8365
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
8366
+ import { fileURLToPath as fileURLToPath2 } from "url";
8367
+ var KIT_URI = "bettercms://kit";
8368
+ var KIT_ENTRY_URI_PREFIX = "bettercms://kit/entry/";
8369
+ var KIT_ENTRY_URI_TEMPLATE = `${KIT_ENTRY_URI_PREFIX}{id}`;
8370
+ function loadKitCatalog(path = fileURLToPath2(new URL("./kit-catalog.json", import.meta.url))) {
8371
+ return existsSync2(path) ? JSON.parse(readFileSync2(path, "utf8")) : null;
8372
+ }
8373
+ var list = (xs) => xs.map((x) => `- ${x}`).join("\n");
8374
+ function entryNotes(e, h = "###") {
8375
+ const out = [`${h} ${e.name} (\`${e.id}\`)`, "", `Tags: ${e.tags.join(", ") || "none"}. Proves: ${e.proves}`];
8376
+ if (!e.why && !e.mcp) out.push("", "No model notes in the kit for this entry: read its props.");
8377
+ if (e.why) out.push("", `**Why this shape.** ${e.why}`);
8378
+ if (e.htmlSignals.length) out.push("", "**HTML signals.**", list(e.htmlSignals));
8379
+ if (e.mcp) out.push("", `**Do.** ${e.mcp}`);
8380
+ if (e.pitfalls.length) out.push("", "**Pitfalls.**", list(e.pitfalls));
8381
+ if (e.notCarried.length) out.push("", `Not carried by a stored component: ${e.notCarried.join(", ")}.`);
8382
+ return out.join("\n");
8383
+ }
8384
+ function kitIndexText(k) {
8385
+ const byId = new Map(k.entries.map((e) => [e.id, e]));
8386
+ return [
8387
+ "# BetterCMS kit catalogue",
8388
+ "",
8389
+ `${k.entries.length} modelled website sections. Before designing a section, find the closest entry by its edge tag or category, read its notes and follow its mcp + pitfalls; its create_component props are at ${KIT_ENTRY_URI_TEMPLATE}.`,
8390
+ "",
8391
+ "## Categories",
8392
+ "",
8393
+ ...k.categories.map((c) => `- ${c.name} (${c.entries.length}): ${KIT_URI}/${c.slug}`),
8394
+ "",
8395
+ "## Edge tags",
8396
+ "",
8397
+ ...k.tags.map((t) => `- \`${t.tag}\` (${t.hint}): ${t.entries.join(", ") || "none"}`),
8398
+ "",
8399
+ "## Entries",
8400
+ "",
8401
+ ...k.categories.flatMap((c) => [
8402
+ `### ${c.name}`,
8403
+ "",
8404
+ ...c.entries.map((id) => {
8405
+ const e = byId.get(id);
8406
+ return `- \`${e.id}\` ${e.name} [${e.tags.join(", ")}]: ${e.proves}`;
8407
+ }),
8408
+ ""
8409
+ ])
8410
+ ].join("\n");
8411
+ }
8412
+ function kitCategoryText(k, slug2) {
8413
+ const c = k.categories.find((x) => x.slug === slug2);
8414
+ const byId = new Map(k.entries.map((e) => [e.id, e]));
8415
+ return [
8416
+ `# Kit: ${c.name}`,
8417
+ "",
8418
+ `${c.entries.length} entries. Each one's create_component props: ${KIT_ENTRY_URI_TEMPLATE}. Index: ${KIT_URI}.`,
8419
+ "",
8420
+ ...c.entries.map((id) => entryNotes(byId.get(id), "##") + "\n")
8421
+ ].join("\n");
8422
+ }
8423
+ function kitEntryText(e) {
8424
+ return [
8425
+ entryNotes(e, "#"),
8426
+ "",
8427
+ "## create_component payload",
8428
+ "",
8429
+ "Props in the exact shape create_component accepts. Adapt keys and copy to the site; keep the types, `ui` and config.",
8430
+ "",
8431
+ "```json",
8432
+ JSON.stringify(e.component, null, 1),
8433
+ "```",
8434
+ ""
8435
+ ].join("\n");
8436
+ }
8437
+ function kitDocs(k = loadKitCatalog()) {
8438
+ if (!k) return [];
8439
+ const doc = (uri, name, title, description, text2) => ({ uri, name, title, description, mimeType: "text/markdown", text: text2 });
8440
+ return [
8441
+ doc(KIT_URI, "kit", "BetterCMS kit catalogue", "Index of modelled website sections by category and edge tag: find the closest one before designing a section.", kitIndexText(k)),
8442
+ ...k.categories.map(
8443
+ (c) => doc(`${KIT_URI}/${c.slug}`, `kit-${c.slug}`, `Kit: ${c.name}`, `Model notes for the ${c.entries.length} ${c.name} entries: why, HTML signals, mcp, pitfalls.`, kitCategoryText(k, c.slug))
8444
+ )
8445
+ ];
8446
+ }
8447
+ function kitEntryTexts(k = loadKitCatalog()) {
8448
+ return k ? Object.fromEntries(k.entries.map((e) => [`${KIT_ENTRY_URI_PREFIX}${e.id}`, kitEntryText(e)])) : {};
8449
+ }
8450
+
8310
8451
  // src/server.ts
8311
8452
  var SERVER_NAME = "bettercms";
8312
8453
  var SERVER_VERSION = "1.5.0";
@@ -8332,7 +8473,7 @@ function buildServer(deps) {
8332
8473
  // 🔴 The plain-language ask, routed — the stdio twin of the hosted connector's line
8333
8474
  // (src/routes/mcp/index.ts MCP_INSTRUCTIONS). "Make my site editable" has one recipe and
8334
8475
  // one definition of done; without this an agent converts the page it landed on and stops.
8335
- instructions: RECEIPT_FIRST_INSTRUCTION + " 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. " + LAYOUT_CHROME_INSTRUCTION + " " + CTA_BUTTON_INSTRUCTION + " 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 componentize_sections, 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. " + // The default after authoring (the structure standard): organise what you made. Same
8476
+ instructions: TOOL_LOADING_INSTRUCTION + " " + RECEIPT_FIRST_INSTRUCTION + " 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. " + LAYOUT_CHROME_INSTRUCTION + " " + CTA_BUTTON_INSTRUCTION + " 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 componentize_sections, 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. " + // The default after authoring (the structure standard): organise what you made. Same
8336
8477
  // sentence as the hosted connector's MCP_INSTRUCTIONS.
8337
8478
  STRUCTURE_DEFAULT_INSTRUCTION + " " + SKILLS_ROUTING_INSTRUCTION + " " + DOCK_UI_ROUTING_INSTRUCTION + " " + PUBLISH_HOLD_INSTRUCTION
8338
8479
  }
@@ -8368,6 +8509,22 @@ function buildServer(deps) {
8368
8509
  for (const { uri, name, text: text2, ...meta } of loadSkillDocs()) {
8369
8510
  server.registerResource(name, uri, meta, () => ({ contents: [{ uri, mimeType: meta.mimeType, text: text2 }] }));
8370
8511
  }
8512
+ for (const { uri, name, text: text2, ...meta } of kitDocs()) {
8513
+ server.registerResource(name, uri, meta, () => ({ contents: [{ uri, mimeType: meta.mimeType, text: text2 }] }));
8514
+ }
8515
+ const kitEntries = kitEntryTexts();
8516
+ if (Object.keys(kitEntries).length) {
8517
+ server.registerResource(
8518
+ "kit-entry",
8519
+ new ResourceTemplate(KIT_ENTRY_URI_TEMPLATE, { list: void 0 }),
8520
+ { title: "Kit entry", description: "One kit entry: its model notes and the create_component props. Ids are in bettercms://kit.", mimeType: "text/markdown" },
8521
+ (uri) => {
8522
+ const text2 = kitEntries[uri.href];
8523
+ if (text2 === void 0) throw new Error(`Unknown kit entry: ${uri.href}`);
8524
+ return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: text2 }] };
8525
+ }
8526
+ );
8527
+ }
8371
8528
  registerTools(server, {
8372
8529
  auth: deps.auth,
8373
8530
  // `project` is the per-call target of a workspace-wide grant; the SDK sends it as
@@ -8395,7 +8552,7 @@ function isMainModule() {
8395
8552
  const argv1 = process.argv[1];
8396
8553
  if (!argv1) return false;
8397
8554
  try {
8398
- return realpathSync(argv1) === fileURLToPath2(import.meta.url);
8555
+ return realpathSync(argv1) === fileURLToPath3(import.meta.url);
8399
8556
  } catch {
8400
8557
  return false;
8401
8558
  }