@bettercms-ai/mcp 0.73.2 → 0.74.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
  ]);
@@ -3308,8 +3308,8 @@ var fieldShape = {
3308
3308
  rules: z.array(
3309
3309
  z.object({
3310
3310
  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")
3311
+ op: z.enum(["eq", "neq", "in", "notIn", "gt", "lt", "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); 'gt'/'lt' take one number as a string ("10"); required for every other op`)
3313
3313
  })
3314
3314
  )
3315
3315
  }).optional().describe(
@@ -3995,14 +3995,18 @@ function buildToolDefs(deps) {
3995
3995
  match: z.enum(["all", "any"]),
3996
3996
  rules: z.array(z.object({
3997
3997
  field: z.string().min(1),
3998
- op: z.enum(["eq", "neq", "in", "isSet", "notEmpty", "empty"]),
3998
+ op: z.enum(["eq", "neq", "in", "notIn", "gt", "lt", "isSet", "notEmpty", "empty"]),
3999
3999
  value: z.union([z.string(), z.array(z.string())]).optional()
4000
4000
  })).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."),
4001
+ }).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
4002
  // V2 editor-only keys, the model field's (playbook §14). Declared: this is a strip site.
4003
4003
  helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
4004
4004
  advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
4005
4005
  recommendedMax: z.number().int().positive().optional().describe(PARAM_TEXT.recommendedMax),
4006
+ // F1.x `unique` (a list's items / a table's rows distinct) and the server's `stale` marker:
4007
+ // undeclared here, an agent's `unique` and an echoed `stale` were dropped with a 200.
4008
+ unique: z.boolean().optional(),
4009
+ stale: z.boolean().optional(),
4006
4010
  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
4011
  // ⚠ A STRIP SITE, exactly like OutField above: this is a `z.object`, so a key the model
4008
4012
  // sends and this shape does not declare is dropped BEFORE the request leaves — a 200 with
@@ -6039,13 +6043,13 @@ ${notes.join("\n")}` : summary, created);
6039
6043
  handler: guard(
6040
6044
  async (args) => withClient(async (client) => {
6041
6045
  if (args.detail) {
6042
- const list2 = await client.listComponents();
6043
- return ok(`${list2.length} component(s) in the bound project (full definitions).`, list2);
6046
+ const list3 = await client.listComponents();
6047
+ return ok(`${list3.length} component(s) in the bound project (full definitions).`, list3);
6044
6048
  }
6045
- const list = await client.listComponents({ select: "summary" });
6049
+ const list2 = await client.listComponents({ select: "summary" });
6046
6050
  return ok(
6047
- `${list.length} component(s) in the bound project.`,
6048
- list.map((cmp) => ({
6051
+ `${list2.length} component(s) in the bound project.`,
6052
+ list2.map((cmp) => ({
6049
6053
  id: cmp.id,
6050
6054
  name: cmp.name,
6051
6055
  slug: cmp.slug,
@@ -6490,6 +6494,11 @@ DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every
6490
6494
 
6491
6495
  ## 2. The decision tree
6492
6496
 
6497
+ Designing ANY section? -> FIRST find its closest kit entry in bettercms://kit
6498
+ (by category, then edge tag); follow that entry's
6499
+ mcp notes and avoid its pitfalls (tag rules below).
6500
+ Its props carry NO target: give each one a target
6501
+ into YOUR blockJson (\xA73) before create_component
6493
6502
  Repeated visual region on a page? -> a COMPONENT with sectionType (a Section)
6494
6503
  ...and it comes in more than one look? -> siblings sharing that sectionType = VARIANTS
6495
6504
  A stack of mixed, reorderable content? -> kind:'block' models + a \`modular\` field
@@ -6510,6 +6519,29 @@ DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every
6510
6519
  Site-wide nav/footer/modal? -> the LAYOUT (get_layout/update_layout), never a
6511
6520
  page section and never one component per page
6512
6521
 
6522
+ Kit edge tags, one rule each (long form with entry ids: the skill's kit-scenarios reference):
6523
+
6524
+ deep-nesting levels = content kinds, not wrappers; past 6, split the deepest subtree out
6525
+ repeater one band holds the list; never store what order says (step no., totals)
6526
+ polymorphic-array each item names its _type; merge consecutive prose into one text item
6527
+ slots children differ in shape; span/surface/phone order go in slotSettings
6528
+ variants column count, side, carousel = a variant, never a free number or boolean
6529
+ conditional a select reveals one branch; fill only it (hidden values still publish)
6530
+ references reference the entry/product/page; store only what differs as overrides
6531
+ dynamic-query store collection + filters + sort + limit; import the items as entries first
6532
+ global-synced same on ~80% of pages = one global; an instance overrides only what differs
6533
+ localized match locales by position, never text; never translate on import; no hreflang
6534
+ rich-text one body; callouts, tables, media, instances are islands; no h1, no chrome
6535
+ media-heavy largest source, real alt, video poster + caption file, positions in %
6536
+ embed store provider + id or canonical URL, never the script; iframes need a title
6537
+ form a Form reference by id; never the inputs copied into the section
6538
+ commerce the store owns price, stock, SKU, options, cart: reference, never copy
6539
+ personalization only with evidence; complete default, audiences hold only the differences
6540
+ validation numbers as numbers, datetimes with offset; check cross-field rules on import
6541
+ a11y every visual-only element gets text: logo name, pin label, iframe title
6542
+ seo-structured JSON-LD is emitted from content; never model meta or JSON-LD fields (\xA75b)
6543
+ responsive-layout per-breakpoint values as settings that inherit desktop; spacing as tokens
6544
+
6513
6545
  ## 3. Anatomy of a real section component
6514
6546
 
6515
6547
  A component that is only headings and text renders as an unstyled stack \u2014 plain text on white
@@ -7263,7 +7295,7 @@ conversion is yours to write.
7263
7295
  markup inside a \`text\` field.
7264
7296
  **Prose MIGRATED from another CMS needs one extra check.** Every block's \`_type\` must be one
7265
7297
  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?}}\`,
7298
+ 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
7299
  groups \`[{_key, label}]\`; Sanity's row of plain strings, \`rows:[{cells:["a","b"]}]\`,
7268
7300
  is admitted too), \`bcmsCode\`, \`bcmsDivider\`, \`bcmsMention\`, \`pullQuote\`
7269
7301
  (\`{quote, name, role?, link?}\`), \`keyTakeaways\` (\`{title?, points:[...strings]}\`,
@@ -7502,7 +7534,7 @@ Importance orders a screen: required fields first, then array order.
7502
7534
  | Calm defaults for rare settings | \`advanced: true\` |
7503
7535
  | A soft length warning (amber, never a refusal) | \`recommendedMax: 60\` |
7504
7536
  | 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\` |
7537
+ | 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
7538
  | A good row title and thumbnail | \`ui.preview: { title, subtitle, media, fallback }\` (\`fallback\` is a literal label) |
7507
7539
  | 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
7540
  | A star rating | \`type: 'rating'\`: a number from 0 to \`config.max\` (5), \`config.step\` 1 or 0.5 (\`rating_value_invalid\`) |
@@ -8307,6 +8339,93 @@ function loadSkillDocs(dir = defaultSkillDir()) {
8307
8339
  return docs;
8308
8340
  }
8309
8341
 
8342
+ // src/kit.ts
8343
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
8344
+ import { fileURLToPath as fileURLToPath2 } from "url";
8345
+ var KIT_URI = "bettercms://kit";
8346
+ var KIT_ENTRY_URI_PREFIX = "bettercms://kit/entry/";
8347
+ var KIT_ENTRY_URI_TEMPLATE = `${KIT_ENTRY_URI_PREFIX}{id}`;
8348
+ function loadKitCatalog(path = fileURLToPath2(new URL("./kit-catalog.json", import.meta.url))) {
8349
+ return existsSync2(path) ? JSON.parse(readFileSync2(path, "utf8")) : null;
8350
+ }
8351
+ var list = (xs) => xs.map((x) => `- ${x}`).join("\n");
8352
+ function entryNotes(e, h = "###") {
8353
+ const out = [`${h} ${e.name} (\`${e.id}\`)`, "", `Tags: ${e.tags.join(", ") || "none"}. Proves: ${e.proves}`];
8354
+ if (!e.why && !e.mcp) out.push("", "No model notes in the kit for this entry: read its props.");
8355
+ if (e.why) out.push("", `**Why this shape.** ${e.why}`);
8356
+ if (e.htmlSignals.length) out.push("", "**HTML signals.**", list(e.htmlSignals));
8357
+ if (e.mcp) out.push("", `**Do.** ${e.mcp}`);
8358
+ if (e.pitfalls.length) out.push("", "**Pitfalls.**", list(e.pitfalls));
8359
+ if (e.notCarried.length) out.push("", `Not carried by a stored component: ${e.notCarried.join(", ")}.`);
8360
+ return out.join("\n");
8361
+ }
8362
+ function kitIndexText(k) {
8363
+ const byId = new Map(k.entries.map((e) => [e.id, e]));
8364
+ return [
8365
+ "# BetterCMS kit catalogue",
8366
+ "",
8367
+ `${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}.`,
8368
+ "",
8369
+ "## Categories",
8370
+ "",
8371
+ ...k.categories.map((c) => `- ${c.name} (${c.entries.length}): ${KIT_URI}/${c.slug}`),
8372
+ "",
8373
+ "## Edge tags",
8374
+ "",
8375
+ ...k.tags.map((t) => `- \`${t.tag}\` (${t.hint}): ${t.entries.join(", ") || "none"}`),
8376
+ "",
8377
+ "## Entries",
8378
+ "",
8379
+ ...k.categories.flatMap((c) => [
8380
+ `### ${c.name}`,
8381
+ "",
8382
+ ...c.entries.map((id) => {
8383
+ const e = byId.get(id);
8384
+ return `- \`${e.id}\` ${e.name} [${e.tags.join(", ")}]: ${e.proves}`;
8385
+ }),
8386
+ ""
8387
+ ])
8388
+ ].join("\n");
8389
+ }
8390
+ function kitCategoryText(k, slug2) {
8391
+ const c = k.categories.find((x) => x.slug === slug2);
8392
+ const byId = new Map(k.entries.map((e) => [e.id, e]));
8393
+ return [
8394
+ `# Kit: ${c.name}`,
8395
+ "",
8396
+ `${c.entries.length} entries. Each one's create_component props: ${KIT_ENTRY_URI_TEMPLATE}. Index: ${KIT_URI}.`,
8397
+ "",
8398
+ ...c.entries.map((id) => entryNotes(byId.get(id), "##") + "\n")
8399
+ ].join("\n");
8400
+ }
8401
+ function kitEntryText(e) {
8402
+ return [
8403
+ entryNotes(e, "#"),
8404
+ "",
8405
+ "## create_component payload",
8406
+ "",
8407
+ "Props in the exact shape create_component accepts. Adapt keys and copy to the site; keep the types, `ui` and config.",
8408
+ "",
8409
+ "```json",
8410
+ JSON.stringify(e.component, null, 1),
8411
+ "```",
8412
+ ""
8413
+ ].join("\n");
8414
+ }
8415
+ function kitDocs(k = loadKitCatalog()) {
8416
+ if (!k) return [];
8417
+ const doc = (uri, name, title, description, text2) => ({ uri, name, title, description, mimeType: "text/markdown", text: text2 });
8418
+ return [
8419
+ 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)),
8420
+ ...k.categories.map(
8421
+ (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))
8422
+ )
8423
+ ];
8424
+ }
8425
+ function kitEntryTexts(k = loadKitCatalog()) {
8426
+ return k ? Object.fromEntries(k.entries.map((e) => [`${KIT_ENTRY_URI_PREFIX}${e.id}`, kitEntryText(e)])) : {};
8427
+ }
8428
+
8310
8429
  // src/server.ts
8311
8430
  var SERVER_NAME = "bettercms";
8312
8431
  var SERVER_VERSION = "1.5.0";
@@ -8368,6 +8487,22 @@ function buildServer(deps) {
8368
8487
  for (const { uri, name, text: text2, ...meta } of loadSkillDocs()) {
8369
8488
  server.registerResource(name, uri, meta, () => ({ contents: [{ uri, mimeType: meta.mimeType, text: text2 }] }));
8370
8489
  }
8490
+ for (const { uri, name, text: text2, ...meta } of kitDocs()) {
8491
+ server.registerResource(name, uri, meta, () => ({ contents: [{ uri, mimeType: meta.mimeType, text: text2 }] }));
8492
+ }
8493
+ const kitEntries = kitEntryTexts();
8494
+ if (Object.keys(kitEntries).length) {
8495
+ server.registerResource(
8496
+ "kit-entry",
8497
+ new ResourceTemplate(KIT_ENTRY_URI_TEMPLATE, { list: void 0 }),
8498
+ { title: "Kit entry", description: "One kit entry: its model notes and the create_component props. Ids are in bettercms://kit.", mimeType: "text/markdown" },
8499
+ (uri) => {
8500
+ const text2 = kitEntries[uri.href];
8501
+ if (text2 === void 0) throw new Error(`Unknown kit entry: ${uri.href}`);
8502
+ return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: text2 }] };
8503
+ }
8504
+ );
8505
+ }
8371
8506
  registerTools(server, {
8372
8507
  auth: deps.auth,
8373
8508
  // `project` is the per-call target of a workspace-wide grant; the SDK sends it as
@@ -8395,7 +8530,7 @@ function isMainModule() {
8395
8530
  const argv1 = process.argv[1];
8396
8531
  if (!argv1) return false;
8397
8532
  try {
8398
- return realpathSync(argv1) === fileURLToPath2(import.meta.url);
8533
+ return realpathSync(argv1) === fileURLToPath3(import.meta.url);
8399
8534
  } catch {
8400
8535
  return false;
8401
8536
  }