@bettercms-ai/mcp 0.84.4 → 0.85.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.
@@ -147,7 +147,7 @@ resources: `bettercms://skill/bettercms` for this file, and each reference under
147
147
  | Nuxt, SvelteKit, React Router, Vite: no adapter | [other-frameworks.md](references/other-frameworks.md) | neither `astro.config.*` nor `next.config.*`; `nuxt.config.*`, `svelte.config.*`, `react-router.config.*` | Nuxt, SvelteKit, React Router, Vite, createClient |
148
148
  | Rich text and field types | [portable-text.md](references/portable-text.md) | writing a richtext value; converting HTML or Markdown | rich text, Portable Text, richtext, HTML, Markdown |
149
149
  | Brand kit, tokens, fonts, logo, images, icons | [media-and-brand.md](references/media-and-brand.md) | design fidelity, alt text, a logo or favicon change | brand, tokens, fonts, logo, image, alt text, favicon |
150
- | Connect a vendor with a public API (jobs board, store, CRM, video): declare, approve, call, recipes | [integrations.md](references/integrations.md) | the user names Greenhouse, JobDiva, Shopify, HubSpot, Zoom, Apollo or any API; a 409 `integration_not_approved` | integration, API key, Greenhouse, JobDiva, Shopify, HubSpot, Zoom, Apollo, sync, vendor, token |
150
+ | Connect a vendor with a public API (jobs board, store, CRM, video): declare, approve, call, recipes | [integrations.md](references/integrations.md) | the user names Greenhouse, JobDiva, Shopify, HubSpot, Zoom, Apollo or any API; a 409 "integration_not_approved" | integration, API key, Greenhouse, JobDiva, Shopify, HubSpot, Zoom, Apollo, sync, vendor, token |
151
151
  | Forms, menus, accordions, modals, consent | [forms-and-interactions.md](references/forms-and-interactions.md) | attaching or verifying a form; an interactive section | form, submission, Turnstile, modal, accordion, menu |
152
152
  | Languages, locale prefixes, translation, hreflang | [localization.md](references/localization.md) | the build serves `/fr`-style prefixes; `locale-unvarianted` in a binding report | language, locale, /fr, translate, hreflang, i18n |
153
153
  | SEO and AEO | [seo.md](references/seo.md) | meta, JSON-LD, sitemap, robots, redirects, AI crawlers, `list_seo_issues` | SEO, meta, JSON-LD, schema.org, sitemap, robots, OG image, redirect, 410, llms.txt |
@@ -9,6 +9,15 @@ Doctrine (what should be editable, the conversion loop): §11 and §13 of
9
9
  `bettercms://playbook/schema`, and the `convert_site` prompt. This is the template side. Rich-text
10
10
  values: [portable-text.md](portable-text.md). Adapter wiring and env: [frameworks.md](frameworks.md).
11
11
 
12
+ ## Contents
13
+
14
+ - [1. Why the attributes matter](#1-why-the-attributes-matter) · [2. Bind the leaf](#2-bind-the-leaf-and-know-which-vocabulary-you-are-writing-in)
15
+ - [3. Addresses](#3-addresses) · [4. Declared mode, publish, and the report](#4-declared-mode-publish-and-the-report)
16
+ - [5. Preview strings (stega)](#5-preview-strings-stega) · [6. Classify an unfamiliar codebase](#6-classify-an-unfamiliar-codebase-before-binding-it)
17
+ - [7. Edit-to-render test](#7-edit-to-render-test) · [Acceptance](#acceptance)
18
+ - [Reading the binding report and the conversion receipt](#reading-the-binding-report-and-the-conversion-receipt)
19
+ - [Editor setup, proposals and binding decisions](#editor-setup-proposals-and-binding-decisions)
20
+
12
21
  ## 1. Why the attributes matter
13
22
 
14
23
  A `data-bcms-*` attribute is the only thing that locates a stored value on a rendered page. Five
@@ -47,7 +47,7 @@ defaultValue? }`.
47
47
  a prop's help only for a format or unit, a place the canvas cannot show, a real constraint or
48
48
  a fallback (most props get none). Write your own by the rules in
49
49
  [content-modeling.md](content-modeling.md) ("Writing help text"): one short plain sentence,
50
- under 90 characters, with no semicolon, no dash and no figure taken from the page.
50
+ of at most 160 characters, with no semicolon, no dash and no figure taken from the page.
51
51
  - A prop takes `showIf` (`{ match, rules: [{ field, op, value? }] }`): hidden until SIBLING props match. `op` is `eq`, `neq`, `in`, `isSet`, `notEmpty` or `empty`; the last three take no `value`, the first three need one. Props and model fields share all six, and a sub-field's `showIf` names its sibling sub-fields.
52
52
  - `object` holds ONE `{ childKey: value }` built from `config.fields` (required), like the model field `object`. Groups, tables and objects nest to 10 levels counting the prop; an 11th is refused `field_depth_exceeded`. Aim an `object` at `props` or at a key your component code reads, never at a text slot such as a heading's `props.text` (`prop_target_unrenderable`).
53
53
  - `datetime`, `slug`, `email`, `phone`, `json`, `location` (`{ lat, lng }`), `tags` (`string[]`), `rating` (0..`config.max`) and `code` (`{ language?, code }` or a string) hold what the same-named model field holds, and a placement's value is checked by that field's rules (`prop_value_invalid`). Like `object`, a `tags` or `code` prop never targets a text slot.
@@ -204,7 +204,7 @@ stored field. Text you send is stored as yours (`helpSource: "agent"`) and is ne
204
204
 
205
205
  Ask one question per field: does the editor need this line to type the right value? Write your
206
206
  own `helpText` (on a field or a prop) only for one of four reasons, written as one short plain
207
- sentence under 90 characters, with no semicolon, no dash and no figure taken from the page.
207
+ sentence of at most 160 characters, with no semicolon, no dash and no figure taken from the page.
208
208
 
209
209
  | Reason | Good |
210
210
  |---|---|
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Custom dock inputs"
3
- description: "When a field earns its own input in the editor dock, how to ask the user first, the input contract and protocol, the writes allowlist, the file layout, and the fallback guarantee."
3
+ description: "When a field earns its own input in the editor dock, how to ask the user first, the input contract and protocol, the writes allowlist, the file layout, the fallback guarantee, and UI stored in the CMS (write/approve/validate/publish_dock_ui)."
4
4
  ---
5
5
 
6
6
  # Custom dock inputs
@@ -123,3 +123,91 @@ The stock control comes back whenever the input cannot run: an unknown name, inp
123
123
  the project, a build that failed, a frame that throws or misses `ready`. The value is unchanged
124
124
  and stays editable. So a custom input can only add to the dock, never take a field away from an
125
125
  author. Removing `ui.input` returns the field to the stock control at once.
126
+
127
+ ## 9. UI stored in the CMS (no repo push)
128
+
129
+ The same input, or a panel, can live in the CMS instead of `bettercms/inputs/`. Use it when the
130
+ site has no repo you can push to, or for a panel a section, a page or the Layout needs. The
131
+ restraint ladder (section 1) and the user's yes (section 2) still come first.
132
+
133
+ | Step | Tool | What happens |
134
+ |---|---|---|
135
+ | 1 | `write_dock_ui { name, scope, kind, target, source }` | saves a new DRAFT version; the same bytes again write nothing |
136
+ | 2 | show the user the source and what it is for | wait for an explicit yes |
137
+ | 3 | `approve_dock_ui { name, version, userApproved: true }` | approves that ONE version; a later write drops it |
138
+ | 4 | `validate_dock_ui { name }` | the import allow-list, the size caps, a name clash; the build receipt |
139
+ | 5 | `publish_dock_ui { name }` | validates again, then the dock serves that version |
140
+ | — | `list_dock_ui { scope?, status? }` | every source, its status and the project's usage |
141
+
142
+ - `name` is what `cms:<name>` imports resolve: a lowercase letter first, then letters, digits and
143
+ hyphens, at most 48 (`DOCK_UI_NAME_UNSERVABLE` otherwise: such a name could never build).
144
+ A name the repo also ships (`bettercms/inputs/<name>.tsx`) refuses `build.name_conflict`: rename one.
145
+ - `source` is ONE `.tsx` file, at most 256 KiB, whose default export is the component (section 5).
146
+ It imports only `react`, `react-dom`, `@bettercms-ai/kit/*`, `@bettercms-ai/dock-input` and its
147
+ siblings as `cms:<name>`, which resolve to their PUBLISHED version. Anything else, or a computed
148
+ `import()`, refuses `build.import_denied` naming the import.
149
+ - Limits per project: 64 sources and 4 MiB across their newest versions (`build.size_exceeded`).
150
+ - `approve_dock_ui` without `userApproved: true` refuses `policy.confirmation_required`. A version an
151
+ agent wrote is approved by an agent only on a connection allowed to publish; otherwise it refuses
152
+ `policy.not_approved` (detail "self_approval") and a person approves it in the dashboard.
153
+ - `publish_dock_ui` on a version nobody approved refuses `policy.not_approved`. On success it rebuilds
154
+ the project's dock UI release (the repo's inputs plus every published CMS source) and answers its
155
+ `release` key. A project with no site handle yet refuses `build.compile_error` (detail "no_site_handle"):
156
+ publish the site once, then publish again. A failed build leaves the stock control (section 8).
157
+
158
+ One minimal example per scope. The `target` says what the UI is for.
159
+
160
+ ```
161
+ // field: an input for one field (`kind: "input"`). Then set ui.input { name } as in section 2.
162
+ write_dock_ui { name: "brand-swatch", scope: "field", kind: "input",
163
+ target: { modelId: "<model id>", path: "accent" },
164
+ source: "import { Button } from '@bettercms-ai/kit/ui'\nconst SWATCHES = ['#0f172a', '#2563eb', '#16a34a']\nexport default function BrandSwatch({ value, onChange, readOnly }) {\n return <div>{SWATCHES.map((c) => <Button key={c} disabled={readOnly} aria-pressed={value === c} onClick={() => onChange(c)}>{c}</Button>)}</div>\n}" }
165
+
166
+ // section: a panel beside one component's props
167
+ write_dock_ui { name: "hero-checklist", scope: "section", kind: "panel",
168
+ target: { componentId: "<component id>" }, source: "export default function Panel({ document }) { … }" }
169
+
170
+ // page: a panel on one page
171
+ write_dock_ui { name: "launch-notes", scope: "page", kind: "panel", target: { pageId: "<page id>" }, source: "…" }
172
+
173
+ // layout: a panel on the global Layout (header, footer)
174
+ write_dock_ui { name: "nav-audit", scope: "layout", kind: "panel", target: {}, source: "…" }
175
+ ```
176
+
177
+ ### What each tool takes and answers
178
+
179
+ The shapes are `@bettercms-ai/types` `dock-ui.ts`. A `source` row is `DockUiSource`: `{ id, name, scope, kind,
180
+ target, headVersion, publishedVersion | null, status: draft | approved | published, approvedBy, approvedAt,
181
+ buildReceipt | null, createdAt, updatedAt }`. A version is `{ version, sourceSha, actorId, origin, createdAt }`.
182
+
183
+ | Tool | Takes | Answers |
184
+ |---|---|---|
185
+ | `write_dock_ui` | `{ name, scope: field \| section \| page \| layout, kind: input \| panel, target, source }` | `{ source, version, created }` (`created: false` when the same bytes were already the newest) |
186
+ | `approve_dock_ui` | `{ name, version, userApproved? }` | `{ source }`, status `approved` |
187
+ | `validate_dock_ui` | `{ name, version? }` (default: the newest) | `{ name, receipt }`: `{ ok: true, version, bundleKey, bytes, builtAt }` or `{ ok: false, version, refusals, builtAt }` |
188
+ | `publish_dock_ui` | `{ name, version? }` | `{ ok: true, source, receipt, release: { K, built, cached } }` |
189
+ | `list_dock_ui` | `{ scope?, status? }` | `{ sources, usage: { sources, bytes, limits } }` |
190
+
191
+ **Who may approve.** A person in the dashboard approves any version (the route needs `content:publish`).
192
+ An agent approves only with the user's yes, sent as `userApproved: true` after they saw the source.
193
+ For a version an agent wrote, the connection must also hold the publish delegation (`content:publish`,
194
+ granted when the user connected the AI client), so an agent never approves its own write on a
195
+ connection that may not publish. A write after the approval drops it: approve the newest version.
196
+
197
+ ### Every refusal, and its fix
198
+
199
+ | Code | When | Fix |
200
+ |---|---|---|
201
+ | `DOCK_UI_NAME_INVALID` (400) | the name is not lowercase letters, digits and hyphens | rename it |
202
+ | `DOCK_UI_NAME_UNSERVABLE` (400) | it does not start with a letter, or is over 48 characters | rename it |
203
+ | `build.size_exceeded` (413 / 409) | one version over 256 KiB, a 65th source, or the project over 4 MiB (`detail` says which) | split the source, or remove a source you no longer need |
204
+ | `build.import_denied` (422) | an import off the allow-list, a computed `import()`, a `cms:` sibling that is not published, or a source importing itself | import only the allowed packages, as string literals; publish the sibling first |
205
+ | `build.name_conflict` (422) | the site's repo ships a dock UI of the same name | rename one of the two |
206
+ | `build.compile_error` (422 / 409) | the build failed (`detail` names it); "no_site_handle": the project has no site to build for | fix the source; for "no_site_handle", publish the site once, then publish again |
207
+ | `policy.confirmation_required` (403) | `approve_dock_ui` without the user's yes | show the user the source, then approve with `userApproved: true` |
208
+ | `policy.not_approved` (403 / 409) | publishing a version nobody approved; "self_approval": an agent approving its own write without the publish delegation | approve it first; for "self_approval", a person approves it in the dashboard |
209
+ | `DOCK_UI_VERSION_NOT_HEAD` (409) | approving an older version, or a newer one was saved meanwhile | `list_dock_ui`, then approve and publish the newest |
210
+ | `DOCK_UI_NOT_FOUND` / `DOCK_UI_VERSION_NOT_FOUND` (404) | no such name, or no such version | `list_dock_ui` lists them; `write_dock_ui` creates one |
211
+ | `DOCK_UI_REBUILD_FAILED` (502) | the version is published but the UI release failed | nothing to change: the next site release builds it; the stock control shows until then |
212
+ | `DOCK_UI_UNAVAILABLE` (503) | this server has no dock UI storage yet | try again after the server update |
213
+
@@ -30,7 +30,7 @@ Give a fieldset a `kind`:
30
30
  |---|---|---|
31
31
  | `group` | Fields folded under one header (`collapsed: true` starts it shut) | 12 rows inside one group |
32
32
  | `row` | Short fields on one line | at most 3 fields |
33
- | `tab` | A tab strip | at most 3 tabs, names of 40 characters or fewer, top-level fields only, tabs never nest |
33
+ | `tab` | A tab strip | at most 8 tabs (the ones that do not fit fold into "More"), names of 40 characters or fewer, top-level fields only, tabs never nest |
34
34
 
35
35
  A group joins a tab with `tab: "<tab id>"`. A fieldset with no `kind` stays flat. Props have no
36
36
  fieldsets: the first prop of a group sends `ui.group: { id, label, kind }` and each later one
@@ -49,7 +49,7 @@ from the dock icon list the MCP serves as a resource (`resources/list`); any oth
49
49
 
50
50
  ## 4. Segmented, dropdown or slider
51
51
 
52
- - `ui.control: "segmented"` on a single-choice select with 2–4 short options: one click replaces
52
+ - `ui.control: "segmented"` on a single-choice select with 2–8 short options: one click replaces
53
53
  two. A multi-select or 5+ options is refused; keep the default dropdown there.
54
54
  - `"slider"` needs a `number` with numeric `config.min` below `config.max` and at most 1000 steps.
55
55
 
@@ -203,7 +203,7 @@ and use what it calls things (`plan.cents`, `billingFrequency`, `StatsBand.astro
203
203
  | A layout of that kind (a component's `name`) | how THIS layout differs from the kind's others; the kind alone when it has one layout | `Centered`, `Split with media`, `Pricing` | its heading's copy, the page it came from with a number (`Hero 2`) |
204
204
  | One placed section's title | nothing you write. The outline shows the section's own heading, as written and read live, with its kind underneath | `Equip Your Projects with ScrewFast` over `Hero` | a `label` you invent, a shortened or reworded heading, the kind said again |
205
205
  | A field or prop | what it HOLDS there, one to three words | `Dismiss key`, `Starter plan price`, `Dashboard screenshot`, `Billing frequency` | its own copy, a role with a number (`Text 4`), the section's name alone |
206
- | Help text | most fields get none. One plain sentence under 90 characters, only for a format or unit, a place the canvas cannot show, a real constraint or a fallback. No semicolons, no dashes, no values from the page | `Leave empty to use the page title.` | the label or the value said again, writing advice, a figure copied from another page |
206
+ | Help text | most fields get none. One plain sentence of at most 160 characters, only for a format or unit, a place the canvas cannot show, a real constraint or a fallback. No semicolons, no dashes, no values from the page | `Leave empty to use the page title.` | the label or the value said again, writing advice, a figure copied from another page |
207
207
 
208
208
  **Advice is not work left**, except the names. `gate.naming` is part of the job: rename every
209
209
  row before you report, as above. `receipt.gate.advice`, `gate.families` and `gate.kit`
@@ -110,7 +110,7 @@ The pipeline with the code that enforces each hop.
110
110
 
111
111
  | # | Hop | Code |
112
112
  |---|---|---|
113
- | 1 | Define: `create_form` / `update_form`, or the dashboard. Postgres `forms` row, `status` defaults to `draft` | `packages/db/src/schema/forms.ts:35` |
113
+ | 1 | Define: `create_form` / `update_form`, or the dashboard. Postgres `forms` row, `status` defaults to `published` (a `draft` when the creator may not publish) | `packages/db/src/schema/forms.ts:35` |
114
114
  | 2 | Publish: `publish_form`, or the dashboard Forms tab. Both need publish permission | `src/routes/management/forms.ts:276`, `src/routes/admin/forms.ts:529`, `:530` |
115
115
  | 3 | Place: a `form` block `{ formId }` inside a section | `packages/types/src/block.ts:253`, `:257` |
116
116
  | 4 | Render: hosted renderer, Next `BcmsForm`, or Astro `BcmsForm.astro` | `packages/next/src/form.tsx:74` |
@@ -13,6 +13,17 @@ any vendor with a public HTTPS API and a key works the same way.
13
13
  Everything here can also be done by a person in the dashboard (Site settings → Integrations), with the
14
14
  same records. What you declare, they see and can edit.
15
15
 
16
+ ## Contents
17
+
18
+ - [1. The order of work](#1-the-order-of-work) · [2. Declaring one](#2-declaring-one)
19
+ - [3. Approval](#3-approval) · [4. Calling the vendor](#4-calling-the-vendor)
20
+ - [5. Token exchanges](#5-token-exchanges) · [6. Syncing a list into a collection](#6-syncing-a-list-into-a-collection)
21
+ - [7. A custom panel (always build one)](#7-a-custom-panel-always-build-one) · [8. Recipes](#8-recipes)
22
+ - [9. Webhooks: the vendor tells the site something changed](#9-webhooks-the-vendor-tells-the-site-something-changed) · [10. Functions: your own code, never your secrets](#10-functions-your-own-code-never-your-secrets)
23
+ - [11. Site endpoints: live reads for visitors, no key in the page](#11-site-endpoints-live-reads-for-visitors-no-key-in-the-page) · [12. Live references: point at a vendor item without copying it](#12-live-references-point-at-a-vendor-item-without-copying-it)
24
+ - [13. Sign in with OAuth (the customer's own OAuth app)](#13-sign-in-with-oauth-the-customers-own-oauth-app) · [14. Templates, the call log, alerts](#14-templates-the-call-log-alerts)
25
+ - [15. Two-way sync: BetterCMS edits go back to the vendor](#15-two-way-sync-bettercms-edits-go-back-to-the-vendor)
26
+
16
27
  ## 1. The order of work
17
28
 
18
29
  1. **Model where the data lands.** A collection per vendor list (Jobs, Products), with fields for
@@ -52,7 +63,7 @@ create_integration {
52
63
  ## 3. Approval
53
64
 
54
65
  Until an admin approves the integration, every vendor call and sync is refused (409
55
- `integration_not_approved`). You cannot approve it: no tool and no header does. The create and
66
+ "integration_not_approved"). You cannot approve it: no tool and no header does. The create and
56
67
  update answers carry `approval.changes` (in plain words: which hosts it will call, which env vars
57
68
  it will read, where it sends them) and `approval.approveUrl`. Give the user both, and say why the
58
69
  integration needs each host and secret. They (or an admin) press Approve on that page.
@@ -262,7 +273,7 @@ update_integration { functions: [{ name: "to-hubspot", triggers: ["form:<formId>
262
273
  - Return `{ upsert: [{ sync, externalKey, data, slug? }] }` to write entries into a sync's
263
274
  collection (drafts, like a sync). `run_integration_function { waitSeconds: 20 }` answers with the
264
275
  output, the captured `console` lines and any error; runs are in `get_integration.functionRuns`.
265
- - If the server answers `functions_not_enabled`, functions are not available yet on this
276
+ - If the server answers "functions_not_enabled", functions are not available yet on this
266
277
  BetterCMS server: use a sync or an endpoint instead.
267
278
 
268
279
  ## 11. Site endpoints: live reads for visitors, no key in the page