@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 +154 -19
- package/dist/index.js.map +1 -1
- package/dist/kit-catalog.json +1362 -0
- package/dist/skills/bettercms/SKILL.md +4 -1
- package/dist/skills/bettercms/references/content-modeling.md +5 -0
- package/dist/skills/bettercms/references/detection.md +15 -0
- package/dist/skills/bettercms/references/kit-catalog.md +3259 -0
- package/dist/skills/bettercms/references/kit-scenarios.md +712 -0
- package/dist/skills/bettercms/references/worked-examples.md +146 -1
- package/package.json +1 -1
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
|
|
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
|
|
2762
|
-
const kids = getIn(field,
|
|
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,
|
|
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((
|
|
2781
|
-
const kids = getIn(f,
|
|
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(
|
|
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
|
|
6043
|
-
return ok(`${
|
|
6046
|
+
const list3 = await client.listComponents();
|
|
6047
|
+
return ok(`${list3.length} component(s) in the bound project (full definitions).`, list3);
|
|
6044
6048
|
}
|
|
6045
|
-
const
|
|
6049
|
+
const list2 = await client.listComponents({ select: "summary" });
|
|
6046
6050
|
return ok(
|
|
6047
|
-
`${
|
|
6048
|
-
|
|
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) ===
|
|
8533
|
+
return realpathSync(argv1) === fileURLToPath3(import.meta.url);
|
|
8399
8534
|
} catch {
|
|
8400
8535
|
return false;
|
|
8401
8536
|
}
|