@bettercms-ai/mcp 0.79.1 → 0.80.1

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.
@@ -146,6 +146,7 @@ resources: `bettercms://skill/bettercms` for this file, and each reference under
146
146
  | 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 |
147
147
  | 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 |
148
148
  | 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 |
149
+ | 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 |
149
150
  | 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 |
150
151
  | 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 |
151
152
  | 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 |
@@ -0,0 +1,179 @@
1
+ ---
2
+ title: "Custom integrations"
3
+ description: "Connect any vendor with a public API (Greenhouse, JobDiva, Shopify, HubSpot, Zoom, Apollo...) to a BetterCMS site: declare the integration, name its secrets and where they go, have an admin approve it, call the vendor through the server, and recipes per vendor."
4
+ ---
5
+
6
+ # Custom integrations
7
+
8
+ **Outcome.** The site shows the user's own data from another tool (jobs from their ATS, products
9
+ from their store, webinars from their video platform) through an integration you declared, that
10
+ an admin approved, and that never exposes a secret. There is no native per-vendor integration:
11
+ any vendor with a public HTTPS API and a key works the same way.
12
+
13
+ Everything here can also be done by a person in the dashboard (Site settings → Integrations), with the
14
+ same records. What you declare, they see and can edit.
15
+
16
+ ## 1. The order of work
17
+
18
+ 1. **Model where the data lands.** A collection per vendor list (Jobs, Products), with fields for
19
+ what the site shows (content-modeling.md). Nothing vendor-specific in the names.
20
+ 2. **Name the secrets, never handle them.** Pick env var names (`GREENHOUSE_API_KEY`,
21
+ `SHOPIFY_ADMIN_TOKEN`). The user sets the VALUES under Settings → Environment variables
22
+ (production) or with `bettercms env push`. `list_env_vars` shows which names exist. Never ask
23
+ the user to paste a key into the chat, and never put one in a file you write.
24
+ 3. **Declare the integration** with `create_integration` (section 2).
25
+ 4. **Have an admin approve it** (section 3).
26
+ 5. **Discover the shape** with `test_integration_fetch` (section 4).
27
+ 6. **Sync the list into the collection** (section 6) and check the entries.
28
+ 7. **Show it on the site**: register components for the data (components.md); they appear in the
29
+ section library under "Add section".
30
+
31
+ ## 2. Declaring one
32
+
33
+ ```
34
+ create_integration {
35
+ slug: "shopify", name: "Shopify products", vendor: "Shopify",
36
+ allowedHosts: ["acme.myshopify.com"],
37
+ secrets: { TOKEN: "SHOPIFY_ADMIN_TOKEN" }, // template name -> env var KEY
38
+ inject: { headers: { "X-Shopify-Access-Token": "{{secret.TOKEN}}" } }, // where it goes
39
+ settings: { shop: "acme" } // non-secret, the panel reads it
40
+ }
41
+ ```
42
+
43
+ - `allowedHosts` are exact hostnames, called over https on the default port only. A redirect must
44
+ stay on the same origin. List the API host and, for a token exchange, the auth host.
45
+ - `secrets` maps a template name to an env var KEY. `inject` is the ONLY place a secret goes: the
46
+ headers and query parameters added to every request to the integration's hosts. A request you
47
+ send (a sync's, `test_integration_fetch`'s, a panel's) carries no `{{...}}` at all.
48
+ - Vendor calls are GET only. `allowPost: true` lets other requests through: vendor writes, and
49
+ search APIs that take POST (Apollo). Ask for it only when the integration needs it.
50
+ - The answer has `secretsPresent` per name: false means the user has not set that env var yet.
51
+
52
+ ## 3. Approval
53
+
54
+ 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
56
+ update answers carry `approval.changes` (in plain words: which hosts it will call, which env vars
57
+ it will read, where it sends them) and `approval.approveUrl`. Give the user both, and say why the
58
+ integration needs each host and secret. They (or an admin) press Approve on that page.
59
+
60
+ Changing `allowedHosts`, `secrets`, `inject`, `allowPost` or `auth` later withdraws the approval until an admin
61
+ approves again. Name, settings, panel and syncs never need approval.
62
+
63
+ ## 4. Calling the vendor
64
+
65
+ `test_integration_fetch { integration, url, method?, headers?, query?, json? }` runs one request
66
+ through the server and returns `{ status, headers, body }`. The `inject` secrets are added
67
+ server-side and every secret value is replaced by `[secret]` in what comes back. Bodies over 1 MB
68
+ are truncated. Use it to read the response shape before you map anything: list paths and ids are
69
+ JSON Pointers (`/jobs`, `/id`, `/data/0/attributes/name`).
70
+
71
+ ## 5. Token exchanges
72
+
73
+ Some vendors trade credentials for a short-lived token (Zoom Server-to-Server OAuth, JobDiva).
74
+ Declare the exchange once and inject `{{token}}`:
75
+
76
+ ```
77
+ auth: {
78
+ request: { method: "POST", url: "https://zoom.us/oauth/token",
79
+ query: { grant_type: "account_credentials", account_id: "{{secret.ACCOUNT_ID}}" },
80
+ headers: { authorization: "Basic {{secret.BASIC}}" } },
81
+ tokenPath: "/access_token", expiresInPath: "/expires_in"
82
+ },
83
+ inject: { headers: { authorization: "Bearer {{token}}" } }
84
+ ```
85
+
86
+ The token is cached until shortly before it expires, and the token endpoint itself can never be
87
+ called with `test_integration_fetch`. A vendor that wants `base64(id:secret)` in a Basic header
88
+ gets it as ONE env var holding the encoded string (the user can make it with
89
+ `printf 'id:secret' | base64`). `tokenPath: ""` reads a plain-text response as the token.
90
+
91
+ ## 6. Syncing a list into a collection
92
+
93
+ A sync reads every page of a vendor list and writes each item as a DRAFT entry of one collection,
94
+ matched on its `externalKey`: a re-run updates what changed and leaves the rest `unchanged`, so a
95
+ sync is safe to run any number of times. Add it with `update_integration { syncs: [...] }` (the
96
+ list is replaced whole) and run it with `run_integration_sync { integration, sync, waitSeconds: 20 }`.
97
+
98
+ ```
99
+ { key: "jobs", label: "Open roles", contentModelId: "<Jobs collection id>",
100
+ request: { method: "GET", url: "https://boards-api.greenhouse.io/v1/boards/acme/jobs",
101
+ query: { content: "true" } },
102
+ pagination: { style: "none" }, // page: param=1,2,3 | cursor: param=<cursorPath value> | link: rel="next"
103
+ items: "/jobs", externalKey: "/id", slug: "/title",
104
+ map: { title: "/title", location: "/location/name", department: { path: "/departments", transform: "join" },
105
+ applyUrl: "/absolute_url", body: "/content" },
106
+ publish: false, schedule: "hourly" }
107
+ ```
108
+
109
+ - `map` keys are the collection's field keys; values are JSON Pointers into one item, with an
110
+ optional `transform` (`string`, `number`, `boolean`, `join` for a list of names).
111
+ - Run it once with `publish: false`, read a few entries, then turn on `publish` and a `schedule`
112
+ (`hourly` or `daily`) if the user wants the site to follow the vendor on its own.
113
+ A sync that publishes is a publish: adding one, changing one, or running one needs a connection
114
+ allowed to publish (`PUBLISH_NOT_GRANTED` otherwise). Ask the user to turn it on from the dashboard.
115
+ - The run reports `created`, `updated`, `unchanged`, `failed`, `pages` and `items`. An item with no
116
+ external key, or a row a field rejects (a required field the vendor left empty), is `failed`;
117
+ adjust the map or the field and run again. A developer-locked field is never written by a sync.
118
+ - Entries remember where they came from (`provenance.source` = `integration:<slug>`), and
119
+ `list_content_entries` with `externalKey` finds the one for a vendor id.
120
+
121
+ ## 7. A custom panel
122
+
123
+ The stock integration page (Site settings → Integrations → the integration) already shows the
124
+ approval, secrets, syncs with Import now and Auto-sync, a request tester and the runs. Build a
125
+ panel only when the user wants their own screen for it (a store picker, a mapping matrix, an
126
+ "Add component" drawer), the way Storesynk draws one for Shopify in Webflow.
127
+
128
+ 1. Install `@bettercms-ai/dock-input` (0.2.0 or later) and React in the site repo if they are not there.
129
+ 2. Write `bettercms/panels/<name>.tsx` (lowercase, hyphens), default-exporting a React component.
130
+ It is built with the site, served from the project's own isolated origin and drawn in a
131
+ sandboxed frame on the integration's page and in the Visual editor's dock.
132
+ 3. Set `update_integration { integration, panel: "<name>" }`. A panel never needs approval.
133
+
134
+ ```tsx
135
+ import type { PanelProps } from "@bettercms-ai/dock-input";
136
+ import { Button, Switch } from "@bettercms-ai/kit"; // when the site has the kit
137
+
138
+ export default function ShopifyPanel({ integration, call, place }: PanelProps) {
139
+ const sync = integration.syncs[0] as { key: string; schedule?: string | null };
140
+ return (
141
+ <div>
142
+ <Switch checked={!!sync?.schedule} onCheckedChange={(on) =>
143
+ call("syncs.set", { syncs: integration.syncs.map((s) => (s === sync ? { ...s, schedule: on ? "hourly" : null } : s)) })} />
144
+ <Button onClick={() => call("sync.run", { sync: sync.key })}>Import Shopify data</Button>
145
+ {place === "dock" ? <Button onClick={() => call("editor.insertSection", { componentId: "<buy-button component id>" })}>Add buy button</Button> : null}
146
+ </div>
147
+ );
148
+ }
149
+ ```
150
+
151
+ The panel can make no request of its own (its CSP is `connect-src 'none'`), and it never sees a token
152
+ or a secret; navigating its frame away ends it. A `syncs.set` that would make a sync publish, run on a
153
+ schedule, write into another collection, or add or remove a sync asks the person first. Everything goes through `call(method, params)`, run by the dashboard with the signed-in
154
+ person's rights:
155
+
156
+ | Method | Params → result |
157
+ |---|---|
158
+ | `vendor.fetch` | `{ method?, url, query?, headers?, json? }` → `{ status, headers, body }`, as test_integration_fetch |
159
+ | `sync.run` / `sync.status` | `{ sync }` → the run / `{ runId }` → the run |
160
+ | `runs.list` | → the last runs |
161
+ | `settings.set` | `{ settings }` → settings (non-secret, ≤ 16 KB; store ids, toggles) |
162
+ | `syncs.set` | `{ syncs }` → syncs (schedules, mappings) |
163
+ | `models.list` | → the collections a sync can write into |
164
+ | `components.list` / `editor.insertSection` | the project's section components / `{ componentId }` adds one to the open page (dock only) |
165
+
166
+ ## 8. Recipes
167
+
168
+ | Vendor | Hosts | Secrets and inject | List request | Items / key |
169
+ |---|---|---|---|---|
170
+ | Greenhouse job board | `boards-api.greenhouse.io` | none (public board) | `GET /v1/boards/{board}/jobs?content=true` | `/jobs` / `/id` |
171
+ | Shopify Admin | `{shop}.myshopify.com` | `TOKEN` → header `X-Shopify-Access-Token` | `GET /admin/api/2024-10/products.json?limit=250` | `/products` / `/id`, Link-header pages |
172
+ | HubSpot CRM | `api.hubapi.com` | `TOKEN` (private app) → header `authorization: Bearer {{secret.TOKEN}}` | `GET /crm/v3/objects/contacts?limit=100` | `/results` / `/id`, cursor `/paging/next/after` |
173
+ | Zoom webinars | `zoom.us`, `api.zoom.us` | `ACCOUNT_ID`, `BASIC` + token exchange → `authorization: Bearer {{token}}` | `GET https://api.zoom.us/v2/users/me/webinars` | `/webinars` / `/id`, cursor `/next_page_token` |
174
+ | JobDiva | `api.jobdiva.com` | `CLIENT_ID`, `USERNAME`, `PASSWORD` + token exchange → `authorization: Bearer {{token}}` | the job search endpoint of their API | per their docs |
175
+ | Apollo | `api.apollo.io` | `KEY` → header `x-api-key`; `allowPost: true` | `POST /api/v1/mixed_people/search` | `/people` / `/id` |
176
+
177
+ Form submissions to a CRM do not need an integration: attach the form to HubSpot or a webhook
178
+ (forms-and-interactions.md). Client-side widgets (a Shopify cart) use the vendor's PUBLIC
179
+ storefront token in site code; an Admin token never goes into the site.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/mcp",
3
- "version": "0.79.1",
3
+ "version": "0.80.1",
4
4
  "type": "module",
5
5
  "description": "BetterCMS MCP server — schema, content, forms + components authoring tools over the Management API, authed via OAuth device-code.",
6
6
  "bin": {