@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 +167 -24
- package/dist/index.js.map +1 -1
- package/dist/kit-catalog.json +1362 -0
- package/dist/skills/bettercms/SKILL.md +5 -2
- package/dist/skills/bettercms/references/animation.md +47 -2
- package/dist/skills/bettercms/references/content-modeling.md +5 -0
- package/dist/skills/bettercms/references/detection.md +15 -0
- package/dist/skills/bettercms/references/finish-site.md +26 -4
- 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 +2 -2
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
|
]);
|
|
@@ -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: "
|
|
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(
|
|
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:
|
|
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
|
|
6040
|
-
return ok(`${
|
|
6046
|
+
const list3 = await client.listComponents();
|
|
6047
|
+
return ok(`${list3.length} component(s) in the bound project (full definitions).`, list3);
|
|
6041
6048
|
}
|
|
6042
|
-
const
|
|
6049
|
+
const list2 = await client.listComponents({ select: "summary" });
|
|
6043
6050
|
return ok(
|
|
6044
|
-
`${
|
|
6045
|
-
|
|
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
|
|
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) ===
|
|
8533
|
+
return realpathSync(argv1) === fileURLToPath3(import.meta.url);
|
|
8391
8534
|
} catch {
|
|
8392
8535
|
return false;
|
|
8393
8536
|
}
|