@bettercms-ai/mcp 0.51.0 → 0.52.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
@@ -16,6 +16,19 @@ import { z } from "zod";
16
16
 
17
17
  // ../types/src/component.ts
18
18
  var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: inside a component, ONLY the leaves a declared prop TARGETS are click-to-edit on the canvas \u2014 each declared prop's `target` is re-keyed to that PLACEMENT's own override address, so editing it changes this page and not the shared definition. A prop with no `target` still SHOWS as a dock control, but its override reaches no rendered slot, so editing it changes nothing on the page. Copy that no prop points at is worse still: no binding and no control, unreachable from click-to-edit AND from the dock, changeable only by editing the component definition, which rewrites every page that places it. A `section` block's children stay click-to-edit unconditionally. So when you choose a component, DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section blueprint (list_components returns locked `builtin:*` blueprints with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and nine more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; for a recurring band, materialize the blueprint with create_component so it becomes project-scoped before implementation validation, Output or publication. Direct `builtin:*` component references exist only for legacy delivery compatibility. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
19
+ var COMPONENT_PROP_TYPES = [
20
+ "text",
21
+ "richtext",
22
+ "image",
23
+ "url",
24
+ "boolean",
25
+ "number",
26
+ "select",
27
+ "group",
28
+ "table",
29
+ "slot",
30
+ "form"
31
+ ];
19
32
 
20
33
  // ../types/src/layout-lucide-icons.ts
21
34
  var LAYOUT_SECTION_ICONS = Object.freeze([
@@ -1986,6 +1999,7 @@ import { DeviceAuthPendingError } from "@bettercms-ai/device-auth";
1986
1999
  // src/structure-playbook.ts
1987
2000
  var STRUCTURE_PLAYBOOK_URI = "bettercms://playbook/structure";
1988
2001
  var STRUCTURE_DEFAULT_INSTRUCTION = `After you create collections or pages, organise them per the structure playbook: read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure (read-only), show the user its outline, then apply it with set_content_structure and the version it returned (the If-Match). A project that already has a structure keeps it: file only what you created, with move_to_folder, unless the user asks for a full re-organisation.`;
2002
+ var SKILLS_ROUTING_INSTRUCTION = "Before writing code in a repo that uses BetterCMS, read the installed `bettercms` skill if there is one: it explores the repo and names the one or two bettercms-* skills the task needs.";
1989
2003
  var STRUCTURE_EXAMPLE_PAYLOAD = {
1990
2004
  version: 0,
1991
2005
  doc: {
@@ -2578,6 +2592,11 @@ function buildToolDefs(deps) {
2578
2592
  "the page's VISUAL composition \u2014 how a components-first page is built. Place one `component` block per section: {type:'component', id:'<stable>', props:{componentId:'<id from create_component>'}}. The component must be PUBLISHED (publish_component) or it renders as nothing on the live site. Independent of `fields`, which is a typed schema for a site's own code to read."
2579
2593
  ),
2580
2594
  fields: z.array(fieldObject).optional().describe("the page's typed schema fields"),
2595
+ // Undeclared, this was stripped by the SDK before the request, so a singleton created with its
2596
+ // values arrived without them. The remote adapter and POST /management/pages always took it.
2597
+ data: z.record(z.string(), z.unknown()).optional().describe(
2598
+ "a SINGLETON's field VALUES keyed by field key \u2014 seeds its entry in the same call. If any value is refused the page is still created, none of `data` is saved, and the result names ENTRY_SEED_FAILED with the per-field reasons: fix them and write with set_page_content."
2599
+ ),
2581
2600
  metaTitle: z.string().optional().describe("SEO meta title"),
2582
2601
  metaDescription: z.string().optional().describe("SEO meta description")
2583
2602
  });
@@ -2766,10 +2785,12 @@ function buildToolDefs(deps) {
2766
2785
  key: z.string().min(1),
2767
2786
  label: z.string().min(1),
2768
2787
  target: z.object({ blockId: z.string().min(1), path: z.string().min(1) }),
2769
- // MUST stay at parity with componentPropDefSchema on the server. `update_component`
2770
- // REPLACES the whole `props` array, so a type this enum omits cannot be echoed back: an
2771
- // agent that reads a component and writes it back DESTROYS every prop of that type.
2772
- type: z.enum(["text", "richtext", "image", "url", "boolean", "number", "select", "group", "table", "slot"]),
2788
+ // Parity with componentPropDefSchema is no longer a promise to keep by hand — both now read
2789
+ // the SAME list from @bettercms-ai/types. It used to be a copy, and the failure that made
2790
+ // was quiet and total: `update_component` REPLACES the whole `props` array, so a type this
2791
+ // enum omitted could not be echoed back, and an agent reading a component and writing it
2792
+ // straight back DESTROYED every prop of that type on a 200.
2793
+ type: z.enum(COMPONENT_PROP_TYPES),
2773
2794
  // 'slot' holds ONE nested component instance; config.componentIds restricts what may
2774
2795
  // fill it. Absent here until now, so a slot allowlist was unreachable from stdio even
2775
2796
  // once the enum allowed the type.
@@ -2901,6 +2922,15 @@ function buildToolDefs(deps) {
2901
2922
  const submitComponentizeReceiptInput = z.object({
2902
2923
  receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --componentize --receipt <file>` wrote, verbatim.")
2903
2924
  });
2925
+ const componentValidationRouteInput = z.object({
2926
+ componentId: z.string().min(1).describe("component id (from list_components)"),
2927
+ path: z.string().min(1).max(500).optional().describe("route in the app that serves the preview; omit for the default"),
2928
+ nativeViewports: z.array(z.object({
2929
+ name: z.string().min(1).max(64),
2930
+ width: z.number().int().positive().max(1e4),
2931
+ height: z.number().int().positive().max(1e4)
2932
+ })).min(1).max(8).optional().describe("viewports to check; omit for the defaults")
2933
+ });
2904
2934
  const clearComponentSourceInput = z.object({
2905
2935
  componentId: z.string().min(1).describe("component id (from list_components)")
2906
2936
  });
@@ -3329,7 +3359,7 @@ ${d.outline}` : "Proposed Content structure.", d);
3329
3359
  def(
3330
3360
  "get_project",
3331
3361
  "Get the connected project",
3332
- "Get the connected project's info \u2014 id, name, slug, subdomain, and its live URL (https://<handle>.bettercms.site). Use it to tell the user where their site is published / link the result. " + BRAND_KIT_NOTE,
3362
+ "Get the connected project's info \u2014 id, name, slug, subdomain, and its live URL (https://<handle>.bettercms.site). Use it to tell the user where their site is published / link the result. It also returns `useCdn` and `mediaBaseUrl` \u2014 the origin this project's media is minted and resized on. A deployed site's `PUBLIC_BCMS_MEDIA_URL` / `mediaUrl` must equal `mediaBaseUrl`, or resized images 404 on any rung but production. " + BRAND_KIT_NOTE,
3333
3363
  z.object({}).shape,
3334
3364
  async (c) => ok("Project.", await data(c, "GET", `/management/projects/current`))
3335
3365
  ),
@@ -3395,7 +3425,7 @@ ${d.outline}` : "Proposed Content structure.", d);
3395
3425
  def(
3396
3426
  "set_media_delivery",
3397
3427
  "Choose where this site's media is served from",
3398
- "Record whether this project's media (images, video, files) is served from the BetterCMS CDN or from the API origin. `useCdn: true` is the DEFAULT and the recommendation: URLs are minted on the CDN host, or on the project's own CDN base when it stores media in its own bucket (BYOK). `useCdn: false` mints them on the API origin instead \u2014 the same service serving the same bytes, only under a different hostname. ASK THE USER; do not pick for them. Called without `useCdn`, this tool asks them directly (or hands you the question to ask). \u{1F534} IT APPLIES TO URLs MINTED FROM NOW ON \u2014 new uploads, entry saves, get_media. It does NOT rewrite URLs already stored in published content, and it does not need to: the old URLs keep resolving. To move an existing image, re-save the field that holds it. \u{1F534} IT IS THE BACKEND HALF ONLY. A deployed frontend builds its own transform URLs with @bettercms-ai/image-url, which has its own media host \u2014 set `PUBLIC_BCMS_MEDIA_URL` (or the integration's `mediaUrl` option) to match, or that site's resized images stay on the CDN while everything else moves. Read the current answer from get_project (`useCdn`). Re-callable; the last answer wins.",
3428
+ "Record whether this project's media (images, video, files) is served from the BetterCMS CDN or from the API origin. `useCdn: true` is the DEFAULT and the recommendation: URLs are minted on the CDN host, or on the project's own CDN base when it stores media in its own bucket (BYOK). `useCdn: false` mints them on the API origin instead \u2014 the same service serving the same bytes, only under a different hostname. ASK THE USER; do not pick for them. Called without `useCdn`, this tool asks them directly (or hands you the question to ask). \u{1F534} IT APPLIES TO URLs MINTED FROM NOW ON \u2014 new uploads, entry saves, get_media. It does NOT rewrite URLs already stored in published content, and it does not need to: the old URLs keep resolving. To move an existing image, re-save the field that holds it. \u{1F534} IT IS THE BACKEND HALF ONLY. A deployed frontend builds its own transform URLs with @bettercms-ai/image-url, which has its own media host \u2014 set `PUBLIC_BCMS_MEDIA_URL` (or the integration's `mediaUrl` option) to match, or that site's resized images stay on the CDN while everything else moves. Read the current answer from get_project: `useCdn` is the choice, `mediaBaseUrl` is the origin it produces \u2014 the value the frontend's `PUBLIC_BCMS_MEDIA_URL` must match. Re-callable; the last answer wins.",
3399
3429
  // Optional in the schema for the same reason `framework` and `preference` are: a
3400
3430
  // required arg is rejected by the SDK before the handler runs, which would kill the
3401
3431
  // elicitation below and leave the model guessing.
@@ -3413,7 +3443,7 @@ ${d.outline}` : "Proposed Content structure.", d);
3413
3443
  def(
3414
3444
  "submit_conversion_receipt",
3415
3445
  "Record what the conversion codemod could and could not do",
3416
- "Hand BetterCMS the codemod's own account of a conversion run, so the coverage meter can say WHY a path is not declared instead of only that it is not. Submit the receipt `npx @bettercms-ai/convert` wrote (`--receipt out.json`) for the SAME `briefDigest` get_conversion_brief { complete: true } returned: `{ briefDigest, receipt }`, where the receipt carries `paths: { declared, rewritten, alreadyDeclared, pending[{ route, scope, path, kind, file, reason, message, fix }] }`. `paths.declared` must equal rewritten + alreadyDeclared + pending.length, and each pending `reason` is one of the converter's own (IN_EXPRESSION, AMBIGUOUS_LITERAL, REPEATER_FIXED_LENGTH, PARSE_ERROR, \u2026) \u2014 a path with no receipt row simply reads `not-declared`. \u{1F534} A RECEIPT WITH PENDING PATHS IS A PROGRESS REPORT, NOT A FINISH LINE. The response answers `complete` and `pendingTotal`, and echoes the first 40 pending rows WITH their `fix` \u2014 `{ action, file, line, col?, snippet, why? }`, where `action` is one of `wrap-span` (wrap the literal in a `<span data-bcms-field=\u2026>`), `declare-attr` (add `data-bcms-field=\u2026` to the element at file:line), `bind-expression` (replace the expression with the framework's bcmsField helper), `declare-richtext` (bind the container with the richtext helper), `bind-data` (the literal comes from the data file at file:line \u2014 bind that field) or `manual` (with a one-sentence `why`). Apply every fix in the source, rerun the codemod, resubmit. `complete: true` is the only receipt that ends a conversion \u2014 do not report a site converted on anything less. It is a RECORD, not a release: it changes nothing about the site, and the meter picks it up on the next get_binding_report after the next deploy. A 404 `unknown-brief` means that digest was never issued here, so convert against a brief this project actually returned. \u{1F534} SUBMIT THE BINDING RECEIPT, NOT THE `--forms` ONE. A run of `npx @bettercms-ai/convert --forms` writes a receipt whose `paths` are all zero and whose account is in a `forms` block (`{ wired, alreadyWired, pending[{ id, name, reason }], notes }`); it passes this endpoint's arithmetic and would overwrite the real coverage with zeros. Write it to its own file (`--receipt forms-receipt.json`), read `forms.pending` and publish every form `forms.notes` names in the Forms tab, and submit the binding run's receipt here. Requires artifact:write, the same authority as set_binding_mode; on a workspace-wide connection pass `projectId`.",
3446
+ "Hand BetterCMS the codemod's own account of a conversion run, so the coverage meter can say WHY a path is not declared instead of only that it is not. Submit the receipt `npx @bettercms-ai/convert` wrote (`--receipt out.json`) for the SAME `briefDigest` get_conversion_brief { complete: true } returned: `{ briefDigest, receipt }`, where the receipt carries `paths: { declared, rewritten, alreadyDeclared, pending[{ route, scope, path, kind, file, reason, message, fix }] }`. `paths.declared` must equal rewritten + alreadyDeclared + pending.length, and each pending `reason` is one of the converter's own (IN_EXPRESSION, AMBIGUOUS_LITERAL, REPEATER_FIXED_LENGTH, PARSE_ERROR, \u2026) \u2014 a path with no receipt row simply reads `not-declared`. \u{1F534} A RECEIPT WITH PENDING PATHS IS A PROGRESS REPORT, NOT A FINISH LINE. The response answers `complete` and `pendingTotal`, and echoes the first 40 pending rows WITH their `fix` \u2014 `{ action, file, line, col?, snippet, why? }`, where `action` is one of `wrap-span` (wrap the literal in a `<span data-bcms-field=\u2026>`), `declare-attr` (add `data-bcms-field=\u2026` to the element at file:line), `bind-expression` (replace the expression with the framework's bcmsField helper), `declare-richtext` (bind the container with the richtext helper), `bind-data` (the literal comes from the data file at file:line \u2014 bind that field) or `manual` (with a one-sentence `why`). Apply every fix in the source, rerun the codemod, resubmit. `complete: true` is the only receipt that ends a conversion \u2014 do not report a site converted on anything less. It is a RECORD, not a release: it changes nothing about the site, and the meter picks it up on the next get_binding_report after the next deploy. A 404 `unknown-brief` means that digest was never issued here, so convert against a brief this project actually returned. \u{1F534} SUBMIT THE `--forms` RECEIPT TOO, AS A SECOND CALL. A run of `npx @bettercms-ai/convert --forms` writes a receipt whose `paths` are all zero and whose account is in a `forms` block (`{ wired, alreadyWired, pending[{ id, name, reason }], notes, wiredForms[{ id, file }] }`). Write it to its own file (`--receipt forms-receipt.json`) and submit it here under the same `briefDigest`: it is stored BESIDE the binding receipt and never touches coverage, and the next release reads `wiredForms` to say which component renders each form. Then read `forms.pending` and publish every form `forms.notes` names in the Forms tab. Requires artifact:write, the same authority as set_binding_mode; on a workspace-wide connection pass `projectId`.",
3417
3447
  z.object({
3418
3448
  briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
3419
3449
  receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim.")
@@ -3482,7 +3512,7 @@ ${d.outline}` : "Proposed Content structure.", d);
3482
3512
  def(
3483
3513
  "update_page",
3484
3514
  "Edit a page",
3485
- "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). 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_page_field / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call. 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,
3515
+ "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). 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_page_field / 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,
3486
3516
  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() }).shape,
3487
3517
  async (c, a) => {
3488
3518
  const res = await c.fetchJSON(
@@ -3554,7 +3584,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
3554
3584
  def(
3555
3585
  "get_binding_report",
3556
3586
  "Check what on the live site is editable",
3557
- "The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; this returns what that scan found, per slot: `mode` ('text-match' = bindings guessed from rendered text, 'declared' = the template declares them), `pagesInspected`, `bound` (elements carrying a binding), and `unmatched` \u2014 per page, each path with its kind and the reason it failed (not-declared / ambiguous-text / no-element). DEPLOY FIRST: before any release there is no report and this answers pages 0, mode null, refreshRequired true. It certifies exactly one thing \u2014 that every non-empty field of every page has SOME element carrying its path. It cannot see copy that was never modelled, so diff each route's visible text against its entry values yourself before calling a page done. `builtRoutes` lists the routes the live build has HTML for (null when unknown: runtime release, >500 routes, or a report older than this field). `canvas.lane` names the live-preview lane this build gives the editor \u2014 `bridge` (the build ships BcmsDraftBridge), `draft-route` (a node runtime rendering drafts server-side) or `none`, in which case get_next_steps carries the recipe (playbook section 11); null means the report predates the field. `unaddressable` counts visible text on the live site that no field owns \u2014 the one thing `unmatched` structurally cannot see, because it only ever speaks about fields that already exist. EVERY release measures it server-side on every inspected page: `{count, pages, measuredAt, routes:[{path, count, visible, buckets:[{tag, context, chars, nodes, samples}]}]}`, where `path` is the route (the empty string is the home page), `visible` its countable characters (ornaments, skip links and the platform badge excluded from both sides) and `buckets` where the unowned text is, biggest first. Anything over `count / visible > 0.02` is copy the CMS cannot see: 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 section 11 has the loop. An author turning the editor's Unbound text control on posts a bare count of text NODES for the route they opened (no `visible`, no buckets); those rows are reported only for a build the release never measured, never summed with the release's characters. `skipReasons` says why pages were not inspected (`no-html`, `route-cap`, `authored-content` = the singleton lane declined the project, so NO route of it has a page), and `coverage.error` = `nothing-bound` means not one element of this build carries a binding, so the percentage beside it describes a site this build is not. `unaddressable` is measured PER SLOT; on a promote-gated project pass slot:'current' to read the tree the editor frames. Pass `slot` ('current' or 'staging') to read the other tree; the default is the slot this project's releases land in.",
3587
+ "The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; this returns what that scan found, per slot: `mode` ('text-match' = bindings guessed from rendered text, 'declared' = the template declares them), `pagesInspected`, `bound` (elements carrying a binding), and `unmatched` \u2014 per page, each path with its kind and the reason it failed (not-declared / ambiguous-text / no-element). DEPLOY FIRST: before any release there is no report and this answers pages 0, mode null, refreshRequired true. `refreshReason` says which of two states that is: `never-generated` \u2014 no report was ever written for this slot, and only a release BetterCMS itself builds and serves writes one, so a HEADLESS site never gets one and redeploying the same way will not change it (verify that site by fetching it); `outdated` \u2014 a report exists in an older shape, and the next release rewrites it. It certifies exactly one thing \u2014 that every non-empty field of every page has SOME element carrying its path. It cannot see copy that was never modelled, so diff each route's visible text against its entry values yourself before calling a page done. `builtRoutes` lists the routes the live build has HTML for (null when unknown: runtime release, >500 routes, or a report older than this field). `canvas.lane` names the live-preview lane this build gives the editor \u2014 `bridge` (the build ships BcmsDraftBridge), `draft-route` (a node runtime rendering drafts server-side) or `none`, in which case get_next_steps carries the recipe (playbook section 11); null means the report predates the field. `unaddressable` counts visible text on the live site that no field owns \u2014 the one thing `unmatched` structurally cannot see, because it only ever speaks about fields that already exist. EVERY release measures it server-side on every inspected page: `{count, pages, measuredAt, routes:[{path, count, visible, buckets:[{tag, context, chars, nodes, samples}]}]}`, where `path` is the route (the empty string is the home page), `visible` its countable characters (ornaments, skip links and the platform badge excluded from both sides) and `buckets` where the unowned text is, biggest first. Anything over `count / visible > 0.02` is copy the CMS cannot see: 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 section 11 has the loop. An author turning the editor's Unbound text control on posts a bare count of text NODES for the route they opened (no `visible`, no buckets); those rows are reported only for a build the release never measured, never summed with the release's characters. `skipReasons` says why pages were not inspected (`no-html`, `route-cap`, `authored-content` = the singleton lane declined the project, so NO route of it has a page), and `coverage.error` = `nothing-bound` means not one element of this build carries a binding, so the percentage beside it describes a site this build is not. `unaddressable` is measured PER SLOT; on a promote-gated project pass slot:'current' to read the tree the editor frames. Pass `slot` ('current' or 'staging') to read the other tree; the default is the slot this project's releases land in.",
3558
3588
  z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read; defaults to the one this project deploys to") }).shape,
3559
3589
  async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
3560
3590
  ),
@@ -3592,7 +3622,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
3592
3622
  def(
3593
3623
  "componentize_sections",
3594
3624
  "Turn this site's derived sections into components",
3595
- "Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections and say how many components it will create and which pages it will rewrite. It creates each proposed component as a DRAFT (its `sectionType` family, category 'section', placeable on any page) and replaces each page's DRAFT blocks with an ordered list of `component` instances \u2014 one per group, each carrying `props.bind: \"<groupKey>\"`, which points at the page field group that already holds the copy. So nothing is copied and nothing moves: the page keeps its `fields`, its values and its bindings, click-to-edit keeps working and the coverage meter does not change. Pass the plan's `digest`; a 409 `stale-plan` means the site changed since you read that plan, so read it again, show the user what changed and confirm again. Call it with `dryRun: true` first \u2014 same receipt, nothing written. Running it twice is safe: a group that already has a placement comes back in `sections.pending` as ALREADY_COMPONENTIZED and no second component is created. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages before this reaches a visitor. Then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`. `copy` says WHO OWNS THE COPY. The default `bind` is the above: the words stay in the page's field group. `copy: \"instance\"` is the page-builder model \u2014 each placement takes that group's CURRENT draft values into its own `props.overrides` (a repeater becomes one table prop holding every row), records which page path each came from in `props.source`, and drops `props.bind`; the page's fields are KEPT and marked `origin: \"componentized\"`, so nothing is lost and the editor stops showing a second place to type the same words. Run it again and it converts nothing and duplicates nothing. `sections` narrows a run to named groupKeys (`hero`, `group-fast`); a key the plan does not list on the pages you named is refused as `unknown-section` rather than quietly doing nothing. Every component it CREATES is filed into a Component Group (the folders of the dashboard's Components tab): the one you name in `group` (found or created), else its section family \u2014 nav, header, footer or menu \u2192 Layout, an unnamed section \u2192 Sections, otherwise the family name (Hero, FAQ). A component it reuses keeps the Group it has; the receipt's `components.groups` lists the Groups it filed into.",
3625
+ "Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections and say how many components it will create and which pages it will rewrite. It creates each proposed component as a DRAFT (its `sectionType` family, category 'section', placeable on any page) and replaces each page's DRAFT blocks with an ordered list of `component` instances \u2014 one per group, each carrying `props.bind: \"<groupKey>\"`, which points at the page field group that already holds the copy. So nothing is copied and nothing moves: the page keeps its `fields`, its values and its bindings, click-to-edit keeps working and the coverage meter does not change. Pass the plan's `digest`; a 409 `stale-plan` means the site changed since you read that plan, so read it again, show the user what changed and confirm again. Call it with `dryRun: true` first \u2014 same receipt, nothing written. Read its `components.wouldDuplicate` before applying: each id is an existing component of the same family this run would sit a NEW one beside, because reuse is by identity and slug, never by family. Place those by hand or accept the second component knowingly. Running it twice is safe: a group that already has a placement comes back in `sections.pending` as ALREADY_COMPONENTIZED and no second component is created. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages before this reaches a visitor. Then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`. `copy` says WHO OWNS THE COPY. The default `bind` is the above: the words stay in the page's field group. `copy: \"instance\"` is the page-builder model \u2014 each placement takes that group's CURRENT draft values into its own `props.overrides` (a repeater becomes one table prop holding every row), records which page path each came from in `props.source`, and drops `props.bind`; the page's fields are KEPT and marked `origin: \"componentized\"`, so nothing is lost and the editor stops showing a second place to type the same words. Run it again and it converts nothing and duplicates nothing. `sections` narrows a run to named groupKeys (`hero`, `group-fast`); a key the plan does not list on the pages you named is refused as `unknown-section` rather than quietly doing nothing. Every component it CREATES is filed into a Component Group (the folders of the dashboard's Components tab): the one you name in `group` (found or created), else its section family \u2014 nav, header, footer or menu \u2192 Layout, an unnamed section \u2192 Sections, otherwise the family name (Hero, FAQ). A component it reuses keeps the Group it has; the receipt's `components.groups` lists the Groups it filed into.",
3596
3626
  z.object({
3597
3627
  digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
3598
3628
  pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
@@ -3839,13 +3869,18 @@ ${res.warnings.join("\n")}` : summary, res.data);
3839
3869
  // Cast: the SDK's ContentModelField types `options` as string[], narrower than the API,
3840
3870
  // which also takes {label, value} (backend selectFieldSchema).
3841
3871
  ...args.fields ? { fields: args.fields.map(toField) } : {},
3872
+ ...args.data !== void 0 ? { data: args.data } : {},
3842
3873
  ...args.metaTitle !== void 0 ? { metaTitle: args.metaTitle } : {},
3843
3874
  ...args.metaDescription !== void 0 ? { metaDescription: args.metaDescription } : {}
3844
3875
  });
3845
- return ok(
3846
- `Created ${page.pageType ?? "page"} page '${page.title}' (id ${page.id}, slug ${page.slug}) with ${page.fields.length} field(s).`,
3847
- page
3848
- );
3876
+ const { warnings, seedErrors, ...created } = page;
3877
+ const summary = `Created ${created.pageType ?? "page"} page '${created.title}' (id ${created.id}, slug ${created.slug}) with ${created.fields.length} field(s).`;
3878
+ const notes = [
3879
+ ...warnings ?? [],
3880
+ ...seedErrors ? Object.entries(seedErrors).map(([key, why]) => ` ${key}: ${why}`) : []
3881
+ ];
3882
+ return ok(notes.length ? `${summary}
3883
+ ${notes.join("\n")}` : summary, created);
3849
3884
  })
3850
3885
  )
3851
3886
  },
@@ -4015,7 +4050,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
4015
4050
  name: "update_content_entry",
4016
4051
  config: {
4017
4052
  title: "Update a content entry's values",
4018
- description: "Update a content entry's `data` (field values) and/or status by id. `data` is keyed by field key; a nested 'array' (zone) value is an object { nonRepeatable: {\u2026}, repeatable: [{\u2026}] }, a primitive 'array' is a plain list. Use this to edit an existing entry; for a singleton page prefer set_page_content. " + ENTRY_META_NOTE,
4053
+ description: "Update a content entry's `data` (field values) and/or status by id. `data` is keyed by field key; a nested 'array' (zone) value is an object { nonRepeatable: {\u2026}, repeatable: [{\u2026}] }, a primitive 'array' is a plain list. Use this to edit an existing entry; for a singleton page prefer set_page_content. \u{1F534} `data` REPLACES the entry's whole value object: an optional field you leave out is DELETED, and a required one you leave out fails the whole write. To change one field, read the entry (get_content_entry), change that key, and send the whole object back. " + ENTRY_META_NOTE,
4019
4054
  inputSchema: updateEntryInput.shape
4020
4055
  },
4021
4056
  handler: guard(
@@ -4222,7 +4257,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
4222
4257
  name: "publish_layout",
4223
4258
  config: {
4224
4259
  title: "Publish the Global Layout draft",
4225
- description: "Publish the connected project's GLOBAL Layout draft (navigation, footer, every reserved section) so the live site builds from it. Until this is called the layout stays draft and get_layout copy:'published' answers PUBLISHED_LAYOUT_UNAVAILABLE \u2014 site chrome authored with update_layout is NOT live. Read get_layout first and pass its revision as ifMatch; a stale revision returns 409 \u2014 re-read, never retry blindly. A 422 lists validation issues to fix with update_layout first. Verify with get_layout copy:'published' and check the copy echo \u2014 this tool's own response is the write's echo, not a receipt. Page overrides go live with the page (update_page status:'published'). A 403 PUBLISH_NOT_GRANTED means this connection can author drafts but cannot publish \u2014 say so and let the user allow publishing or publish from the dashboard.",
4260
+ description: "Publish the connected project's GLOBAL Layout draft (navigation, footer, every reserved section) so the live site builds from it. Until this is called the layout stays draft and get_layout copy:'published' answers PUBLISHED_LAYOUT_UNAVAILABLE \u2014 site chrome authored with update_layout is NOT live. Read get_layout first and pass its revision as ifMatch; a stale revision returns 409 \u2014 re-read, never retry blindly. A 422 lists validation issues to fix with update_layout first. A new project's chrome is draft: publish_component navigation-default and footer-default first (each can publish while the other is draft), then publish_layout. navigation.logo is a REQUIRED image \u2014 new projects get a generated one; if it is empty (required_value) or not a stored asset (image_asset_missing), upload one and set it with update_layout as { id, url, name, altText }. Verify with get_layout copy:'published' and check the copy echo \u2014 this tool's own response is the write's echo, not a receipt. Page overrides go live with the page (update_page status:'published'). A 403 PUBLISH_NOT_GRANTED means this connection can author drafts but cannot publish \u2014 say so and let the user allow publishing or publish from the dashboard.",
4226
4261
  inputSchema: publishLayoutInput.shape
4227
4262
  },
4228
4263
  handler: guard(async (args) => withClient(async (client) => {
@@ -4333,7 +4368,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
4333
4368
  name: "publish_components",
4334
4369
  config: {
4335
4370
  title: "Publish many components in one call",
4336
- description: "Publish up to 100 components in ONE call \u2014 the same act as publish_component, once per item, copying each DRAFT definition to the live copy. Each item reports `{ ok, status, error, readiness }` and a failure never stops the run. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: it means this exact variant still needs passing implementation evidence and a HUMAN's approval in the BetterCMS dashboard, which no tool on this connection can grant and which has no bulk form. The item's `readiness` names what is missing \u2014 relay that to the user as the list of what only they can approve. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish; 422 means the publish would break a Layout, and the item names which.",
4371
+ description: "Publish up to 100 components in ONE call \u2014 the same act as publish_component, once per item, copying each DRAFT definition to the live copy. Each item reports `{ ok, status, error, readiness }` and a failure never stops the run. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: no tool on this connection can clear it, and it has no bulk form. It is never a component you just made with create_component in no variant group \u2014 that is dashboard-managed and publishes on the preflight alone. The refusal is for a REPO-managed component without exact validation evidence and a human's approval, or for a variant-group member (the provisioned navigation and footer) whose group's canonical-input contract is not published. Report its `readiness`. The item's `readiness` names what is missing \u2014 relay that to the user as the list of what only they can approve. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish; 422 means the publish would break a Layout, and the item names which.",
4337
4372
  inputSchema: publishComponentsInput.shape
4338
4373
  },
4339
4374
  handler: guard(
@@ -4523,11 +4558,61 @@ ${lines.join("\n")}`, found);
4523
4558
  })
4524
4559
  )
4525
4560
  },
4561
+ {
4562
+ name: "get_component_readiness",
4563
+ config: {
4564
+ title: "Read a component's Output validation readiness",
4565
+ description: "Read what stands between a component and a validated Output: `validation.gap` and `validation.fix` say why a validation run cannot take it yet (COMPONENT_SOURCE_NOT_RECORDED: record its file with set_component_source, or place it and deploy a build whose markup carries its data-bcms-block / data-bcms-field attributes), and the readiness says where its request, evidence and approval stand. Call it after authoring a component and after every validation run. outputReady means the Output renders; publishReady also needs the owner's Visual Approval in the dashboard, which no tool can give.",
4566
+ inputSchema: clearComponentSourceInput.shape
4567
+ },
4568
+ handler: guard(
4569
+ async (args) => withClient(async (client) => {
4570
+ const res = await client.fetchJSON(
4571
+ client.url(`/management/components/${encodeURIComponent(args.componentId)}/readiness`)
4572
+ );
4573
+ return ok("Component validation readiness.", res.data);
4574
+ })
4575
+ )
4576
+ },
4577
+ {
4578
+ name: "declare_component_route",
4579
+ config: {
4580
+ title: "Declare where a component's preview is served",
4581
+ description: "Declare where this component's preview is served in the app (the dashboard's \"Show preview\"). Omit `path` to use the server's default route; pass `nativeViewports` only to check sizes other than the defaults. The origin is the project's preview URL and is never taken from you. Answers the readiness.",
4582
+ inputSchema: componentValidationRouteInput.shape
4583
+ },
4584
+ handler: guard(
4585
+ async (args) => withClient(async (client) => {
4586
+ const res = await client.fetchJSON(
4587
+ client.url(`/management/components/${encodeURIComponent(args.componentId)}/adapter`),
4588
+ { method: "PUT", body: JSON.stringify({ path: args.path, nativeViewports: args.nativeViewports }) }
4589
+ );
4590
+ return ok("Preview route declared.", res.data);
4591
+ })
4592
+ )
4593
+ },
4594
+ {
4595
+ name: "request_component_validation",
4596
+ config: {
4597
+ title: "Request Output validation for a component",
4598
+ description: "Ask for this component's Output to be validated by YOU, the connected agent (provider user-agent). Record its source first (set_component_source, or the componentize codemod plus a deploy): a component nothing renders is refused with 422 and the fix, and no request is opened. After a 202, run `npx @bettercms-ai/preview-runtime validate --local --request <requestId>` in the app's repository with BCMS_API_KEY set in the environment (never pass a key in a tool call), then read get_component_readiness. Your evidence is self-attested: publishing still needs the owner's Visual Approval in the dashboard \u2014 never claim you approved it. CI validation (github-app) runs in the owner's repository at their cost, so it is refused here with VALIDATION_PROVIDER_DASHBOARD_ONLY: ask the owner to start it from the dashboard. 409 COMPONENT_ORCHESTRATION_V2_REQUIRED means continue in Agent Dock.",
4599
+ inputSchema: clearComponentSourceInput.shape
4600
+ },
4601
+ handler: guard(
4602
+ async (args) => withClient(async (client) => {
4603
+ const res = await client.fetchJSON(
4604
+ client.url(`/management/components/${encodeURIComponent(args.componentId)}/implementation-requests`),
4605
+ { method: "POST", body: JSON.stringify({}) }
4606
+ );
4607
+ return ok("Validation requested. Run `npx @bettercms-ai/preview-runtime validate --local --request <requestId>` in the repository next.", res.data);
4608
+ })
4609
+ )
4610
+ },
4526
4611
  {
4527
4612
  name: "publish_component",
4528
4613
  config: {
4529
4614
  title: "Publish a component",
4530
- description: "Publish one project-scoped component variant after its exact implementation evidence and human approval are ready: copy its DRAFT definition to the live copy and re-bake every published page that embeds it. This is the ONLY way a component reaches the live site. Built-in `builtin:*` rows are locked blueprints, not implementations; materialize one with create_component first. Workspace-global component publication is currently fail-closed. create_component and update_component write drafts, and an unpublished component renders as NOTHING live, with no error. Publish every component you place on a page. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish: say so and let the user publish from the dashboard. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: this exact variant still needs passing implementation evidence and a HUMAN's approval in the BetterCMS dashboard, which no tool on this connection can grant. The response's `readiness` names what is missing \u2014 relay it to the user instead of retrying or routing around it. 422 means publishing would break a Layout that uses it \u2014 the response names which.",
4615
+ description: "Publish one project-scoped component variant after its exact implementation evidence and human approval are ready: copy its DRAFT definition to the live copy and re-bake every published page that embeds it. This is the ONLY way a component reaches the live site. Built-in `builtin:*` rows are locked blueprints, not implementations; materialize one with create_component first. Workspace-global component publication is currently fail-closed. create_component and update_component write drafts, and an unpublished component renders as NOTHING live, with no error. Publish every component you place on a page. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish: say so and let the user publish from the dashboard. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: no tool on this connection can clear it. It is never a component you just made with create_component in no variant group \u2014 that is dashboard-managed and publishes on the preflight alone. The refusal is for a REPO-managed component without exact validation evidence and a human's approval, or for a variant-group member (the provisioned navigation and footer) whose group's canonical-input contract is not published. Report its `readiness`. The response's `readiness` names what is missing \u2014 relay it to the user instead of retrying or routing around it. 422 means publishing would break a Layout that uses it \u2014 the response names which.",
4531
4616
  inputSchema: getComponentInput.shape
4532
4617
  },
4533
4618
  handler: guard(
@@ -5084,7 +5169,10 @@ axis: it is the picker TAB (hero / content / social-proof / conversion for secti
5084
5169
  footer for chrome; form; custom for page-specific).
5085
5170
 
5086
5171
  **4. Editor fields are \`props\`.** Types: \`text richtext image url boolean number select group
5087
- table slot\`. Always set \`defaultValue\`. \`required\`, \`helpText\` and \`placeholder\` are editor
5172
+ table slot form\`. Always set \`defaultValue\`. \`form\` points at ONE Form document: target a
5173
+ \`form\` block's \`props\` and set \`defaultValue\` to \`{formId}\`. NEVER re-declare a form's fields
5174
+ as props \u2014 the Form document owns them, a second copy drifts on the first edit, and the Forms
5175
+ tab is where an author edits them. \`required\`, \`helpText\` and \`placeholder\` are editor
5088
5176
  hints the dashboard renders \u2014 they are NOT enforced at write time, so an override missing a
5089
5177
  required prop still saves; \`get_site_composition\` is what reports those. Use \`select\` with
5090
5178
  \`config.options\` for layout / theme / alignment / spacing variants that change only styling,
@@ -5212,8 +5300,9 @@ hand. THE ORDER, and every step of it matters:
5212
5300
  fallback, and declares each binding.
5213
5301
  2b. IF THE BRIEF CARRIES \`forms\`, wire them too:
5214
5302
  \`npx @bettercms-ai/convert --forms --brief brief.json --root . --receipt forms-receipt.json\`
5215
- \u2014 ITS OWN receipt file, because the forms receipt claims no path and submitting it would
5216
- overwrite the coverage the binding run just earned with zeros.
5303
+ \u2014 ITS OWN receipt file, submitted with submit_conversion_receipt as a second call: the
5304
+ server stores its \`forms\` block beside the binding receipt (coverage is untouched), and
5305
+ the next release reads \`wiredForms\` to say which component renders each form.
5217
5306
  A site imported by phase 1 has a DRAFT form row per \`<form>\` in its build, so the Forms tab
5218
5307
  is full while the repository's markup still posts wherever it always did \u2014 to nothing, or to
5219
5308
  somebody else's endpoint. This pass writes the endpoint, the form id, a marker per field and
@@ -5290,7 +5379,7 @@ conversion is yours to write.
5290
5379
  \`{...bcms.home.hero.title}\` / \`{...bcms.blog.features.$(i)}\`. The hand form is
5291
5380
  \`data-bcms-field="<path>"\` (plus \`data-bcms-kind="richtext"|"image"\`), the \xA711 layout markers
5292
5381
  for nav and footer, and \`<div data-bcms-field="body" data-bcms-kind="document">\` around a
5293
- Portable Text render.
5382
+ Portable Text render, whose per-block elements carry their own \`data-bcms-kind="richtext"\`.
5294
5383
  **A value that lives in an ATTRIBUTE \u2014 an \`href\`, an \`alt\`, an \`src\` \u2014 rides
5295
5384
  \`data-bcms-props\`, and its grammar is PIPES, NOT JSON:**
5296
5385
  \`data-bcms-props="<path>|<kind>|<domAttribute>"\`, semicolon-separated for several on one
@@ -5582,8 +5671,11 @@ error-prone, so go slow and confirm. Never guess the layout.
5582
5671
  the same prop keys across a family or a swap drops content.
5583
5672
  4. **Props** \u2014 declare only what should really be editable:
5584
5673
  { key, label, target: { blockId, path },
5585
- type: text|richtext|image|url|boolean|number|select|group|table|slot }. 'select' needs
5586
- config.options; 'group'/'table' need config.fields; 'slot' holds ONE nested component.
5674
+ type: text|richtext|image|url|boolean|number|select|group|table|slot|form }. 'select' needs
5675
+ config.options; 'group'/'table' need config.fields; 'slot' holds ONE nested component;
5676
+ 'form' holds ONE Form document \u2014 point target.path at 'props' on an empty form block and set
5677
+ defaultValue to {formId}. Never re-declare a form's FIELDS as props: the Form document owns
5678
+ them, and a second copy is a second source of truth that drifts on the first edit.
5587
5679
  5. **Confirm the structure** (AskUserQuestion: show the block tree), then \`create_component\`.
5588
5680
  6. **\`publish_component\`.** It lands as a DRAFT, and a draft component renders as NOTHING on
5589
5681
  the live site \u2014 no error, no placeholder. This step is not optional.
@@ -6151,7 +6243,7 @@ function buildServer(deps) {
6151
6243
  // one definition of done; without this an agent converts the page it landed on and stops.
6152
6244
  instructions: "When the user asks to make a site or all of its pages editable, to convert it, or to bind its fields: this is playbook \xA713. Read `bettercms://playbook/schema` \xA713, call get_binding_report and get_conversion_brief { complete: true }, convert EVERY route the brief lists, and finish only when get_binding_report shows coverage.pending empty on every route \u2014 not when the first page works. On a workspace-wide connection pass projectId on every call; never ask the user to re-scope the connection. When the user asks to componentize the whole site, to turn every section into a component, or to build a component library from the site: this is playbook \xA712. Read `bettercms://playbook/schema` \xA712, start with get_site_composition, and use the batch tools \u2014 create_components, compose_pages, update_layout with `commands`, publish_components \u2014 rather than one call per component. Finish with get_site_composition and tell the user what the platform does not model (cookie banners, modals, breadcrumbs, pagination) and which components still need the owner's approval in the dashboard before they can be published. BetterCMS never executes a customer's Section renderer or app code. An ordinary MCP connection is not a push runner: explicitly poll list_section_validation_requests, claim one request at an exact git commit, run implementation and responsive checks inside the user's own repository and real app shell, then submit manifest + validation with that requestId and complete it\u2014or truthfully fail it when implementation/evidence is missing. Never invent a manifest, a passing validation, or visual evidence; these tools cannot grant the separate human Visual Approval required for publication. " + // The default after authoring (the structure standard): organise what you made. Same
6153
6245
  // sentence as the hosted connector's MCP_INSTRUCTIONS.
6154
- STRUCTURE_DEFAULT_INSTRUCTION
6246
+ STRUCTURE_DEFAULT_INSTRUCTION + " " + SKILLS_ROUTING_INSTRUCTION
6155
6247
  }
6156
6248
  );
6157
6249
  server.registerResource(