@bettercms-ai/mcp 0.66.6 → 0.67.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
@@ -2695,6 +2695,7 @@ var NEW_TOOL_TEXT = {
2695
2695
  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.";
2696
2696
  var PARAM_TEXT = {
2697
2697
  runtimeBundle: "instead of pageId: the server-app bundle cleanup",
2698
+ pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
2698
2699
  editorSetupForce: "re-run a setup already ready for the live build",
2699
2700
  editorSetupCheck: "true = only report the state; starts nothing",
2700
2701
  pageDynamic: "a dynamic (collection template) page only",
@@ -2705,7 +2706,7 @@ var PARAM_TEXT = {
2705
2706
  where: "field filters: {field: value} is equality, {field: {op: value}} with op eq|ne|gt|gte|lt|lte|contains|in, and an array means `in`. System fields slug, status, publishedAt, locale work anywhere; a data field needs modelId. Compares the served copy (live for published, working for draft)",
2706
2707
  sort: "comma list, a leading - sorts descending, e.g. '-publishedAt,title'",
2707
2708
  page: "1-based page of `limit` rows; the response's meta says totalPages",
2708
- locale: "BCP-47 locale, one of the project's locales; null = the default language",
2709
+ locale: "language, a BCP-47 tag (zh-CN \u2192 zh-cn), one of the project's locales; null = the default",
2709
2710
  translationOf: "the SOURCE's id this translates (never another translation); null unlinks. One per locale: set locale on the translation and translationOf to the source",
2710
2711
  pageScheduledAt: "ISO datetime to publish this page's draft at (needs publish permission); null cancels. An edit that omits scheduledAt CANCELS a pending schedule",
2711
2712
  pageUnpublishAt: "ISO datetime at which the published page returns to draft; null cancels; omitting it never cancels it",
@@ -2716,7 +2717,7 @@ var PARAM_TEXT = {
2716
2717
  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"
2717
2718
  };
2718
2719
  var INPUT_CONFIRMED_HEADER = "x-bcms-confirmed-input";
2719
- var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant?, size?, arrow?, icon?, newTab?}: never a text field beside a link field.";
2720
+ var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant?, size?, arrow?, icon?, newTab?} (arrow:false, icon:null turn one off): never a text field beside a link field.";
2720
2721
  var UPDATE_FIELD_DESCRIPTION = "Change ONE existing field of a content model (`modelId`) or of a page's schema (`pageId`), addressed by `path`: its key, or dot-separated keys through groups and repeaters ('hero.title'). Set its label, helpText, required, validation `config` (merged by key; null deletes one), showIf, ui or placement (null clears those). The rest of the schema is written back unchanged at the version read; a 409 means it changed since, so read again. `type` changes how every stored value is read and needs allowRetype: true. `key` renames it: stored values stay under the old key, so the route answers 409 with the entry count unless allowFieldRemoval: true discards them \u2014 migrate the values first. Ask the user before a retype or a rename. A page's label + link pair (primaryCtaText + primaryCtaHref) becomes one button with `mergeHref`.";
2721
2722
  var GET_SEO_DESCRIPTION = "Get the connected project's site-wide SEO settings \u2014 every key update_seo writes: seoDefaults (metaTitle, metaDescription, ogImage, twitterHandle, og, twitter, collectionSchemaTypes), siteSchema (the site's JSON-LD), robotsConfig, sitemapConfig, rssConfig, indexnowConfig and searchConfig.";
2722
2723
  var UPDATE_SEO_DESCRIPTION = "Set the connected project's site-wide SEO defaults and config. Each key you send is REPLACED whole (not deep-merged), so read get_seo and send the complete object. `seoDefaults` applies to every page that does not override it, and its `collectionSchemaTypes` maps a collection's model id to the schema.org type of its entries. `siteSchema` is the site's own JSON-LD (Organization/WebSite, emitted on the home page). `robotsConfig`, `sitemapConfig` and `rssConfig` shape robots.txt, sitemap.xml and the feed; `indexnowConfig` pings search engines on publish; `searchConfig` is site search. A key the plan does not include answers 403 FEATURE_LOCKED. Rebuilds the site.";
@@ -3299,6 +3300,7 @@ function buildToolDefs(deps) {
3299
3300
  throw err;
3300
3301
  }
3301
3302
  }
3303
+ const pageIdOf = async (client, ref) => UUID.test(ref) ? ref : (await client.fetchJSON(client.url(`/management/pages/resolve?ref=${encodeURIComponent(ref)}`))).data.id;
3302
3304
  function guard(fn) {
3303
3305
  return async (args) => {
3304
3306
  try {
@@ -3419,10 +3421,10 @@ function buildToolDefs(deps) {
3419
3421
  translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf)
3420
3422
  });
3421
3423
  const getPageInput = z.object({
3422
- pageId: z.string().min(1).describe("page id (from list_pages / create_page)")
3424
+ pageId: z.string().min(1).describe(PARAM_TEXT.pageRef)
3423
3425
  });
3424
3426
  const setPageContentInput = z.object({
3425
- pageId: z.string().min(1).describe("id of the page to write values to"),
3427
+ pageId: z.string().min(1).describe(PARAM_TEXT.pageRef),
3426
3428
  data: z.record(z.string(), z.unknown()).describe(
3427
3429
  "field values keyed by field key. A nested 'array' (zone) value is an OBJECT { nonRepeatable: { childKey: value, \u2026 }, repeatable: [ { childKey: value }, \u2026 ] } \u2014 nonRepeatable holds the fixed-block values, repeatable is the list of item objects (omit a zone you didn't define). A primitive 'array' (itemType) is a plain list. An 'image' value is an asset URL or asset id (from upload_asset) \u2014 the server resolves it to { id, url, name, altText }. Read get_page first to see each field's zones."
3428
3430
  ),
@@ -3722,8 +3724,8 @@ function buildToolDefs(deps) {
3722
3724
  const composePagesInput = z.object({
3723
3725
  pages: z.array(
3724
3726
  z.object({
3725
- pageId: z.string().min(1).optional().describe("page id (from list_pages); or pass `slug`"),
3726
- slug: z.string().min(1).optional().describe("page slug, resolved against list_pages; or pass `pageId`"),
3727
+ pageId: z.string().min(1).optional().describe(PARAM_TEXT.pageRef),
3728
+ slug: z.string().min(1).optional().describe("a page slug, when one page has it; or pass `pageId`"),
3727
3729
  blockJson: z.array(blockObject).describe("REPLACES this page's whole block composition")
3728
3730
  })
3729
3731
  ).min(1).max(50)
@@ -4333,7 +4335,7 @@ ${d.outline}` : "Proposed Content structure.", d);
4333
4335
  "update_page",
4334
4336
  "Edit a page",
4335
4337
  "Edit a page: title, slug, SEO metaTitle/metaDescription, structured data (`schemaType`, `schema`), publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first and send its `optimisticVersion` as `ifMatch`). A `blockJson` that swaps a component block to another component comes back with `warnings` (COMPONENT_SWAP_DROPS_OVERRIDES) naming the overrides the new component does not render; they stay on the block, so swapping back restores them. It does NOT change the field SCHEMA \u2014 use add_field (pageId) / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call \u2014 and on a singleton it publishes the page's content entry too; if the entry's workflow stage refuses, the page still goes live and `warnings` carries ENTRY_STILL_DRAFT with the entry id and the reason. STRUCTURED DATA is native SEO, never a field: leave it out and the page gets Automatic JSON-LD from its content (WebSite on the home page, Blog/CollectionPage on a collection's list page, WebPage otherwise); `schemaType` picks an explicit schema.org type whose properties are filled from the content; `schema` is pasted JSON-LD and wins over both (null clears it). " + PAGE_HEAD_NOTE + " " + SECTION_DOCTRINE,
4336
- z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1), title: z.string().optional(), slug: z.string().optional(), blockJson: z.array(blockObject).optional().describe("REPLACES the page's block composition"), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", "WebPage", "AboutPage", "ContactPage", "CollectionPage", "Blog", "BlogPosting", "Article", "Product", "FAQPage", "Event", "Organization", "LocalBusiness"]).optional().describe("structured data type; 'auto' (the default) derives it from the content"), schema: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).nullable().optional().describe("custom JSON-LD; wins over schemaType; null clears it"), status: z.enum(["draft", "published"]).optional(), scheduledAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageScheduledAt), unpublishAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageUnpublishAt), locale: z.string().nullable().optional().describe(PARAM_TEXT.locale), translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf), ifMatch: z.number().int().min(0).optional().describe(UPDATE_PAGE_IF_MATCH_NOTE), dynamic: z.object({ previewPath: z.string().nullable().optional().describe(PARAM_TEXT.previewPath), modelId: z.string().min(1).optional().describe(PARAM_TEXT.pageModelId) }).optional().describe(PARAM_TEXT.pageDynamic) }).shape,
4338
+ z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1).describe(PARAM_TEXT.pageRef), title: z.string().optional(), slug: z.string().optional(), blockJson: z.array(blockObject).optional().describe("REPLACES the page's block composition"), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", "WebPage", "AboutPage", "ContactPage", "CollectionPage", "Blog", "BlogPosting", "Article", "Product", "FAQPage", "Event", "Organization", "LocalBusiness"]).optional().describe("structured data type; 'auto' (the default) derives it from the content"), schema: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).nullable().optional().describe("custom JSON-LD; wins over schemaType; null clears it"), status: z.enum(["draft", "published"]).optional(), scheduledAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageScheduledAt), unpublishAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageUnpublishAt), locale: z.string().nullable().optional().describe(PARAM_TEXT.locale), translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf), ifMatch: z.number().int().min(0).optional().describe(UPDATE_PAGE_IF_MATCH_NOTE), dynamic: z.object({ previewPath: z.string().nullable().optional().describe(PARAM_TEXT.previewPath), modelId: z.string().min(1).optional().describe(PARAM_TEXT.pageModelId) }).optional().describe(PARAM_TEXT.pageDynamic) }).shape,
4337
4339
  async (c, a) => {
4338
4340
  const res = await c.fetchJSON(
4339
4341
  c.url(`/management/pages/${s(a.pageId)}/meta`),
@@ -4723,7 +4725,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
4723
4725
  def(
4724
4726
  "get_binding_report",
4725
4727
  "Check what on the live site is editable",
4726
- "The receipt for 'is this site actually EDITABLE?'. Each release scans the built HTML for the element rendering each CMS field value; per slot this returns `mode` ('text-match' = guessed from rendered text, 'declared' = template-declared), what was bound, and `unmatched`: each path with its reason \u2014 not-declared, ambiguous-text, no-element, declared-only, needs-wrap, refused, layout-owned, page-owned, layout-locator-stale, locale-unvarianted, svg-unavailable, partial and unstamped (a page list the content apply left as built: `kind` `list <bound>/<total>`), mismatch (a text-match stamp whose element shows MORE than the value, or no longer the text it was matched on (`data-bcms-from`): never written), report-outdated. DEPLOY FIRST: before any release there is no report (pages 0, mode null, refreshRequired true). `refreshReason`: `never-generated` \u2014 only a release BetterCMS builds and serves writes a report, so a HEADLESS site never gets one, redeploy or not; `outdated` \u2014 the next release rewrites it. It certifies only that every non-empty field has SOME element carrying its path. `unaddressable` is the copy it cannot see: visible live text no field owns, by route and bucket, biggest first (null = unmeasured: diff visible text against entry values). Over `count / visible > 0.02`, fix it in the SOURCE (wrap the run in an element the codemod can bind, or declare `<BcmsField path=\u2026>`), deploy, re-read \u2014 playbook \xA711. `canvas.lane`: the editor's live-preview lane (`bridge`, `draft-route`; `none` \u2192 get_next_steps). `coverage.error` `nothing-bound`: no element of this build is bound, so its percentage describes another site.",
4728
+ "The receipt for 'is this site actually EDITABLE?'. Each release scans the built HTML for the element rendering each CMS field value; per slot this returns `mode` ('text-match' = guessed from rendered text, 'declared' = template-declared), what was bound, and `unmatched`: each path with its reason \u2014 not-declared, ambiguous-text, no-element, declared-only, needs-wrap, refused, layout-owned, page-owned, layout-locator-stale, locale-unvarianted, svg-unavailable, partial and unstamped (a page list the content apply left as built: `kind` `list <bound>/<total>`), mismatch (a text-match stamp whose element shows MORE than the value, or no longer the text it was matched on (`data-bcms-from`): never written), report-outdated. DEPLOY FIRST: before any release there is no report (pages 0, mode null, refreshRequired true). `refreshReason`: `never-generated` \u2014 only a release BetterCMS builds and serves writes a report, so a HEADLESS site never gets one, redeploy or not; `outdated` \u2014 the next release rewrites it. It certifies only that every non-empty field has SOME element carrying its path. Global Sections count only on routes that render them (`layoutRoutes`). `unaddressable` is the copy it cannot see: visible live text no field owns, by route and bucket, biggest first (null = unmeasured: diff visible text against entry values). Over `count / visible > 0.02`, fix it in the SOURCE (wrap the run in an element the codemod can bind, or declare `<BcmsField path=\u2026>`), deploy, re-read \u2014 playbook \xA711. `canvas.lane`: the editor's live-preview lane (`bridge`, `draft-route`; `none` \u2192 get_next_steps). `coverage.error` `nothing-bound`: no element of this build is bound, so its percentage describes another site.",
4727
4729
  z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read \u2014 everything here, `unaddressable` included, is measured PER SLOT, so on a promote-gated project pass 'current' to read the tree the editor frames. Defaults to the slot this project's releases land in. The response's `sha` is the build the report describes; `refreshRequired` is true when that is not the build this slot serves. Also per slot: `builtRoutes`: routes the live build has HTML for (null = cannot say). `layoutIncomplete`: Layout paths the last content apply could not write (their publish waits); `layoutUnread`: Layout fields no route reads (`layoutUnreadJudged` false = not judged).") }).shape,
4728
4730
  async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
4729
4731
  ),
@@ -4994,7 +4996,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
4994
4996
  def(
4995
4997
  "get_deploy_status",
4996
4998
  "Get deploy/build status",
4997
- "Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release` is the newest release (id, sha, status, phaseMarks, error, createdAt): while an uploaded build ingests or releases, state is `building`, `release.sha` the new sha and `lastLiveSha` still the old one; a failed deploy shows `release.status` failed with its `error`, and `held` state (`held.by`: staff, archived or no-subdomain) means the release waits until that is cleared. `runtime` ({state: ok|crash-loop, restarts, oomKilled}) appears for a server app only: crash-loop means its container keeps restarting (often out of memory) though the build went live fine. Also returns `canvas.lane` (the editor's live-preview lane for this site: `bridge` / `draft-route` / `none`) and `editorUrl`, the visual editor to hand the user.",
4999
+ "Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release` is the newest release (id, sha, status, phaseMarks, error, createdAt): while an uploaded build ingests or releases, state is `building`, `release.sha` the new sha and `lastLiveSha` still the old one (re-sending the live sha is a no-op); a failed deploy shows `release.status` failed with its `error`, and `held` state (`held.by`: staff, archived or no-subdomain) means the release waits until that is cleared. `runtime` ({state: ok|crash-loop, restarts, oomKilled}) appears for a server app only: crash-loop means its container keeps restarting (often out of memory) though the build went live fine. Also returns `canvas.lane` (the editor's live-preview lane for this site: `bridge` / `draft-route` / `none`) and `editorUrl`, the visual editor to hand the user.",
4998
5000
  z.object({}).shape,
4999
5001
  // Flat body, no `data` envelope: `data()` read it as undefined.
5000
5002
  async (c) => ok("Deploy status.", await c.fetchJSON(c.url(`/management/projects/deploy-status`), { method: "GET" }))
@@ -5052,7 +5054,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
5052
5054
  name: "list_pages",
5053
5055
  config: {
5054
5056
  title: "List pages in the current project",
5055
- description: "List the pages in the project this MCP key is bound to, each with its full field SCHEMA, pageType (singleton|dynamic), and status. Use it to verify WHERE content lands and to read field keys/types before set_page_content / add_field.",
5057
+ description: "List the pages in the project this MCP key is bound to, each with its id, `path`, `language`, full field SCHEMA, pageType (singleton|dynamic), and status. Use it to verify WHERE content lands and to read field keys/types before set_page_content / add_field.",
5056
5058
  inputSchema: {}
5057
5059
  },
5058
5060
  handler: guard(
@@ -5064,6 +5066,9 @@ ${res.warnings.join("\n")}` : summary, res.data);
5064
5066
  id: p.id,
5065
5067
  title: p.title,
5066
5068
  slug: p.slug,
5069
+ // Its URL and language: slugs are unique per folder, so `about` can be seven pages.
5070
+ path: p.path,
5071
+ language: p.language,
5067
5072
  pageType: p.pageType,
5068
5073
  status: p.status,
5069
5074
  fields: p.fields
@@ -5076,7 +5081,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
5076
5081
  name: "get_page",
5077
5082
  config: {
5078
5083
  title: "Get a page (with its field schema)",
5079
- description: "Get one page by id INCLUDING its full field schema (keys, types, nested group/repeater children) and pageType. Read this before set_page_content so you write correctly-keyed values, or before add_field so you know the existing keys.",
5084
+ description: "Get one page INCLUDING its full field schema (keys, types, nested group/repeater children), pageType, `path` and `language`. Read this before set_page_content so you write correctly-keyed values, or before add_field so you know the existing keys.",
5080
5085
  inputSchema: getPageInput.shape
5081
5086
  },
5082
5087
  handler: guard(
@@ -5643,18 +5648,20 @@ ${notes.join("\n")}` : summary, created);
5643
5648
  },
5644
5649
  handler: guard(
5645
5650
  async (args) => withClient(async (client) => {
5646
- const needsSlug = args.pages.some((p) => !p.pageId);
5647
- const [known, pageList] = await Promise.all([
5648
- client.listComponents({ select: "summary" }),
5649
- needsSlug ? client.listPages() : Promise.resolve([])
5650
- ]);
5651
+ const known = await client.listComponents({ select: "summary" });
5651
5652
  const ids = new Set(known.map((c) => c.id));
5652
- const bySlug = new Map(pageList.map((p) => [p.slug, p.id]));
5653
5653
  const results = [];
5654
5654
  for (const page of args.pages) {
5655
- const pageId = page.pageId ?? (page.slug ? bySlug.get(page.slug) : void 0);
5656
- if (!pageId) {
5657
- results.push({ ok: false, slug: page.slug, error: `No page with slug '${page.slug}' in this project \u2014 pass a pageId from list_pages.` });
5655
+ const ref = page.pageId ?? page.slug;
5656
+ if (!ref) {
5657
+ results.push({ ok: false, error: "Pass a pageId or path from list_pages." });
5658
+ continue;
5659
+ }
5660
+ let pageId;
5661
+ try {
5662
+ pageId = await pageIdOf(client, ref);
5663
+ } catch (err) {
5664
+ results.push({ ok: false, slug: page.slug, error: errorText(err) });
5658
5665
  continue;
5659
5666
  }
5660
5667
  const unknown = [...new Set(componentIdsIn(page.blockJson))].filter((id) => !ids.has(id));
@@ -5672,7 +5679,7 @@ ${notes.join("\n")}` : summary, created);
5672
5679
  client.url(`/management/pages/${pageId}/meta`),
5673
5680
  { method: "PATCH", body: JSON.stringify({ blockJson: page.blockJson }) }
5674
5681
  );
5675
- results.push({ ok: true, pageId, slug: written.data?.slug ?? page.slug, blocks: page.blockJson.length });
5682
+ results.push({ ok: true, pageId, slug: written.data?.slug ?? page.slug, path: written.data?.path, blocks: page.blockJson.length });
5676
5683
  } catch (err) {
5677
5684
  results.push({ ok: false, pageId, slug: page.slug, error: errorText(err) });
5678
5685
  }
@@ -5924,11 +5931,20 @@ ${lines.join("\n")}`, found);
5924
5931
  // through the client's request plumbing — no bespoke SDK method per endpoint.
5925
5932
  ...lifecycleTools()
5926
5933
  ];
5934
+ const withPageRef = (def) => ({
5935
+ ...def,
5936
+ handler: (args) => {
5937
+ const ref = args?.pageId;
5938
+ if (typeof ref !== "string" || !ref || UUID.test(ref)) return def.handler(args);
5939
+ return guard(async () => def.handler({ ...args, pageId: await withClient((c) => pageIdOf(c, ref)) }))(void 0);
5940
+ }
5941
+ });
5927
5942
  return [
5928
5943
  ...defs.filter((d) => !LISTING_TAIL.has(d.name)),
5929
5944
  ...defs.filter((d) => LISTING_TAIL.has(d.name))
5930
- ].map(withProjectId);
5945
+ ].map(withPageRef).map(withProjectId);
5931
5946
  }
5947
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5932
5948
  function registerTools(server, deps, tools = process.env.BETTERCMS_MCP_TOOLS) {
5933
5949
  const withElicit = {
5934
5950
  ...deps,
@@ -7335,7 +7351,7 @@ the host has a stale cached MCP \u2014 tell the user to \`rm -rf ~/.npm/_npx\` a
7335
7351
 
7336
7352
  ${SCHEMA_PROPOSAL_FLOW}
7337
7353
 
7338
- After creating, report each page's id, slug, type, and field count, and the project it
7354
+ After creating, report each page's id, path, type, and field count, and the project it
7339
7355
  landed in. On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs
7340
7356
  (re)authorizing.`
7341
7357
  }
@@ -7367,7 +7383,7 @@ ${PAGE_FLOW}
7367
7383
 
7368
7384
  ${STRUCTURE_RULE}
7369
7385
 
7370
- After creating, report the page id, slug, type, field count, and the project it landed in.
7386
+ After creating, report the page id, path, type, field count, and the project it landed in.
7371
7387
  On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs (re)authorizing.`
7372
7388
  }
7373
7389
  }