@bettercms-ai/mcp 0.84.1 → 0.84.3
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/README.md +13 -34
- package/dist/index.js +47 -2
- package/dist/index.js.map +1 -1
- package/dist/kit-catalog.json +47 -44
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
# @bettercms-ai/mcp
|
|
2
2
|
|
|
3
|
-
BetterCMS [Model Context Protocol](https://modelcontextprotocol.io) server
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
the OAuth 2.0 Device Authorization Grant (RFC 8628) — no pasted long-lived key.
|
|
3
|
+
BetterCMS [Model Context Protocol](https://modelcontextprotocol.io) server, as a local stdio
|
|
4
|
+
process. It signs in with the OAuth 2.0 Device Authorization Grant (RFC 8628): no pasted
|
|
5
|
+
long-lived key.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
| `create_model` | Create a content model (schema) with optional initial fields. |
|
|
13
|
-
| `add_field` | Append a field to an existing model (read-modify-write; never removes fields). |
|
|
14
|
-
| `create_entry` | Create a content entry; applies `data`/`status` via a follow-up update. |
|
|
15
|
-
| `create_form` / `update_form` | Author a form (fields + settings) to embed with `<BcmsForm>`. |
|
|
16
|
-
| `create_component` / `update_component` | Author a reusable component (blockJson tree + props) to render with `<BcmsBlocks>`. |
|
|
17
|
-
| `list_forms` / `get_form` / `list_components` / `get_component` | Discover forms & components to wire into site code. |
|
|
7
|
+
> Most people should use the **hosted** server instead: add `https://mcp.bettercms.ai/mcp` to
|
|
8
|
+
> your client, or run `npx @bettercms-ai/install --mcp`. This package exposes the same tools
|
|
9
|
+
> for clients that only speak stdio. Setup per client and the full tool reference:
|
|
10
|
+
> https://bettercms.ai/docs/ai/connect-mcp and https://bettercms.ai/docs/ai/mcp-tools
|
|
18
11
|
|
|
19
|
-
|
|
12
|
+
## Tools
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
The same tool set as the hosted server: pages and sections, content models and fields, entries,
|
|
15
|
+
components, media, forms, SEO, publishing and deploys, and more. Writes stay drafts unless you
|
|
16
|
+
allowed publishing when you approved the client. Agents learn the order of work from the
|
|
17
|
+
`bettercms` skill (`npx @bettercms-ai/install`).
|
|
23
18
|
|
|
24
19
|
## Usage
|
|
25
20
|
|
|
@@ -30,28 +25,12 @@ Add to your MCP client config (e.g. Claude Desktop / Claude Code):
|
|
|
30
25
|
"mcpServers": {
|
|
31
26
|
"bettercms": {
|
|
32
27
|
"command": "npx",
|
|
33
|
-
"args": ["-y", "@bettercms-ai/mcp"]
|
|
34
|
-
"env": { "BETTERCMS_API_URL": "https://api.bettercms.ai" }
|
|
28
|
+
"args": ["-y", "@bettercms-ai/mcp"]
|
|
35
29
|
}
|
|
36
30
|
}
|
|
37
31
|
}
|
|
38
32
|
```
|
|
39
33
|
|
|
40
|
-
### Registry auth (current)
|
|
41
|
-
|
|
42
|
-
This package is published to **GitHub Packages**, which requires authentication
|
|
43
|
-
**even for public scoped packages**. For `npx @bettercms-ai/mcp` to resolve, the
|
|
44
|
-
host machine needs an `~/.npmrc` with:
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
@bettercms-ai:registry=https://npm.pkg.github.com/
|
|
48
|
-
//npm.pkg.github.com/:_authToken=<a GitHub PAT with read:packages>
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
> **TODO (future):** publish `@bettercms-ai/mcp` (and `types`/`sdk`) to the public
|
|
52
|
-
> **npmjs.org** registry so `npx @bettercms-ai/mcp` works with zero `.npmrc` setup
|
|
53
|
-
> for any host/user. Until then the GitHub Packages auth above is required.
|
|
54
|
-
|
|
55
34
|
On the first tool call the server prints an authorization prompt to **stderr**
|
|
56
35
|
(visible in your MCP client's logs):
|
|
57
36
|
|
package/dist/index.js
CHANGED
|
@@ -3488,7 +3488,7 @@ var fieldShape = {
|
|
|
3488
3488
|
"choices when type is 'select'. A plain string, or {label, value, icon?, description?} when the stored value differs from what the editor shows (e.g. {label:'Sold out', value:'soldOut'}); icon a dock name, description one line \u2264140."
|
|
3489
3489
|
),
|
|
3490
3490
|
config: z.record(z.string(), z.unknown()).optional().describe(
|
|
3491
|
-
"per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, repeater {minItems, maxItems} (the list's item limits; the editor shows 'max N' and stops Add there), array {itemType: text|number|date|image|link|richtext}, date {includeTime, timezone}, modular|slot {blockSlugs: ['quote','gallery'], minItems?, maxItems?} (slot: playbook \xA75) \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}. Undocumented keys open the classic editor."
|
|
3491
|
+
"per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, repeater {minItems, maxItems} (the list's item limits; the editor shows 'max N' and stops Add there), array {itemType: text|number|date|image|link|richtext}, date {includeTime, timezone}, modular|slot {blockSlugs: ['quote','gallery'], minItems?, maxItems?} (slot: playbook \xA75) \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; richtext article blocks and references {posts?, authors?}: playbook/schema \xA75c; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}. Undocumented keys open the classic editor."
|
|
3492
3492
|
),
|
|
3493
3493
|
fields: z.array(z.lazy(() => fieldObject)).optional().describe(
|
|
3494
3494
|
"NESTED child fields \u2014 REQUIRED for type 'group' (one nested object, a Non-Repeatable Zone like blog_hero \u2192 heading, description, hero_image) and type 'repeater' (a repeatable array of such objects, a Repeatable Zone / section-list like testimonials \u2192 quote, author). A section with repeating items is a 'repeater'; a fixed grouped block is a 'group'. Recurse to any depth \u2014 do NOT flatten zones into separate top-level fields."
|
|
@@ -7093,6 +7093,49 @@ The one legitimate exception is a field that is genuinely CONTENT and happens to
|
|
|
7093
7093
|
too \u2014 a blog post's \`excerpt\`, an article's \`coverImage\`. Model those as content, name them
|
|
7094
7094
|
for what they are, and let \`generate_seo_meta\` read them.
|
|
7095
7095
|
|
|
7096
|
+
## 5c. Rich-text article blocks (V2)
|
|
7097
|
+
|
|
7098
|
+
A \`richtext\` or \`document\` value (Portable Text) holds V2's article blocks as types of its own. A
|
|
7099
|
+
field's \`config.features\` gates each one, and a new field accepts them all; a block outside its
|
|
7100
|
+
field's features is refused \`richtext.block_not_accepted:<type>\`, never stripped. Store ONLY these
|
|
7101
|
+
shapes:
|
|
7102
|
+
|
|
7103
|
+
feature stored node
|
|
7104
|
+
button a \`link\` markDef with \`button: true\`
|
|
7105
|
+
embed {_type:'embed', url, title (\u2264120), aspect: '16:9'|'4:3'|'1:1'|'9:16'}
|
|
7106
|
+
gallery {_type:'gallery', layout: 'grid'|'carousel', images: [{_key, image: {src, alt,
|
|
7107
|
+
mediaId?, crop?, focal?} | null, caption? (\u2264160)}]}, at most 24 images
|
|
7108
|
+
tableOfContents {_type:'tableOfContents', title? (\u226440), depth: '2'|'3'}
|
|
7109
|
+
footnotes INLINE {_type:'footnote', _key, note} in a body text block's children
|
|
7110
|
+
(never in a table cell or a column)
|
|
7111
|
+
columns {_type:'columns', gap: 's'|'m'|'l', columns: [{_key, content: [text blocks]}]},
|
|
7112
|
+
2 to 4, text blocks only (no block, no columns, no footnote inside)
|
|
7113
|
+
authorBio {_type:'authorBio', author?: {_ref}}
|
|
7114
|
+
relatedPosts {_type:'relatedPosts', title? (\u226440), mode: 'topic'|'newest'|'manual',
|
|
7115
|
+
count: 1-6, posts?: [{_key, _ref}] (hand-picked, at most 6, unique)}
|
|
7116
|
+
|
|
7117
|
+
\`crop\` ({top, right, bottom, left}) and \`focal\` ({x, y}) are 0\u20131 fractions of the source, the
|
|
7118
|
+
image-url shape; a \`builtin:image\`'s fields take them too. A top-level h2 or h3 may carry
|
|
7119
|
+
\`anchor\`: the id an imported page gave it.
|
|
7120
|
+
|
|
7121
|
+
**Never write what is derived.** Each is refused \`richtext.derived_field:<type>.<path>\`: embed
|
|
7122
|
+
provider, shape, src; footnote number; columns cols; tableOfContents outline; authorBio name,
|
|
7123
|
+
avatar, bio; relatedPosts posts[].title, slug, url, cover, category; and \`resolved\`, on a block or
|
|
7124
|
+
a value. Who an author is and which posts are related are resolved when the page is produced, from
|
|
7125
|
+
the field's \`config.references: { posts?, authors? }\` (content-model ids; posts absent = the
|
|
7126
|
+
entry's own collection, authors absent = no author bio). A \`topic\` block in a collection with no
|
|
7127
|
+
topic field is refused \`relatedPosts.no_topic_field\`.
|
|
7128
|
+
|
|
7129
|
+
**An X post is an \`embed\` with its url.** The server fetches its card. Never paste X's
|
|
7130
|
+
blockquote or its script: X's widget script is a site setting (\`custom_scripts.embeds.xScript\`),
|
|
7131
|
+
loaded once per page.
|
|
7132
|
+
|
|
7133
|
+
**Refused at the write, by name:** \`gallery_invalid\`, \`gallery_limit_exceeded\`, \`toc_invalid\`,
|
|
7134
|
+
\`toc_limit_exceeded\`, \`footnote_invalid\`, \`columns_invalid\`, \`columns_limit_exceeded\`,
|
|
7135
|
+
\`author_bio_invalid\`, \`related_posts_invalid\`, \`related_posts_limit_exceeded\`,
|
|
7136
|
+
\`relatedPosts.no_topic_field\`. A block still being built stores: one gallery image, no author yet,
|
|
7137
|
+
no picks yet.
|
|
7138
|
+
|
|
7096
7139
|
## 6. Blogs
|
|
7097
7140
|
|
|
7098
7141
|
author collection: name, avatar, bio
|
|
@@ -7726,7 +7769,9 @@ conversion is yours to write.
|
|
|
7726
7769
|
groups \`[{_key, label}]\`; Sanity's row of plain strings, \`rows:[{cells:["a","b"]}]\`,
|
|
7727
7770
|
is admitted too), \`bcmsCode\`, \`bcmsDivider\`, \`bcmsMention\`, \`pullQuote\`
|
|
7728
7771
|
(\`{quote, name, role?, link?}\`), \`keyTakeaways\` (\`{title?, points:[...strings]}\`,
|
|
7729
|
-
title at most 40 characters, at most 8 points),
|
|
7772
|
+
title at most 40 characters, at most 8 points), the article blocks (\xA75c: \`embed\`,
|
|
7773
|
+
\`gallery\`, \`tableOfContents\`, \`columns\`, \`authorBio\`, \`relatedPosts\`, an inline
|
|
7774
|
+
\`footnote\`), or \`bcmsBlock\` carrying a
|
|
7730
7775
|
\`schemaKey\` \u2014 and a foreign node (a Sanity image, \`{_type:"image", asset:{_ref}}\`, for one)
|
|
7731
7776
|
is not merely unrendered: it has no component here, so it
|
|
7732
7777
|
vanishes from the HTML every delivery surface reads while the stored value still looks whole.
|