@bettercms-ai/mcp 0.41.0 → 0.42.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
@@ -3107,7 +3107,7 @@ function buildToolDefs(deps) {
3107
3107
  def(
3108
3108
  "submit_conversion_receipt",
3109
3109
  "Record what the conversion codemod could and could not do",
3110
- "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 }] }`. `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`. 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`.",
3110
+ "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`.",
3111
3111
  z.object({
3112
3112
  briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
3113
3113
  receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim.")
@@ -3171,7 +3171,20 @@ function buildToolDefs(deps) {
3171
3171
  "Edit a page",
3172
3172
  "Edit a page: title, slug, SEO metaTitle/metaDescription, publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first). 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. " + SECTION_DOCTRINE,
3173
3173
  z.object({ 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(), status: z.enum(["draft", "published"]).optional() }).shape,
3174
- async (c, a) => ok("Updated page.", await data(c, "PATCH", `/management/pages/${s(a.pageId)}/meta`, { title: a.title, slug: a.slug, blockJson: a.blockJson, metaTitle: a.metaTitle, metaDescription: a.metaDescription, status: a.status }))
3174
+ async (c, a) => {
3175
+ const res = await c.fetchJSON(
3176
+ c.url(`/management/pages/${s(a.pageId)}/meta`),
3177
+ {
3178
+ method: "PATCH",
3179
+ body: JSON.stringify({ title: a.title, slug: a.slug, blockJson: a.blockJson, metaTitle: a.metaTitle, metaDescription: a.metaDescription, status: a.status })
3180
+ }
3181
+ );
3182
+ const note = res.redirectNote;
3183
+ return ok(
3184
+ note ? `Updated page. An existing redirect from ${note.source} already points to ${note.existingDestination}; it wasn't changed, so the old address does not redirect to the new one.` : "Updated page.",
3185
+ res.data
3186
+ );
3187
+ }
3175
3188
  ),
3176
3189
  // ── Code + deploy (parity with remote /mcp; needs artifact:write) ──
3177
3190
  // Both grant shapes carry that scope now; a workspace-wide one names its target per
@@ -4790,8 +4803,14 @@ an empty string on the live site.
4790
4803
 
4791
4804
  ## 13. Convert an imported repo into a CMS-backed, editable site
4792
4805
 
4793
- \xA711 says a deploy does not make a site editable. This is the recipe that does, and it ends in a
4794
- receipt you can read: \`get_binding_report\` says \`mode: "declared"\` with zero unmatched paths.
4806
+ \xA711 says a deploy does not make a site editable. This is the recipe that does.
4807
+
4808
+ \u{1F534} **A CONVERSION ENDS ONLY WHEN NOTHING IS PENDING.** The codemod's first pass never
4809
+ declares every path \u2014 measured on a real site, one pass declared 282 of 767 \u2014 and every path it
4810
+ leaves in \`paths.pending\` carries a \`fix\` naming the file, the line and the one edit. Apply
4811
+ them, rerun, resubmit, and repeat until \`submit_conversion_receipt\` answers \`complete: true\`.
4812
+ That boolean is the finish line; a receipt with pending paths is a progress report. The
4813
+ \`convert_site\` MCP prompt walks exactly this loop.
4795
4814
 
4796
4815
  **Scope, before you start.**
4797
4816
 
@@ -4865,21 +4884,30 @@ hand. THE ORDER, and every step of it matters:
4865
4884
  could not match are in \`forms.pending\` with a reason (\`FORM_NOT_IN_SOURCE\`,
4866
4885
  \`FIELD_UNMATCHED\`, \`FORM_AMBIGUOUS\`, \u2026) \u2014 it refuses rather than guessing, because a form
4867
4886
  wired to the wrong id delivers the customer's leads into another form's inbox.
4868
- 3. REVIEW THE PENDING LIST. Every path the codemod could not do is in \`paths.pending\` with a
4869
- reason (\`IN_EXPRESSION\`, \`AMBIGUOUS_LITERAL\`, \`REPEATER_FIXED_LENGTH\`, \`PARSE_ERROR\`, \u2026).
4870
- Do those by hand, or decide they are genuinely not convertible \u2014 do not skip past them.
4871
- 4. \`submit_conversion_receipt { briefDigest, receipt }\` with the receipt file, verbatim. This is
4872
- what lets the coverage meter say WHY a path is undeclared instead of only that it is; without
4873
- it every one of them reads \`not-declared\`, which looks like a broken site rather than work
4874
- with a reason. It records and releases nothing.
4887
+ 3. APPLY EVERY PENDING FIX, THEN RERUN. Each entry of \`paths.pending\` carries its reason
4888
+ (\`IN_EXPRESSION\`, \`AMBIGUOUS_LITERAL\`, \`REPEATER_FIXED_LENGTH\`, \`PARSE_ERROR\`, \u2026) and,
4889
+ where the codemod could work one out, a \`fix\`: \`{ action, file, line, col?, snippet, why? }\`.
4890
+ Go to \`file\`:\`line\` and do what \`action\` says \u2014 \`wrap-span\` (wrap the literal in
4891
+ \`<span data-bcms-field="\u2026">\`), \`declare-attr\` (add \`data-bcms-field="\u2026"\` to that element),
4892
+ \`bind-expression\` (replace the expression with the framework's bcmsField helper),
4893
+ \`declare-richtext\` (bind the container with the richtext helper), \`bind-data\` (bind the field
4894
+ in the data file or collection entry it names) or \`manual\` (do what its \`why\` says). A row
4895
+ with no \`fix\` is still yours. Then RERUN step 2 and repeat until \`paths.pending\` is empty \u2014
4896
+ the codemod is idempotent, so a second run over converted source rewrites nothing.
4897
+ 4. \`submit_conversion_receipt { briefDigest, receipt }\` with the receipt file, verbatim. It
4898
+ answers \`complete\`, \`pendingTotal\` and the first 40 pending rows with their \`fix\`. \`complete: true\` \u2014 nothing
4899
+ pending \u2014 is the ONLY receipt that ends a conversion; on anything less go back to step 3. It
4900
+ is also what lets the coverage meter say WHY a path is undeclared instead of only that it is.
4901
+ It records and releases nothing.
4875
4902
  5. Push, or \`deploy_project\`. A PLAIN HTML repo \u2014 no \`package.json\` \u2014 deploys by PUSH ONLY:
4876
4903
  \`deploy_project\` refuses such an archive by design, so pushing is the whole lane. Either way,
4877
4904
  poll \`get_deploy_status\` until \`slot\` is \`current\` (or \`awaitingPromote\` is false) before you
4878
4905
  read the report in step 6 \u2014 a report read against the previous release is the previous
4879
4906
  release's verdict.
4880
4907
  6. \`get_binding_report\`. Alongside \`unmatched\` it now carries \`coverage\` \u2014 \`declared\` (what the
4881
- brief listed), \`bound\` (what the build declares) and \`pending\` with the reasons. Fix what it
4882
- names and release again until \`coverage.pending\` is empty.
4908
+ brief listed), \`bound\` (what the build declares) and \`pending\` with the reasons. It must read
4909
+ \`unmatched\` 0 and \`unaddressable\` under 2% of each route's visible text (\xA711). Fix what it
4910
+ names IN THE SOURCE and release again until \`coverage.pending\` is empty.
4883
4911
  7. \`set_binding_mode { declaredBindings: true }\` and release ONE MORE TIME \u2014 the mode applies to
4884
4912
  the next release, not to the one already out.
4885
4913
 
@@ -5108,6 +5136,31 @@ disagrees with the array the moment anyone drags a row in the builder.
5108
5136
  // src/prompts.ts
5109
5137
  var STRUCTURE_RULE = `### Page structure (non-negotiable)
5110
5138
  ${SECTION_DOCTRINE}`;
5139
+ var CONVERT_SITE_TEXT = (projectId) => `Convert my site so every visible element is editable in BetterCMS${projectId ? `, project ${projectId}` : ""}.
5140
+ Do not stop at the plan or at a partial receipt \u2014 finish the whole site.
5141
+
5142
+ 1. \`list_projects\`; pass \`projectId\`${projectId ? ` (${projectId})` : ""} on every project tool from here on. If the project
5143
+ is not listed the grant is on another workspace \u2014 stop and say so.
5144
+ 2. \`get_conversion_plan\`. On a 404 \`no-approved-plan\`, call
5145
+ \`get_conversion_brief { complete: true }\` and follow \`cursor\` to the LAST page, keeping every
5146
+ page \u2014 a codemod fed the capped brief converts a truncated site. Write them to \`brief.json\`.
5147
+ 3. \`pull_project_source\` (or clone the \`github\` remote it names) and work from the SOURCE, never
5148
+ from the deployed HTML.
5149
+ 4. \`npx @bettercms-ai/convert --brief brief.json --root . --receipt receipt.json\`; read \`git diff\`.
5150
+ 5. For EVERY entry of \`paths.pending\`, go to its \`fix.file\`:\`fix.line\` and do what \`fix.action\`
5151
+ says: \`wrap-span\` wrap the literal in \`<span data-bcms-field="\u2026">\`; \`declare-attr\` add
5152
+ \`data-bcms-field="\u2026"\` to that element; \`bind-expression\` replace the expression with the
5153
+ framework's bcmsField helper; \`declare-richtext\` bind the container with the richtext helper;
5154
+ \`bind-data\` bind the field in the data file it names; \`manual\` do what its \`why\` says. A row
5155
+ with no \`fix\` is still yours. Then rerun step 4 until \`paths.pending\` is EMPTY.
5156
+ 6. \`submit_conversion_receipt { briefDigest, receipt }\`. It must answer \`complete: true\`; if it
5157
+ does not, go back to step 5 \u2014 that is the only receipt that ends a conversion.
5158
+ 7. Deploy (push, or \`deploy_project\`) and poll \`get_deploy_status\` until the release is live.
5159
+ 8. \`get_binding_report\` must read \`unmatched\` 0 and \`unaddressable\` under 2% of each route's
5160
+ visible text. Fix what it names in the SOURCE, deploy, re-read (playbook section 11).
5161
+ 9. \`set_binding_mode { declaredBindings: true }\`, deploy AGAIN and poll \u2014 the mode applies to the
5162
+ NEXT release \u2014 then confirm \`get_binding_report\` reads mode "declared", unmatched 0, bound
5163
+ above 0, \`coverage.pending\` empty. That report is your final answer to me.`;
5111
5164
  var SCHEMA_PROPOSAL_FLOW = `### Whole-project design (confirm-first) \u2192 \`create_component\` / \`create_page\` / \`create_content_model\`
5112
5165
  Design the WHOLE project from its brief or its code, and **confirm the shape with the user
5113
5166
  BEFORE creating anything**. Never silently guess.
@@ -5487,6 +5540,19 @@ in the dashboard \u2014 those render as an empty string until they are published
5487
5540
  ]
5488
5541
  })
5489
5542
  );
5543
+ server.registerPrompt(
5544
+ "convert_site",
5545
+ {
5546
+ title: "Convert a site so nothing is left pending (guided)",
5547
+ description: "Run the conversion end to end: pull the source, run @bettercms-ai/convert, apply every fix it lists as pending, rerun until nothing is pending, and finish only when submit_conversion_receipt returns complete and get_binding_report reads unmatched 0.",
5548
+ argsSchema: {
5549
+ projectId: z2.string().optional().describe("the project to convert; omit if this connection is scoped to one")
5550
+ }
5551
+ },
5552
+ ({ projectId }) => ({
5553
+ messages: [{ role: "user", content: { type: "text", text: CONVERT_SITE_TEXT(projectId) } }]
5554
+ })
5555
+ );
5490
5556
  server.registerPrompt(
5491
5557
  "build_site",
5492
5558
  {