@bettercms-ai/mcp 0.85.0 → 0.87.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.
package/dist/index.js CHANGED
@@ -2988,6 +2988,10 @@ var LISTING_TAIL = /* @__PURE__ */ new Set([
2988
2988
  // Reporting, history and workflow chrome.
2989
2989
  "get_analytics_overview",
2990
2990
  "get_analytics_top_pages",
2991
+ "get_analytics_top_referrers",
2992
+ "get_analytics_breakdown",
2993
+ "get_conversions",
2994
+ "get_ai_traffic",
2991
2995
  "list_activity",
2992
2996
  "get_workflow_board",
2993
2997
  "move_stage",
@@ -3112,7 +3116,9 @@ var WEBHOOK_EVENTS = [
3112
3116
  "content.created",
3113
3117
  "content.updated",
3114
3118
  "content.deleted",
3115
- "deployment.triggered"
3119
+ "deployment.triggered",
3120
+ "branch.created",
3121
+ "branch.applied"
3116
3122
  ];
3117
3123
  var NEW_TOOL_TEXT = {
3118
3124
  save_reusable_block: "Copy a page section (its layout, values and design) into Saved blocks in the section library, under `name`. The page is not changed; a block placed from Saved blocks is an independent section.",
@@ -3126,12 +3132,12 @@ var NEW_TOOL_TEXT = {
3126
3132
  create_webhook: "Create an outgoing webhook: BetterCMS POSTs each subscribed event to your https `url`, signed with HMAC-SHA256 in X-BetterCMS-Signature. The response carries the signing `secret` ONCE: store it now. `test: true` also sends a sample delivery. Payloads carry ids and metadata, never the content: fetch it from the API. A receiver that writes back on content.updated loops unless it skips its own writes.",
3127
3133
  list_branches: "List the project's content branches (id, name, slug, parent, status, whether main) and the `currentBranchId` this connection is on. To work inside a branch, pass its id as `branchId` on any tool call; apply_branch merges it into its parent.",
3128
3134
  create_branch: "Create a content branch: a copy of the project's content to edit without touching the live site. Returns the branch; pass its id as `branchId` on later calls to work in it, then apply_branch. 409 means the name or slug is taken.",
3129
- apply_branch: "Merge a branch into its parent. dryRun: true returns the diff (changes, summary, conflicts) and writes nothing: ALWAYS read it and show the user first. `include` limits the merge to those change keys; `fields` resolves a conflicting field per change key ('keep_branch' or 'keep_parent'). 409 names unresolved conflicts, missing dependencies, or a merge already running.",
3135
+ apply_branch: "Merge a branch into its parent. dryRun: true returns the diff (changes, summary, conflicts) and writes nothing: ALWAYS read it and show the user first. `include` limits the merge to those change keys; `fields` resolves a conflicting field per change key ('keep_branch' or 'keep_parent'). `publish: true` takes the merged changes live in the same call; `scheduleAt` merges everything and publishes at that time instead (null cancels). Both need a branch made from main and a connection allowed to publish. 409 names unresolved conflicts, missing dependencies, or a merge already running.",
3130
3136
  search_docs: "Search the BetterCMS documentation. Returns the best pages (title, url, breadcrumbs, snippet). Use it before guessing how a feature works; `meta.unavailable` means docs search is down right now.",
3131
3137
  generate_image: "Generate an image with AI and add it to the Media Library (5 AI credits; free with the workspace's own Together key). `width`/`height` are 256-1440 in multiples of 16 (default 1024x768). Returns the new asset (id, url, altText): use the url as an 'image' field value. Always set `altText`.",
3132
3138
  refresh_search_index: "Tell search engines and site search about published changes; costs nothing. action 'indexnow' submits `urls` (this site's own, up to 1000) to IndexNow, or every change since the last submission when omitted. action 'reindex' rebuilds the project's site-search index. 503 means the index is unreachable: try again later.",
3133
3139
  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.",
3134
- 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.",
3140
+ 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` if already run for the live sha, else starts (`running`; poll with `check: true`). `state`: never or stale (run it), running, ready, failed. 409: deploy first. Then get_next_steps. `setup.naming`: skill editor-chrome.",
3135
3141
  get_editable_plan: "Choices the platform can't make alone: `rows`, `options`, `recommended`. Explore the site per row: if confident, answer via confirm_editable_plan and tell the owner; if unsure, ask the owner in chat, recommended first. Never finish with an unsure one unasked.",
3136
3142
  confirm_editable_plan: "Record answers: each `decisions` item is an option's `decision`, unchanged. `check: true` = dry run.",
3137
3143
  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).",
@@ -3149,11 +3155,11 @@ var NEW_TOOL_TEXT = {
3149
3155
  install_integration_template: "Install a workspace template on this project: missing collections are created and the integration waits for approval (`approval.approveUrl`); `secretsNeeded` lists the env vars to set.",
3150
3156
  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.",
3151
3157
  // CMS-stored custom dock UI: write → approve (the user's yes) → validate → publish. Recipe: skill dock-input.
3152
- write_dock_ui: "Save a custom dock input or panel (one .tsx `source`) as a new DRAFT version of `name` (a letter, then [a-z0-9-], \u226448), what `cms:<name>` imports resolve. Imports: react, react-dom, @bettercms-ai/kit/*, @bettercms-ai/dock-input, cms:<name>. Same bytes write nothing. Skill dock-input",
3158
+ write_dock_ui: "Save a custom dock input or panel (one .tsx `source`) as a new DRAFT version of `name` (a letter, then [a-z0-9-], \u226448), what `cms:<name>` imports resolve. Imports: react, react-dom, @bettercms-ai/kit/*, @bettercms-ai/dock-input, cms:<name>. Skill dock-input",
3153
3159
  approve_dock_ui: "Approve the newest `version` for publishing. Show the user the source first; userApproved: true only after their yes. A later write drops it",
3154
- validate_dock_ui: "Check one version (default newest) without publishing: allowed imports, size caps, a name the repo also has. Answers the build receipt",
3155
- publish_dock_ui: "Publish an APPROVED version (default newest): validates first, refusing policy.not_approved, build.name_conflict or the build's refusals, then rebuilds the project's dock UI release",
3156
- list_dock_ui: "The project's dock UI sources, with usage against the limits",
3160
+ validate_dock_ui: "Check one version (default newest) without publishing: imports, size caps, a name the repo also has. Answers the build receipt",
3161
+ publish_dock_ui: "Publish an APPROVED version (default newest); rebuilds the dock UI release. `notShown`: published, not drawn, and the fix to tell the user",
3162
+ list_dock_ui: "The project's dock UI sources (`notShown` when not drawn), usage, limits",
3157
3163
  // A V2 TypeSchema into a model (lane fields, through a change set) or a component's props (lane props).
3158
3164
  write_schema: "Write a kit V2 TypeSchema into a model (lane fields, as a change set; model: null creates) or a component's props (lane props). Run dryRun first: a write whose `loss` is not all in acceptLoss is refused schema.loss_not_accepted"
3159
3165
  };
@@ -3180,6 +3186,7 @@ var PARAM_TEXT = {
3180
3186
  componentIcon: "the block icon, a dock icon name; null clears. \xA73 of bettercms://playbook/schema",
3181
3187
  componentTabs: "/kit tabs no top-level prop sits in: [{id,label,icon?,order?}]. \xA73 of bettercms://playbook/schema",
3182
3188
  componentKitEntry: "the bettercms://kit entry id its V2 dock UI follows, confirmed by you: import only SUGGESTS one, so check its htmlSignals and pitfalls first (kit_entry_unknown otherwise); null clears. The setup gate's dock row needs one",
3189
+ componentDock: "propOrder: each top-level prop key once; tabs 'none': no tab strip; design 'fold': Design under the content, 'tab': own tab; null resets",
3183
3190
  describes: "for editors: what it is and holds; omitted: written for you",
3184
3191
  runtimeBundle: "instead of pageId: the server-app bundle cleanup",
3185
3192
  pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
@@ -3207,9 +3214,10 @@ var PARAM_TEXT = {
3207
3214
  focalX: "focal point, 0-1 across from the left edge; null resets to the centre",
3208
3215
  focalY: "focal point, 0-1 down from the top edge; null resets to the centre",
3209
3216
  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",
3210
- 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",
3217
+ confirmedInput: "true only after the user explicitly approved adding this custom input (`ui.input`), or a hide/fold/required\u2192optional (else a named 409). Removing one needs no approval",
3211
3218
  dockUiUserApproved: "true only after the user said yes to this version",
3212
3219
  dockUiTarget: "per scope: field {modelId|pageId|componentId, path}, section {componentId}, page {pageId}, layout {}",
3220
+ dockUiAction: "unpublish: off the dock; delete: an unpublished source",
3213
3221
  writeSchemaModel: "model or component id/slug; null creates a model",
3214
3222
  writeSchemaAcceptLoss: "loss keys or paths you accept",
3215
3223
  writeSchemaIfMatch: "the dry run's `version`; needed to write over a stored row"
@@ -3848,7 +3856,7 @@ var UPDATE_MODEL_DESCRIPTION = "Edit a content model: rename it, change descript
3848
3856
  var FIELD_REMOVAL_NOTE = "top-level field keys (from get_content_model) to DELETE from this model with every entry's value for them; needs allowFieldRemoval: true. An unknown key refuses the whole call. Never for a rename: rename the label instead.";
3849
3857
  var ALLOW_FIELD_REMOVAL_NOTE = "true = the user accepts that removeFields discards those fields' data in every entry. There is no dry run on this route: confirm first.";
3850
3858
  var MODEL_IF_MATCH_NOTE = "optimisticVersion from get_content_model (with pageId: get_page backingModel). The write applies only at that version; a 409 version-conflict means it changed since: read it again and re-apply, never retry blindly. REQUIRED with field*, override or a fieldset kind/tab/icon/collapsed (428 without); otherwise, omitted, the last write wins.";
3851
- var COMPONENT_IF_MATCH_NOTE = "optimisticVersion from get_component. The write applies only at that version; a 409 version-conflict means it changed since: read it again and re-apply. REQUIRED with prop*, override or a prop ui icon/itemLabel/group/tab (428 without).";
3859
+ var COMPONENT_IF_MATCH_NOTE = "optimisticVersion from get_component. The write applies only at that version; a 409 version-conflict means it changed since: read it again and re-apply. REQUIRED with prop*, dock, override or a prop ui icon/itemLabel/group/tab (428 without).";
3852
3860
  var PROP_UI_ARG_NOTE = "top-level prop key \u2192 ui patch, the same vocabulary as a prop's `ui` (group and tab included): each key merges, a null key clears it, null clears the whole ui. Patches keep every other prop as stored; sending `props` instead REPLACES the whole array, so echo each prop's stored `ui` (its `ui.group` AND `ui.tab`) or the prop leaves its tab";
3853
3861
  var OVERRIDE_NOTE = "replace chrome a person set (otherwise 409 ui_human_owned names those paths). Send it only on the user's word.";
3854
3862
  var FIELD_UI_ARG_NOTE = "path \u2192 ui patch, a field's `ui` vocabulary: each key merges, a null key clears it, null clears the whole ui";
@@ -4476,7 +4484,8 @@ function buildToolDefs(deps) {
4476
4484
  propHelpText: z.record(z.string(), z.string().max(200).nullable()).optional().describe("prop key \u2192 help text; null = no help on purpose (stays empty, never refilled)"),
4477
4485
  override: z.boolean().optional().describe(OVERRIDE_NOTE),
4478
4486
  ifMatch: z.number().int().min(0).optional().describe(COMPONENT_IF_MATCH_NOTE),
4479
- confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput)
4487
+ confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput),
4488
+ dock: z.object({ propOrder: z.array(z.string().min(1)).optional(), tabs: z.enum(["none"]).nullable().optional(), design: z.enum(["tab", "fold"]).nullable().optional() }).strict().optional().describe(PARAM_TEXT.componentDock)
4480
4489
  });
4481
4490
  const setComponentSourceInput = z.object({
4482
4491
  componentId: z.string().min(1).describe("component id (from list_components)"),
@@ -4852,8 +4861,13 @@ ${d.outline}` : "Proposed Content structure.", d);
4852
4861
  "publish_dock_ui",
4853
4862
  "Publish custom dock UI",
4854
4863
  NEW_TOOL_TEXT.publish_dock_ui,
4855
- z.object({ name: dockUiName, version: z.number().int().min(1).optional() }).shape,
4856
- async (c, a) => ok("Published the dock UI version.", await data(c, "POST", `/management/dock-ui/${encodeURIComponent(s(a.name))}/publish`, { version: a.version }))
4864
+ z.object({ name: dockUiName, version: z.number().int().min(1).optional(), action: z.enum(["unpublish", "delete"]).optional().describe(PARAM_TEXT.dockUiAction) }).shape,
4865
+ async (c, a) => {
4866
+ const at = `/management/dock-ui/${encodeURIComponent(s(a.name))}`;
4867
+ if (a.action === "unpublish") return ok("Unpublished the dock UI source.", await data(c, "POST", `${at}/unpublish`, {}));
4868
+ if (a.action === "delete") return ok("Deleted the dock UI source.", await data(c, "DELETE", at));
4869
+ return ok("Published the dock UI version.", await data(c, "POST", `${at}/publish`, { version: a.version }));
4870
+ }
4857
4871
  ),
4858
4872
  def(
4859
4873
  "list_dock_ui",
@@ -5690,9 +5704,11 @@ ${d.outline}` : "Proposed Content structure.", d);
5690
5704
  branchId: z.string().min(1).describe("the branch to merge (from list_branches)"),
5691
5705
  dryRun: z.boolean().optional().describe("true = return the diff and write nothing"),
5692
5706
  include: z.array(z.string().min(1)).max(500).optional().describe("change keys to merge (from the diff); all when omitted"),
5693
- fields: z.record(z.string(), z.record(z.string(), z.enum(["keep_branch", "keep_parent"]))).optional().describe("change key \u2192 field \u2192 'keep_branch' | 'keep_parent', for each conflict")
5707
+ fields: z.record(z.string(), z.record(z.string(), z.enum(["keep_branch", "keep_parent"]))).optional().describe("change key \u2192 field \u2192 'keep_branch' | 'keep_parent', for each conflict"),
5708
+ publish: z.boolean().optional().describe("true = also take the merged changes live, in the same call (a branch made from main)"),
5709
+ scheduleAt: z.string().datetime({ offset: true }).nullable().optional().describe("ISO time to merge everything and publish then, instead of now; null cancels a schedule")
5694
5710
  }).shape,
5695
- async (c, a) => a.dryRun === true ? ok("Branch diff (nothing was written).", await data(c, "GET", `/management/branches/${s(a.branchId)}/diff`)) : ok("Branch applied.", await c.fetchJSON(c.url(`/management/branches/${s(a.branchId)}/apply`), { method: "POST", body: JSON.stringify({ include: a.include, fields: a.fields }) }))
5711
+ async (c, a) => a.dryRun === true ? ok("Branch diff (nothing was written).", await data(c, "GET", `/management/branches/${s(a.branchId)}/diff`)) : a.scheduleAt !== void 0 ? ok(a.scheduleAt ? "Branch scheduled." : "Branch unscheduled.", await data(c, "PUT", `/management/branches/${s(a.branchId)}/schedule`, { at: a.scheduleAt })) : ok(a.publish ? "Branch applied and published." : "Branch applied.", await c.fetchJSON(c.url(`/management/branches/${s(a.branchId)}/apply`), { method: "POST", body: JSON.stringify({ include: a.include, fields: a.fields, publish: a.publish }) }))
5696
5712
  ),
5697
5713
  def(
5698
5714
  "search_docs",
@@ -5817,9 +5833,9 @@ ${d.outline}` : "Proposed Content structure.", d);
5817
5833
  def(
5818
5834
  "get_analytics_overview",
5819
5835
  "Get traffic overview",
5820
- "Traffic totals and the daily series for the connected project \u2014 views, unique visitors, bytes, sessions and bounces. Defaults to the last 30 days; pass `from`/`to` as YYYY-MM-DD.",
5821
- z.object({ from: z.string().optional(), to: z.string().optional() }).shape,
5822
- async (c, a) => ok("Analytics overview.", await data(c, "GET", `/management/insights/analytics/overview${q({ from: a.from, to: a.to })}`))
5836
+ 'Traffic totals and the daily series for the connected project: views, unique visitors, bytes, sessions, bounces and responses by status class. Defaults to the last 30 days; pass `from`/`to` as YYYY-MM-DD, and `compare: "previous"` for the previous period\'s totals beside them.',
5837
+ z.object({ from: z.string().optional(), to: z.string().optional(), compare: z.enum(["previous"]).optional() }).shape,
5838
+ async (c, a) => ok("Analytics overview.", await data(c, "GET", `/management/insights/analytics/overview${q({ from: a.from, to: a.to, compare: a.compare })}`))
5823
5839
  ),
5824
5840
  def(
5825
5841
  "get_analytics_top_pages",
@@ -5828,6 +5844,34 @@ ${d.outline}` : "Proposed Content structure.", d);
5828
5844
  z.object({ from: z.string().optional(), to: z.string().optional(), limit: z.number().optional() }).shape,
5829
5845
  async (c, a) => ok("Top pages.", await data(c, "GET", `/management/insights/analytics/top-pages${q({ from: a.from, to: a.to, limit: a.limit })}`))
5830
5846
  ),
5847
+ def(
5848
+ "get_analytics_top_referrers",
5849
+ "Get top referrers",
5850
+ "Traffic sources: the referrer hosts sending visitors, by views. Defaults to the last 30 days; pass `from`/`to` (YYYY-MM-DD) and `limit`.",
5851
+ z.object({ from: z.string().optional(), to: z.string().optional(), limit: z.number().optional() }).shape,
5852
+ async (c, a) => ok("Top referrers.", await data(c, "GET", `/management/insights/analytics/top-referrers${q({ from: a.from, to: a.to, limit: a.limit })}`))
5853
+ ),
5854
+ def(
5855
+ "get_analytics_breakdown",
5856
+ "Get traffic breakdown",
5857
+ "Views and visitors by one dimension: `country` (ISO code), `device` (desktop, mobile, tablet), `browser` or `crawler` (the bots that fetched the site). Defaults to the last 30 days; pass `from`/`to` (YYYY-MM-DD) and `limit`.",
5858
+ z.object({ dim: z.enum(["country", "device", "browser", "crawler"]), from: z.string().optional(), to: z.string().optional(), limit: z.number().optional() }).shape,
5859
+ async (c, a) => ok("Traffic breakdown.", await data(c, "GET", `/management/insights/analytics/breakdown${q({ dim: a.dim, from: a.from, to: a.to, limit: a.limit })}`))
5860
+ ),
5861
+ def(
5862
+ "get_conversions",
5863
+ "Get conversions",
5864
+ "Conversions: form submissions (the one goal a site measures) per form over the last `days` days (default 30, max 90), with last-7 vs previous-7, all-time spam, the pages each form is on, and the site's conversion rate (submissions per 100 page views). `degraded: true` means the submissions store didn't answer, so the counts are not measurements and the rate is null.",
5865
+ z.object({ days: z.number().int().min(1).max(90).optional() }).shape,
5866
+ async (c, a) => ok("Conversions.", await data(c, "GET", `/management/insights/analytics/conversions${q({ days: a.days })}`))
5867
+ ),
5868
+ def(
5869
+ "get_ai_traffic",
5870
+ "Get AI traffic",
5871
+ "AI traffic: crawler visits for AI products (GPTBot, ClaudeBot\u2026, beside search crawlers) and visitors referred by answer engines (ChatGPT, Perplexity\u2026), with totals. Pro plan and above. Defaults to the last 30 days; pass `from`/`to` (YYYY-MM-DD).",
5872
+ z.object({ from: z.string().optional(), to: z.string().optional() }).shape,
5873
+ async (c, a) => ok("AI traffic.", await data(c, "GET", `/management/insights/analytics/ai-traffic${q({ from: a.from, to: a.to })}`))
5874
+ ),
5831
5875
  def(
5832
5876
  "list_seo_issues",
5833
5877
  "List SEO issues",
@@ -6840,8 +6884,14 @@ ${lines.join("\n")}`, found);
6840
6884
  },
6841
6885
  handler: guard(
6842
6886
  async (args) => withClient(async (client) => {
6843
- const { componentId, ifMatch, confirmed, ...input } = args;
6844
- const res = confirmed === true || typeof input.kitEntry === "string" ? await client.fetchJSON(client.url(`/management/components/${componentId}`), {
6887
+ const { componentId, ifMatch, confirmed, dock, ...rest } = args;
6888
+ const input = {
6889
+ ...rest,
6890
+ ...dock?.propOrder !== void 0 ? { propOrder: dock.propOrder } : {},
6891
+ ...dock?.tabs !== void 0 ? { dockTabs: dock.tabs } : {},
6892
+ ...dock?.design !== void 0 ? { dockDesign: dock.design } : {}
6893
+ };
6894
+ const res = confirmed === true || typeof input.kitEntry === "string" || dock !== void 0 ? await client.fetchJSON(client.url(`/management/components/${componentId}`), {
6845
6895
  method: "PATCH",
6846
6896
  body: JSON.stringify(input),
6847
6897
  headers: { ...ifMatch !== void 0 ? { "If-Match": `W/"${ifMatch}"` } : {}, ...confirmedHeaders(confirmed) }