@bettercms-ai/mcp 0.79.0 → 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.
package/dist/index.js CHANGED
@@ -2907,6 +2907,14 @@ var LISTING_TAIL = /* @__PURE__ */ new Set([
2907
2907
  "generate_image",
2908
2908
  "transform_image",
2909
2909
  "write_alt_text",
2910
+ // Custom integrations: reached for once a site exists and its owner names a vendor.
2911
+ "list_integrations",
2912
+ "get_integration",
2913
+ "create_integration",
2914
+ "update_integration",
2915
+ "delete_integration",
2916
+ "test_integration_fetch",
2917
+ "run_integration_sync",
2910
2918
  // The ABM campaign lane — also hidden from the default listing (ABM_TOOLS below).
2911
2919
  "research_account",
2912
2920
  "build_abm_page",
@@ -2990,7 +2998,14 @@ var NEW_TOOL_TEXT = {
2990
2998
  import_redirects: "Bulk-create redirects for a migration, from `rows` or from `text` in `format` (our CSV export, a Netlify `_redirects` file, or a vercel.json). Call it with `dryRun: true` FIRST: nothing is written and each row comes back `would-create`, `existing` or `skipped` with a reason. Rows get create_redirect's rules. Max 5000.",
2991
2999
  editor_setup: "Make the live build editable: derives pages, collections, forms, bindings and components from the live release (skill bindings). Run it after an import or a rebuild, before the conversion brief. Idempotent: `ready` when it already ran for the live sha, else it starts (`running`; poll with `check: true`). `state`: never or stale (run it), running, ready, failed. 409 = nothing is live yet: deploy first. Then get_next_steps.",
2992
3000
  transform_image: "Edit an existing Media Library image with an instruction (e.g. 'remove the background', 'make it dusk'); the result is saved as a NEW asset and the original is kept (5 AI credits). Returns the new asset (id, url, altText).",
2993
- write_alt_text: "Alt text for a Media Library image (mediaId) or an https image URL (url), written from its pixels (1 AI credit). A suggestion: save it with update_media."
3001
+ write_alt_text: "Alt text for a Media Library image (mediaId) or an https image URL (url), written from its pixels (1 AI credit). A suggestion: save it with update_media.",
3002
+ list_integrations: "List the project's custom integrations (any vendor with a public API: jobs boards, stores, CRMs, video). Each has its allowed hosts, secret NAMES with `secretsPresent` (is the env var set; never the value), `inject` (where the secrets go), syncs, panel and `approved`. Read bettercms://skill/integrations before building one.",
3003
+ get_integration: "One integration (`integration`: id or slug) with its last 20 sync runs (status, counts, error).",
3004
+ create_integration: "Declare a custom integration: `allowedHosts` (exact hostnames the server may call), `secrets` (template name -> project env var KEY), `inject` (the headers / query parameters that carry them on every request, e.g. {headers: {authorization: 'Bearer {{secret.TOKEN}}'}}), `allowPost` (GET only unless true), optional `auth` token exchange (inject {{token}}), `syncs` (vendor list -> collection) and `panel`. Secret VALUES never pass through here: the user sets them under Settings \u2192 Environment variables (production); list_env_vars shows the names. Until an admin approves it, vendor calls and syncs are refused: give the user `approval.approveUrl` and the `approval.changes` in plain words. You cannot approve it yourself.",
3005
+ update_integration: "Change an integration (`integration`: id or slug); keys you leave out are kept, and `secrets`, `inject` and `syncs` replace the stored ones whole. Changing `allowedHosts`, `secrets`, `inject`, `allowPost` or `auth` withdraws its approval until an admin approves again (`approval.approveUrl`). Name, syncs, settings and panel never need approval.",
3006
+ delete_integration: "Delete an integration and its sync runs. Entries it synced stay (their provenance still names it); the env vars stay.",
3007
+ run_integration_sync: "Run one of an integration's syncs now (`integration`, `sync`: its key): every page of the vendor list is fetched and each item is created or updated as a DRAFT entry, matched on its externalKey, so a re-run only changes what changed (`unchanged` counts the rest); `publish: true` on the sync publishes what changed. Answers with the run: queued, running, done (counts: created, updated, unchanged, failed, pages, items) or failed (error). `waitSeconds` (\u2264 20) waits for it to finish; else follow it with get_integration. A sync with `schedule` 'hourly' or 'daily' also runs on its own.",
3008
+ test_integration_fetch: "Call the vendor once through the server, as the integration: an approved host only, with the integration's `inject` secrets added server-side and every secret redacted from the answer. The request itself takes no {{...}} templates, and the token endpoint cannot be called. Use it to read the response shape before writing a sync: `items`, `externalKey` and `map` are JSON Pointers into it. Bodies over 1 MB come back truncated. 409 integration_not_approved: an admin must approve it first."
2994
3009
  };
2995
3010
  var DELETE_PAGE_RUNTIME_BUNDLE = " Or, with no pageId, `runtimeBundle`: 'report' lists the duplicate pages derived from a server app's own bundle (`app/` folders); 'apply' retires them and their empty folders.";
2996
3011
  var FIELD_LOCKED_NOTE = "A readOnly 'developer' field keeps its stored value: changing or omitting it without schema access is 403 FIELD_LOCKED.";
@@ -3017,6 +3032,7 @@ var PARAM_TEXT = {
3017
3032
  runtimeBundle: "instead of pageId: the server-app bundle cleanup",
3018
3033
  pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
3019
3034
  editorSetupForce: "re-run a setup already ready for the live build",
3035
+ nextStepsFull: "true = every list uncut",
3020
3036
  editorSetupCheck: "true = only report the state; starts nothing",
3021
3037
  waitSeconds: "wait up to N seconds (max 50) for it to finish; call again while it runs",
3022
3038
  editorSetupExempt: "instead of running: record setup-gate exceptions [{ route, check, reason, note }]; reason null removes one. Reasons: get_next_steps `receipt.gate`",
@@ -3040,6 +3056,23 @@ var PARAM_TEXT = {
3040
3056
  mergeHref: "pageId only: the link field paired with `path` (the label). Both become ONE `button` field keyed `key` (default: their stem, primaryCtaHref \u2192 primaryCta). Each entry's DRAFT gets {label, href} and keeps the old values; the live copy changes on publish. ifMatch is the page's version. 409 CODE_RENDERED_ROUTE: do its `next` recipe in code",
3041
3057
  confirmedInput: "true only after the user explicitly approved adding this custom input (`ui.input`); else setting or changing one is refused input_needs_confirmation. Removing one needs no approval"
3042
3058
  };
3059
+ var INTEGRATION_PARAM_TEXT = {
3060
+ integration: "the integration's id or slug (from list_integrations)",
3061
+ slug: "lowercase letters, digits, hyphens; at most 48",
3062
+ allowedHosts: "exact hostnames the server may call, e.g. ['boards-api.greenhouse.io']; https only",
3063
+ secrets: "template name (UPPER_SNAKE) -> project env var KEY, e.g. {TOKEN: 'SHOPIFY_ADMIN_TOKEN'}; never a value",
3064
+ inject: "where the secrets go on every vendor request: {headers?: {name: template}, query?: {name: template}}, templates {{secret.NAME}} or {{token}}",
3065
+ allowPost: "true lets requests other than GET through (vendor writes, POST search APIs such as Apollo's); GET only when omitted",
3066
+ auth: "optional token exchange: {request:{method,url,headers?,query?,form?}, tokenPath, expiresInPath?, ttlSeconds?}; {{secret.NAME}} in its values; its result is {{token}}",
3067
+ settings: "non-secret JSON the panel reads and writes (store id, toggles); at most 16 KB",
3068
+ syncs: "[{key, contentModelId, request:{method,url,headers?,query?}, pagination?:{style:'none'|'page'|'cursor'|'link', param?, cursorPath?, maxPages?}, items, externalKey, slug?, map:{fieldKey: pointer | {path, transform?:'string'|'number'|'boolean'|'join'}}, publish?, schedule?:'hourly'|'daily'|null}]; pointers are JSON Pointers",
3069
+ panel: "the custom panel's slug: bettercms/panels/<panel>.tsx in the site repo; null for the stock page",
3070
+ url: "the full https URL on an allowed host (no templates in the URL)",
3071
+ sync: "the sync's key (from the integration's syncs)",
3072
+ headers: "header -> value (no templates: the integration's inject adds the secrets)",
3073
+ query: "query parameter -> value (no templates)",
3074
+ json: "a JSON body for POST"
3075
+ };
3043
3076
  var INPUT_CONFIRMED_HEADER = "x-bcms-confirmed-input";
3044
3077
  var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant? (a Button family look), size?, arrow?, icon?, newTab?, rel?, fullWidth?} (arrow:false, icon:null off): never a text field beside a link field.";
3045
3078
  var LINK_VALUE_NOTE = "A link or button href: a URL string or {type: url|page|entry|email|phone|anchor|file, href, title?, newTab?, rel?, section?, ref: page/entry id}.";
@@ -3067,6 +3100,10 @@ var AUTHORING_DESCRIPTION = "Record which authoring architecture this site uses:
3067
3100
  var MEDIA_DEFAULT_NOTE = "Leave the default (the CDN) unless the owner asked otherwise; never ask.";
3068
3101
  var REBUILD_DESCRIPTION = "Rebuild from the connected repository, as the dashboard's Rebuild does. Use it when a get_next_steps fix names it, or after a push that started no build; then get_deploy_status { waitSeconds: 50 } until live, and editor_setup. 409 NOT_QUEUED: no repository (deploy_project the working tree) or publishing held.";
3069
3102
  var withWait = (path, waitSeconds) => typeof waitSeconds === "number" && waitSeconds > 0 ? `${path}?wait=${Math.min(Math.round(waitSeconds), 50)}` : path;
3103
+ var nextStepsPath = (path, a) => {
3104
+ const q = [a.importJunk === true ? "importJunk=true" : "", a.full === true ? "full=true" : ""].filter(Boolean).join("&");
3105
+ return q ? `${path}?${q}` : path;
3106
+ };
3070
3107
 
3071
3108
  // src/entry-query.ts
3072
3109
  var text = (v) => Array.isArray(v) ? v.map(String).join(",") : String(v);
@@ -3506,6 +3543,16 @@ function proseType(f) {
3506
3543
  }
3507
3544
  var confirmedHeaders = (confirmed) => confirmed === true ? { [INPUT_CONFIRMED_HEADER]: "true" } : {};
3508
3545
  var withHeaders = (headers) => Object.keys(headers).length > 0 ? { headers } : {};
3546
+ var integrationShape = {
3547
+ allowedHosts: z.array(z.string()).max(10).optional().describe(INTEGRATION_PARAM_TEXT.allowedHosts),
3548
+ secrets: z.record(z.string(), z.string()).optional().describe(INTEGRATION_PARAM_TEXT.secrets),
3549
+ inject: z.object({ headers: z.record(z.string(), z.string()).optional(), query: z.record(z.string(), z.string()).optional() }).optional().describe(INTEGRATION_PARAM_TEXT.inject),
3550
+ allowPost: z.boolean().optional().describe(INTEGRATION_PARAM_TEXT.allowPost),
3551
+ auth: z.record(z.string(), z.unknown()).nullable().optional().describe(INTEGRATION_PARAM_TEXT.auth),
3552
+ settings: z.record(z.string(), z.unknown()).optional().describe(INTEGRATION_PARAM_TEXT.settings),
3553
+ syncs: z.array(z.record(z.string(), z.unknown())).max(20).optional().describe(INTEGRATION_PARAM_TEXT.syncs),
3554
+ panel: z.string().max(48).nullable().optional().describe(INTEGRATION_PARAM_TEXT.panel)
3555
+ };
3509
3556
  var DRY_RUN_SUMMARY = "Dry run: nothing was written.";
3510
3557
  var dryRunOf = async (client, method, path, body, headers = {}) => ok(DRY_RUN_SUMMARY, await client.fetchJSON(client.url(path), { method, body: JSON.stringify({ ...body, dryRun: true }), ...withHeaders(headers) }));
3511
3558
  function toField(f) {
@@ -5175,6 +5222,83 @@ ${d.outline}` : "Proposed Content structure.", d);
5175
5222
  // The whole body: a `test` delivery's outcome rides beside `data`.
5176
5223
  async (c, a) => ok("Webhook created. Store the `secret` now: it is never shown again.", await c.fetchJSON(c.url(`/management/webhooks`), { method: "POST", body: JSON.stringify({ name: a.name, url: a.url, events: a.events, isActive: a.isActive, test: a.test }) }))
5177
5224
  ),
5225
+ def(
5226
+ "list_integrations",
5227
+ "List custom integrations",
5228
+ NEW_TOOL_TEXT.list_integrations,
5229
+ z.object({}).shape,
5230
+ async (c) => ok("Integrations.", await data(c, "GET", `/management/integrations`))
5231
+ ),
5232
+ def(
5233
+ "get_integration",
5234
+ "Get a custom integration",
5235
+ NEW_TOOL_TEXT.get_integration,
5236
+ z.object({ integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration) }).shape,
5237
+ async (c, a) => ok("Integration.", await data(c, "GET", `/management/integrations/${s(a.integration)}`))
5238
+ ),
5239
+ def(
5240
+ "create_integration",
5241
+ "Create a custom integration",
5242
+ NEW_TOOL_TEXT.create_integration,
5243
+ z.object({
5244
+ slug: z.string().min(1).max(48).describe(INTEGRATION_PARAM_TEXT.slug),
5245
+ name: z.string().min(1).max(120).describe("a label, 1-120 chars"),
5246
+ vendor: z.string().max(60).optional().describe("the vendor's name, e.g. 'Greenhouse'"),
5247
+ ...integrationShape
5248
+ }).shape,
5249
+ // The whole body: `approval` rides beside `data`.
5250
+ async (c, a) => ok("Integration created.", await c.fetchJSON(c.url(`/management/integrations`), { method: "POST", body: JSON.stringify(a) }))
5251
+ ),
5252
+ def(
5253
+ "update_integration",
5254
+ "Update a custom integration",
5255
+ NEW_TOOL_TEXT.update_integration,
5256
+ z.object({
5257
+ integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration),
5258
+ name: z.string().min(1).max(120).optional().describe("a label, 1-120 chars"),
5259
+ vendor: z.string().max(60).nullable().optional().describe("the vendor's name"),
5260
+ ...integrationShape
5261
+ }).shape,
5262
+ async (c, a) => {
5263
+ const { integration, ...body } = a;
5264
+ return ok("Integration updated.", await c.fetchJSON(c.url(`/management/integrations/${s(integration)}`), { method: "PATCH", body: JSON.stringify(body) }));
5265
+ }
5266
+ ),
5267
+ def(
5268
+ "delete_integration",
5269
+ "Delete a custom integration",
5270
+ NEW_TOOL_TEXT.delete_integration,
5271
+ z.object({ integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration) }).shape,
5272
+ async (c, a) => ok("Integration deleted.", await data(c, "DELETE", `/management/integrations/${s(a.integration)}`))
5273
+ ),
5274
+ def(
5275
+ "run_integration_sync",
5276
+ "Run an integration's sync",
5277
+ NEW_TOOL_TEXT.run_integration_sync,
5278
+ z.object({
5279
+ integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration),
5280
+ sync: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.sync),
5281
+ waitSeconds: z.number().min(0).max(50).optional().describe(PARAM_TEXT.waitSeconds)
5282
+ }).shape,
5283
+ async (c, a) => ok("Sync run.", await data(c, "POST", withWait(`/management/integrations/${s(a.integration)}/syncs/${s(a.sync)}/run`, a.waitSeconds)))
5284
+ ),
5285
+ def(
5286
+ "test_integration_fetch",
5287
+ "Call a vendor through an integration",
5288
+ NEW_TOOL_TEXT.test_integration_fetch,
5289
+ z.object({
5290
+ integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration),
5291
+ url: z.string().url().describe(INTEGRATION_PARAM_TEXT.url),
5292
+ method: z.enum(["GET", "POST"]).optional().describe("GET when omitted"),
5293
+ headers: z.record(z.string(), z.string()).optional().describe(INTEGRATION_PARAM_TEXT.headers),
5294
+ query: z.record(z.string(), z.string()).optional().describe(INTEGRATION_PARAM_TEXT.query),
5295
+ json: z.unknown().optional().describe(INTEGRATION_PARAM_TEXT.json)
5296
+ }).shape,
5297
+ async (c, a) => {
5298
+ const { integration, ...req } = a;
5299
+ return ok("Vendor response.", await data(c, "POST", `/management/integrations/${s(integration)}/fetch`, req));
5300
+ }
5301
+ ),
5178
5302
  def(
5179
5303
  "list_branches",
5180
5304
  "List content branches",
@@ -5258,10 +5382,11 @@ ${d.outline}` : "Proposed Content structure.", d);
5258
5382
  "Get what to do next",
5259
5383
  NEXT_STEPS_DESCRIPTION,
5260
5384
  z.object({
5261
- importJunk: z.boolean().optional().describe("true = add rule import-junk: what an old import left, with removal calls")
5385
+ importJunk: z.boolean().optional().describe("true = add rule import-junk: what an old import left, with removal calls"),
5386
+ full: z.boolean().optional().describe(PARAM_TEXT.nextStepsFull)
5262
5387
  }).shape,
5263
5388
  // The whole body: `receipt` rides beside `data`, and `data()` would drop it.
5264
- async (c, a) => ok("Next steps.", await c.fetchJSON(c.url(`/management/insights/next-steps${a.importJunk === true ? "?importJunk=true" : ""}`), { method: "GET" }))
5389
+ async (c, a) => ok("Next steps.", await c.fetchJSON(c.url(nextStepsPath(`/management/insights/next-steps`, a)), { method: "GET" }))
5265
5390
  ),
5266
5391
  def(
5267
5392
  "get_binding_report",