@bettercms-ai/mcp 0.84.2 → 0.84.4

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 CHANGED
@@ -1,25 +1,20 @@
1
1
  # @bettercms-ai/mcp
2
2
 
3
- BetterCMS [Model Context Protocol](https://modelcontextprotocol.io) server. Lets an
4
- AI assistant author your content schema and entries through the BetterCMS Management
5
- API, authenticated with a short-lived, per-project `content:manage` key minted via
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
- ## Tools
9
-
10
- | Tool | Description |
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
- See [SKILL.md](./SKILL.md) for how an AI agent should author pages, forms, and components.
12
+ ## Tools
20
13
 
21
- All operations are **additive** — there are no destructive tools. Deleting models,
22
- fields, or entries is dashboard-only by design.
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
@@ -2231,6 +2231,7 @@ var DOCK_ICON_NAMES = [
2231
2231
  "file-certificate",
2232
2232
  "file-code",
2233
2233
  "file-download",
2234
+ "file-settings",
2234
2235
  "file-text",
2235
2236
  "filter",
2236
2237
  "flag",
@@ -3488,7 +3489,7 @@ var fieldShape = {
3488
3489
  "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
3490
  ),
3490
3491
  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."
3492
+ "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
3493
  ),
3493
3494
  fields: z.array(z.lazy(() => fieldObject)).optional().describe(
3494
3495
  "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 +7094,49 @@ The one legitimate exception is a field that is genuinely CONTENT and happens to
7093
7094
  too \u2014 a blog post's \`excerpt\`, an article's \`coverImage\`. Model those as content, name them
7094
7095
  for what they are, and let \`generate_seo_meta\` read them.
7095
7096
 
7097
+ ## 5c. Rich-text article blocks (V2)
7098
+
7099
+ A \`richtext\` or \`document\` value (Portable Text) holds V2's article blocks as types of its own. A
7100
+ field's \`config.features\` gates each one, and a new field accepts them all; a block outside its
7101
+ field's features is refused \`richtext.block_not_accepted:<type>\`, never stripped. Store ONLY these
7102
+ shapes:
7103
+
7104
+ feature stored node
7105
+ button a \`link\` markDef with \`button: true\`
7106
+ embed {_type:'embed', url, title (\u2264120), aspect: '16:9'|'4:3'|'1:1'|'9:16'}
7107
+ gallery {_type:'gallery', layout: 'grid'|'carousel', images: [{_key, image: {src, alt,
7108
+ mediaId?, crop?, focal?} | null, caption? (\u2264160)}]}, at most 24 images
7109
+ tableOfContents {_type:'tableOfContents', title? (\u226440), depth: '2'|'3'}
7110
+ footnotes INLINE {_type:'footnote', _key, note} in a body text block's children
7111
+ (never in a table cell or a column)
7112
+ columns {_type:'columns', gap: 's'|'m'|'l', columns: [{_key, content: [text blocks]}]},
7113
+ 2 to 4, text blocks only (no block, no columns, no footnote inside)
7114
+ authorBio {_type:'authorBio', author?: {_ref}}
7115
+ relatedPosts {_type:'relatedPosts', title? (\u226440), mode: 'topic'|'newest'|'manual',
7116
+ count: 1-6, posts?: [{_key, _ref}] (hand-picked, at most 6, unique)}
7117
+
7118
+ \`crop\` ({top, right, bottom, left}) and \`focal\` ({x, y}) are 0\u20131 fractions of the source, the
7119
+ image-url shape; a \`builtin:image\`'s fields take them too. A top-level h2 or h3 may carry
7120
+ \`anchor\`: the id an imported page gave it.
7121
+
7122
+ **Never write what is derived.** Each is refused \`richtext.derived_field:<type>.<path>\`: embed
7123
+ provider, shape, src; footnote number; columns cols; tableOfContents outline; authorBio name,
7124
+ avatar, bio; relatedPosts posts[].title, slug, url, cover, category; and \`resolved\`, on a block or
7125
+ a value. Who an author is and which posts are related are resolved when the page is produced, from
7126
+ the field's \`config.references: { posts?, authors? }\` (content-model ids; posts absent = the
7127
+ entry's own collection, authors absent = no author bio). A \`topic\` block in a collection with no
7128
+ topic field is refused \`relatedPosts.no_topic_field\`.
7129
+
7130
+ **An X post is an \`embed\` with its url.** The server fetches its card. Never paste X's
7131
+ blockquote or its script: X's widget script is a site setting (\`custom_scripts.embeds.xScript\`),
7132
+ loaded once per page.
7133
+
7134
+ **Refused at the write, by name:** \`gallery_invalid\`, \`gallery_limit_exceeded\`, \`toc_invalid\`,
7135
+ \`toc_limit_exceeded\`, \`footnote_invalid\`, \`columns_invalid\`, \`columns_limit_exceeded\`,
7136
+ \`author_bio_invalid\`, \`related_posts_invalid\`, \`related_posts_limit_exceeded\`,
7137
+ \`relatedPosts.no_topic_field\`. A block still being built stores: one gallery image, no author yet,
7138
+ no picks yet.
7139
+
7096
7140
  ## 6. Blogs
7097
7141
 
7098
7142
  author collection: name, avatar, bio
@@ -7726,7 +7770,9 @@ conversion is yours to write.
7726
7770
  groups \`[{_key, label}]\`; Sanity's row of plain strings, \`rows:[{cells:["a","b"]}]\`,
7727
7771
  is admitted too), \`bcmsCode\`, \`bcmsDivider\`, \`bcmsMention\`, \`pullQuote\`
7728
7772
  (\`{quote, name, role?, link?}\`), \`keyTakeaways\` (\`{title?, points:[...strings]}\`,
7729
- title at most 40 characters, at most 8 points), or \`bcmsBlock\` carrying a
7773
+ title at most 40 characters, at most 8 points), the article blocks (\xA75c: \`embed\`,
7774
+ \`gallery\`, \`tableOfContents\`, \`columns\`, \`authorBio\`, \`relatedPosts\`, an inline
7775
+ \`footnote\`), or \`bcmsBlock\` carrying a
7730
7776
  \`schemaKey\` \u2014 and a foreign node (a Sanity image, \`{_type:"image", asset:{_ref}}\`, for one)
7731
7777
  is not merely unrendered: it has no component here, so it
7732
7778
  vanishes from the HTML every delivery surface reads while the stored value still looks whole.