@bettercms-ai/mcp 0.73.1 → 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
  ]);
@@ -2989,7 +2989,8 @@ var PARAM_TEXT = {
2989
2989
  recommendedMax: "a SOFT limit (characters, items or value): the editor warns past it, never refuses",
2990
2990
  openMode: "object/array/group/repeater: how the value (or each row) opens: 'inline', 'drill', 'popover' or 'dialog'",
2991
2991
  fromBuiltin: "a builtin:* id from list_components: copies it into the project; fields you send win",
2992
- componentPreview: "how ONE instance reads as a ROW in a parent's slot list (a /kit schema's document-level `preview`): {title?, subtitle?, media?} each a TOP-LEVEL prop key (422 preview_key_unknown otherwise), fallback? a literal row label (\u2264120), icon? a dock icon name. null clears it",
2992
+ componentPreview: "a slot row's look: {title?, subtitle?, media?} TOP-LEVEL prop keys, fallback?, icon?; null clears. \xA73 of bettercms://playbook/schema",
2993
+ componentIcon: "the block icon, a dock icon name; null clears. \xA73 of bettercms://playbook/schema",
2993
2994
  describes: "for editors: what it is and holds; omitted: written for you",
2994
2995
  runtimeBundle: "instead of pageId: the server-app bundle cleanup",
2995
2996
  pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
@@ -3307,8 +3308,8 @@ var fieldShape = {
3307
3308
  rules: z.array(
3308
3309
  z.object({
3309
3310
  field: z.string().describe("the BARE KEY of another field on this same model"),
3310
- op: z.enum(["eq", "neq", "in", "isSet", "notEmpty", "empty"]),
3311
- 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`)
3312
3313
  })
3313
3314
  )
3314
3315
  }).optional().describe(
@@ -3723,7 +3724,7 @@ function buildToolDefs(deps) {
3723
3724
  ]).describe("block type; section/slider/tabs/columns nest child blocks"),
3724
3725
  id: z.string().min(1).describe("stable unique block id"),
3725
3726
  props: z.record(z.string(), z.unknown()).describe(
3726
- "per-type props: heading {text, level}; text/richtext {html} (NOT {text}), and both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only) \u2014 use it for a title that carries a styled span; image {src, alt}; button {text, href, variant? (hosted pages; else a Button component), size? sm|md|lg, fullWidth?, arrow? boolean, icon? ui.icon name, newTab? boolean, rel? nofollow|sponsored|ugc}; spacer {height}; video {url}; form {formId}; component {componentId, overrides?}; navbar {links:[{label,href}], logo?, cta?}; footer {columns, copyright?}; section {children: block[]}; columns {columns: block[][], gap} \u2014 a column may hold ANY block; slider {slides:[{id,children}]}; tabs {tabs:[{id,label,children}]}; collection {cardComponentId?, source? {modelId}, limit?, filters?, sort?, pinnedIds?, excludeCurrent?}: a collection's published entries as cards (every prop: skill content-modeling)"
3727
+ "per-type props by block type: \xA75 of bettercms://playbook/schema (text/richtext {html}, NOT {text}; button {text, href, variant?, size? sm|md|lg, fullWidth?}; component {componentId, overrides?}). Containers nest blocks to any depth."
3727
3728
  ),
3728
3729
  style: z.record(z.string(), z.unknown()).optional().describe(
3729
3730
  "design tokens: theme, bg (none|surface|muted|accent|dark|custom), bgCustom hex, paddingTop/paddingBottom/paddingSides px, contentWidth (narrow|default|wide|full), align, corner, shadow, borderTop/borderBottom, border (all four sides, in the text colour), hideOn [mobile|tablet|desktop] (all three hides it on the page: kept in the draft, never published). A real marketing band is a `section` block carrying bg + padding + contentWidth."
@@ -3994,14 +3995,18 @@ function buildToolDefs(deps) {
3994
3995
  match: z.enum(["all", "any"]),
3995
3996
  rules: z.array(z.object({
3996
3997
  field: z.string().min(1),
3997
- op: z.enum(["eq", "neq", "in", "isSet", "notEmpty", "empty"]),
3998
+ op: z.enum(["eq", "neq", "in", "notIn", "gt", "lt", "isSet", "notEmpty", "empty"]),
3998
3999
  value: z.union([z.string(), z.array(z.string())]).optional()
3999
4000
  })).min(1).max(20)
4000
- }).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."),
4001
4002
  // V2 editor-only keys, the model field's (playbook §14). Declared: this is a strip site.
4002
4003
  helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
4003
4004
  advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
4004
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(),
4005
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"),
4006
4011
  // ⚠ A STRIP SITE, exactly like OutField above: this is a `z.object`, so a key the model
4007
4012
  // sends and this shape does not declare is dropped BEFORE the request leaves — a 200 with
@@ -4092,6 +4097,7 @@ function buildToolDefs(deps) {
4092
4097
  allowedOn: allowedOn.optional(),
4093
4098
  description: z.string().optional().describe(PARAM_TEXT.describes),
4094
4099
  preview: componentPreviewArg.optional(),
4100
+ icon: dockIconArg.nullable().optional().describe(PARAM_TEXT.componentIcon),
4095
4101
  blockJson: z.array(blockObject).default([]).describe("the component's block tree"),
4096
4102
  props: z.array(componentPropObject).default([]).describe(
4097
4103
  "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
@@ -4109,6 +4115,7 @@ function buildToolDefs(deps) {
4109
4115
  allowedOn: allowedOn.optional().describe("REPLACES the placement allowlist"),
4110
4116
  description: z.string().optional(),
4111
4117
  preview: componentPreviewArg.optional(),
4118
+ icon: dockIconArg.nullable().optional().describe(PARAM_TEXT.componentIcon),
4112
4119
  blockJson: z.array(blockObject).optional().describe("REPLACES the block tree"),
4113
4120
  props: z.array(componentPropObject).optional().describe(
4114
4121
  "REPLACES the prop array \u2014 include every prop you want to keep, WITH its stored `ui`, or that prop's authoring chrome is dropped along with the prop. Read get_component first."
@@ -6036,13 +6043,13 @@ ${notes.join("\n")}` : summary, created);
6036
6043
  handler: guard(
6037
6044
  async (args) => withClient(async (client) => {
6038
6045
  if (args.detail) {
6039
- const list2 = await client.listComponents();
6040
- 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);
6041
6048
  }
6042
- const list = await client.listComponents({ select: "summary" });
6049
+ const list2 = await client.listComponents({ select: "summary" });
6043
6050
  return ok(
6044
- `${list.length} component(s) in the bound project.`,
6045
- list.map((cmp) => ({
6051
+ `${list2.length} component(s) in the bound project.`,
6052
+ list2.map((cmp) => ({
6046
6053
  id: cmp.id,
6047
6054
  name: cmp.name,
6048
6055
  slug: cmp.slug,
@@ -6487,6 +6494,11 @@ DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every
6487
6494
 
6488
6495
  ## 2. The decision tree
6489
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
6490
6502
  Repeated visual region on a page? -> a COMPONENT with sectionType (a Section)
6491
6503
  ...and it comes in more than one look? -> siblings sharing that sectionType = VARIANTS
6492
6504
  A stack of mixed, reorderable content? -> kind:'block' models + a \`modular\` field
@@ -6507,6 +6519,29 @@ DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every
6507
6519
  Site-wide nav/footer/modal? -> the LAYOUT (get_layout/update_layout), never a
6508
6520
  page section and never one component per page
6509
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
+
6510
6545
  ## 3. Anatomy of a real section component
6511
6546
 
6512
6547
  A component that is only headings and text renders as an unstyled stack \u2014 plain text on white
@@ -6544,7 +6579,10 @@ dashboard's prop editor renders. \`helpText\` is three-state: a string is yours,
6544
6579
  purpose (send it for a prop whose /kit schema has no description; nothing refills it), and omitted
6545
6580
  lets the platform write one. They are HINTS: nothing on the write path refuses a placement
6546
6581
  that leaves a required prop empty, so \`get_site_composition\` is what reports one.
6547
- A /kit schema's document-level \`preview\` \u2192 the component's \`preview\` ({title?, subtitle?, media?} are top-level prop keys, plus \`fallback?\`, \`icon?\`); slot rows read it.
6582
+ 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.
6583
+ 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.
6584
+
6585
+ **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).
6548
6586
 
6549
6587
  \u{1F534} **On a components-first page, \`props\` is the ONLY editing surface.** Click-to-edit binds
6550
6588
  \`heading\`/\`text\`/\`button\`/\`image\` blocks; it does **not** bind a \`component\` block, because
@@ -6602,8 +6640,10 @@ config keys (\`componentIds\`, \`multiple\`, \`minItems\`/\`maxItems\`, \`slotSe
6602
6640
  sits at its key in the object or row, \`{ componentId, overrides? }\` or a list with
6603
6641
  \`config.multiple\`, judged like a slot prop's: the same allowlist, depth cap and cycle refusal.
6604
6642
  A list of several item shapes on a component is a \`table\` prop with
6605
- \`config.mode: 'types'\` and \`config.itemTypes: [{ name, label?, fields }]\`, rows
6606
- \`[{ _key?, _type: '<name>', ...values }]\`.
6643
+ \`config.mode: 'types'\` and \`config.itemTypes: [{ name, label?, fields, preview?, icon? }]\`, rows
6644
+ \`[{ _key?, _type: '<name>', ...values }]\`. An item type's \`preview\` names its OWN fields (dotted ok, \`<table>.caption\`).
6645
+
6646
+ **Block props by type** (a block's \`props\` in any \`blockJson\`): heading {text, level 1-6}; text/richtext {html} (NOT {text}), both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only); image {src (asset url), alt, caption?}; button {text, href, variant (hosted pages; else a Button component), size sm|md|lg, fullWidth, arrow, icon (a ui.icon name), newTab, rel nofollow|sponsored|ugc}; spacer {height px}; video {url, poster?}; form {formId}; component {componentId, overrides?}; navbar {links:[{label,href}], logo?, cta?}; footer {columns:[{heading?,links}], copyright?}; section {children: block[], background?, paddingY?, maxWidth?}; columns {columns: block[][], gap} (a column may hold ANY block); slider {slides:[{id,children:block[]}]}; tabs {tabs:[{id,label,children:block[]}]}; collection {cardComponentId?, source? {modelId}, limit?, filters?, sort?, pinnedIds?, excludeCurrent?}: a collection's published entries as cards. Containers nest blocks to any depth.
6607
6647
 
6608
6648
  A component inside a \`richtext\` value is \`{ _type: 'bcmsBlock', _key, schemaKey: 'component',
6609
6649
  schemaLabel?, fields: { componentId, overrides?, variant? } }\` and FOLLOWS the component: publishing
@@ -7255,7 +7295,7 @@ conversion is yours to write.
7255
7295
  markup inside a \`text\` field.
7256
7296
  **Prose MIGRATED from another CMS needs one extra check.** Every block's \`_type\` must be one
7257
7297
  this platform stores \u2014 \`block\`, \`table\` (\`{_type:"table", rows:[{_key, cells:[{_key,
7258
- content:[...blocks], header?, group?}], group?}], caption?, notes?, rowGroups?, columnGroups?}\`,
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}]}\`,
7259
7299
  groups \`[{_key, label}]\`; Sanity's row of plain strings, \`rows:[{cells:["a","b"]}]\`,
7260
7300
  is admitted too), \`bcmsCode\`, \`bcmsDivider\`, \`bcmsMention\`, \`pullQuote\`
7261
7301
  (\`{quote, name, role?, link?}\`), \`keyTakeaways\` (\`{title?, points:[...strings]}\`,
@@ -7494,7 +7534,7 @@ Importance orders a screen: required fields first, then array order.
7494
7534
  | Calm defaults for rare settings | \`advanced: true\` |
7495
7535
  | A soft length warning (amber, never a refusal) | \`recommendedMax: 60\` |
7496
7536
  | A marketer cannot change it | \`readOnly: 'developer'\` plus \`lockReason\` |
7497
- | 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\` |
7498
7538
  | A good row title and thumbnail | \`ui.preview: { title, subtitle, media, fallback }\` (\`fallback\` is a literal label) |
7499
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\`) |
7500
7540
  | A star rating | \`type: 'rating'\`: a number from 0 to \`config.max\` (5), \`config.step\` 1 or 0.5 (\`rating_value_invalid\`) |
@@ -8299,6 +8339,93 @@ function loadSkillDocs(dir = defaultSkillDir()) {
8299
8339
  return docs;
8300
8340
  }
8301
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
+
8302
8429
  // src/server.ts
8303
8430
  var SERVER_NAME = "bettercms";
8304
8431
  var SERVER_VERSION = "1.5.0";
@@ -8360,6 +8487,22 @@ function buildServer(deps) {
8360
8487
  for (const { uri, name, text: text2, ...meta } of loadSkillDocs()) {
8361
8488
  server.registerResource(name, uri, meta, () => ({ contents: [{ uri, mimeType: meta.mimeType, text: text2 }] }));
8362
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
+ }
8363
8506
  registerTools(server, {
8364
8507
  auth: deps.auth,
8365
8508
  // `project` is the per-call target of a workspace-wide grant; the SDK sends it as
@@ -8387,7 +8530,7 @@ function isMainModule() {
8387
8530
  const argv1 = process.argv[1];
8388
8531
  if (!argv1) return false;
8389
8532
  try {
8390
- return realpathSync(argv1) === fileURLToPath2(import.meta.url);
8533
+ return realpathSync(argv1) === fileURLToPath3(import.meta.url);
8391
8534
  } catch {
8392
8535
  return false;
8393
8536
  }