@bettercms-ai/mcp 0.51.1 → 0.53.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
@@ -1999,6 +1999,7 @@ import { DeviceAuthPendingError } from "@bettercms-ai/device-auth";
1999
1999
  // src/structure-playbook.ts
2000
2000
  var STRUCTURE_PLAYBOOK_URI = "bettercms://playbook/structure";
2001
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.";
2002
2003
  var STRUCTURE_EXAMPLE_PAYLOAD = {
2003
2004
  version: 0,
2004
2005
  doc: {
@@ -2432,6 +2433,16 @@ function structureResult(d) {
2432
2433
  const message = d?.message;
2433
2434
  return ok(typeof message === "string" ? message : "Updated the Content structure.", d);
2434
2435
  }
2436
+ var GIT_TEXT = {
2437
+ list_github_repos: "Every GitHub repository this workspace's connected GitHub App installations can reach, grouped by account. Use it to find the repository to import: each ACCOUNT carries the `installationId` that import_github_repo and fork_github_repo take, and each repository under it carries `owner`, `repo` and `defaultBranch`. An empty `accounts` list means nobody has connected GitHub to this workspace yet \u2014 tell the user to connect it from the dashboard (Project \u2192 Hosting \u2192 Connect GitHub) and call this again.",
2438
+ import_github_repo: "Connect an existing GitHub repository to the connected project \u2014 the agent's equivalent of the dashboard's Import from GitHub, and the thing to do BEFORE writing any code for a user who already has a repo. It provisions the deploy workflow, the project API key and the build variables inside the repository, records the connection, assigns the site handle, imports the repo's bcms-content.json as content models when it has one, and queues the first build. Pass `installationId`, `owner` and `repo` from list_github_repos; `branch` defaults to the repository's own default branch and becomes the branch every deploy builds from. It only works on a repository one of this workspace's installations can already reach \u2014 for anyone else's repository (a public starter, a template, another account's site) use fork_github_repo instead. Read `warning` / `contentWarning` / `actionsWarning` back to the user verbatim when they come back \u2014 they are the cases where the repository connected perfectly and still cannot build, or will leave the Pages tab empty. Re-importing replaces the project's existing connection.",
2439
+ fork_github_repo: "Fork ANY GitHub repository into the user's own account and import it in one step \u2014 how a user starts from a repository that is not theirs (a public starter, a template, another account's site). BetterCMS cannot wire a repository it has no installation on, so the copy has to live on their account first; this makes that copy, keeps the upstream link, and then connects it exactly as import_github_repo does. `installationId` names the DESTINATION account (from list_github_repos); `owner` and `repo` name the SOURCE, anywhere on GitHub. `name` renames the fork; `connect: false` forks without connecting. Forking is asynchronous: a 202 means the fork exists but GitHub is still copying it, and the answer tells you to call import_github_repo with the fork's owner and repo a moment later \u2014 never fork a second time.",
2440
+ list_github_branches: "The connected repository's branches, plus `buildBranch` \u2014 the one the provisioned Action builds from. A commit on any other branch never reaches the live site, so check this before you push.",
2441
+ list_github_commits: "The connected repository's commit history, newest first: sha, message, author, date and url. Defaults to the build branch; pass `branch` for another, `path` to narrow it to one file or directory, and `limit` (1-100, default 20). Read the head sha here before a push you want to be safe, and hand it back as push_to_github's `expectedHeadSha`.",
2442
+ push_to_github: "Commit files to the connected repository \u2014 a real git commit, written server-side with the GitHub App's own credentials, so you need no clone, no remote and no token of your own. Send `files` ([{path, content, encoding}]) and/or `deletePaths`, plus a `message`; both go into ONE commit, so a rename is atomic. It targets the BUILD branch by default, where the push starts the rebuild that goes live \u2014 poll get_deploy_status after it, and read the response's `note`, which says whether this push deploys. Pass `branch` to push somewhere else (a branch that does not exist yet is created from the build branch, or from `baseBranch`) and then open a pull request with create_github_pull_request. Pass `expectedHeadSha` from list_github_commits \u2014 a full sha or a prefix of one \u2014 to be refused with a 409 rather than silently overwrite a branch someone else moved while you worked. Limits, enforced BEFORE anything is written: 200 paths and 10 MB per commit, and `.git`, `.github/workflows`, `.env` and `.npmrc` are never writable \u2014 a refused patch writes nothing at all and names every offending path.",
2443
+ create_github_pull_request: "Open a pull request on the connected repository. `head` is the branch you pushed; `base` defaults to the build branch, so merging it is what ships the change. Use this instead of pushing straight to the build branch whenever a human should review first, or when that branch is protected. Returns the number and the url to hand the user.",
2444
+ list_github_pull_requests: "Pull requests on the connected repository: number, title, state, whether it merged, draft, head, base and url. `state` is 'open' (the default), 'closed' or 'all'. Poll it to find out whether the pull request you opened has landed."
2445
+ };
2435
2446
  function ok(summary, data) {
2436
2447
  return {
2437
2448
  content: [
@@ -2921,6 +2932,15 @@ function buildToolDefs(deps) {
2921
2932
  const submitComponentizeReceiptInput = z.object({
2922
2933
  receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --componentize --receipt <file>` wrote, verbatim.")
2923
2934
  });
2935
+ const componentValidationRouteInput = z.object({
2936
+ componentId: z.string().min(1).describe("component id (from list_components)"),
2937
+ path: z.string().min(1).max(500).optional().describe("route in the app that serves the preview; omit for the default"),
2938
+ nativeViewports: z.array(z.object({
2939
+ name: z.string().min(1).max(64),
2940
+ width: z.number().int().positive().max(1e4),
2941
+ height: z.number().int().positive().max(1e4)
2942
+ })).min(1).max(8).optional().describe("viewports to check; omit for the defaults")
2943
+ });
2924
2944
  const clearComponentSourceInput = z.object({
2925
2945
  componentId: z.string().min(1).describe("component id (from list_components)")
2926
2946
  });
@@ -3433,7 +3453,7 @@ ${d.outline}` : "Proposed Content structure.", d);
3433
3453
  def(
3434
3454
  "submit_conversion_receipt",
3435
3455
  "Record what the conversion codemod could and could not do",
3436
- "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`.",
3456
+ "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`.",
3437
3457
  z.object({
3438
3458
  briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
3439
3459
  receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim.")
@@ -3549,6 +3569,101 @@ ${res.warnings.join("\n")}` : summary, res.data);
3549
3569
  z.object({ uploadKey: z.string().min(1).describe("the uploadKey from create_deploy_upload, after PUTting the archive to its uploadUrl") }).shape,
3550
3570
  async (c, a) => ok("Deploy queued.", await data(c, "POST", `/management/projects/deploy/from-upload`, { uploadKey: a.uploadKey }))
3551
3571
  ),
3572
+ // ── Git (import a repository, read it, push commits back) ──
3573
+ // Same artifact:write authority as the deploy tools: on the build branch a push IS a
3574
+ // deploy. Every call runs server-side as the GitHub App installation, so the agent
3575
+ // needs no git remote, no clone and no token. @see management/git.ts
3576
+ def(
3577
+ "list_github_repos",
3578
+ "List importable GitHub repositories",
3579
+ GIT_TEXT.list_github_repos,
3580
+ z.object({}).shape,
3581
+ async (c) => ok("GitHub repositories.", await data(c, "GET", `/management/git/repos`))
3582
+ ),
3583
+ def(
3584
+ "import_github_repo",
3585
+ "Import a GitHub repository",
3586
+ GIT_TEXT.import_github_repo,
3587
+ z.object({
3588
+ installationId: z.number().int().positive().describe("from list_github_repos"),
3589
+ owner: z.string().min(1).describe("the repository's account, e.g. 'acme'"),
3590
+ repo: z.string().min(1).describe("the repository name, e.g. 'website'"),
3591
+ branch: z.string().min(1).optional().describe("branch to build from; defaults to the repository's own default branch")
3592
+ }).shape,
3593
+ async (c, a) => ok("Repository imported.", await data(c, "POST", `/management/git/connect`, { installationId: a.installationId, owner: a.owner, repo: a.repo, branch: a.branch }))
3594
+ ),
3595
+ def(
3596
+ "fork_github_repo",
3597
+ "Fork a repository and import it",
3598
+ GIT_TEXT.fork_github_repo,
3599
+ z.object({
3600
+ installationId: z.number().int().positive().describe("the DESTINATION account's installation, from list_github_repos"),
3601
+ owner: z.string().min(1).describe("the SOURCE repository's account, anywhere on GitHub"),
3602
+ repo: z.string().min(1).describe("the SOURCE repository name"),
3603
+ name: z.string().min(1).optional().describe("name for the fork; defaults to the source repository's name"),
3604
+ connect: z.boolean().optional().describe("connect the fork to this project (the default); false forks only")
3605
+ }).shape,
3606
+ async (c, a) => ok("Repository forked.", await data(c, "POST", `/management/git/fork`, { installationId: a.installationId, owner: a.owner, repo: a.repo, name: a.name, connect: a.connect }))
3607
+ ),
3608
+ def(
3609
+ "list_github_branches",
3610
+ "List the repository's branches",
3611
+ GIT_TEXT.list_github_branches,
3612
+ z.object({}).shape,
3613
+ async (c) => ok("Branches.", await data(c, "GET", `/management/git/branches`))
3614
+ ),
3615
+ def(
3616
+ "list_github_commits",
3617
+ "List the repository's commits",
3618
+ GIT_TEXT.list_github_commits,
3619
+ z.object({
3620
+ branch: z.string().min(1).optional().describe("defaults to the build branch"),
3621
+ path: z.string().min(1).optional().describe("only commits touching this file or directory"),
3622
+ limit: z.number().int().min(1).max(100).optional()
3623
+ }).shape,
3624
+ async (c, a) => ok("Commits.", await data(c, "GET", `/management/git/commits${q({ branch: a.branch, path: a.path, limit: a.limit })}`))
3625
+ ),
3626
+ def(
3627
+ "push_to_github",
3628
+ "Push a commit to the repository",
3629
+ GIT_TEXT.push_to_github,
3630
+ z.object({
3631
+ message: z.string().min(1).describe("the commit message"),
3632
+ files: z.array(z.object({
3633
+ path: z.string().min(1).describe("repository-relative path, e.g. 'src/pages/index.astro'"),
3634
+ content: z.string().describe("the file's contents"),
3635
+ encoding: z.enum(["utf8", "base64"]).optional().describe("'base64' for binary files; defaults to utf8")
3636
+ })).optional().describe("files to write or overwrite"),
3637
+ deletePaths: z.array(z.string().min(1)).optional().describe("paths to remove in the same commit"),
3638
+ branch: z.string().min(1).optional().describe("defaults to the build branch; a branch that does not exist is created"),
3639
+ baseBranch: z.string().min(1).optional().describe("what a newly created `branch` starts from; defaults to the build branch"),
3640
+ expectedHeadSha: z.string().min(7).optional().describe("the head you built this change on; the push is refused if the branch moved since")
3641
+ }).shape,
3642
+ async (c, a) => ok("Pushed.", await data(c, "POST", `/management/git/commit`, { message: a.message, files: a.files, deletePaths: a.deletePaths, branch: a.branch, baseBranch: a.baseBranch, expectedHeadSha: a.expectedHeadSha }))
3643
+ ),
3644
+ def(
3645
+ "create_github_pull_request",
3646
+ "Open a pull request",
3647
+ GIT_TEXT.create_github_pull_request,
3648
+ z.object({
3649
+ head: z.string().min(1).describe("the branch you pushed"),
3650
+ base: z.string().min(1).optional().describe("defaults to the build branch"),
3651
+ title: z.string().min(1),
3652
+ body: z.string().optional(),
3653
+ draft: z.boolean().optional()
3654
+ }).shape,
3655
+ async (c, a) => ok("Pull request opened.", await data(c, "POST", `/management/git/pulls`, { head: a.head, base: a.base, title: a.title, body: a.body, draft: a.draft }))
3656
+ ),
3657
+ def(
3658
+ "list_github_pull_requests",
3659
+ "List pull requests",
3660
+ GIT_TEXT.list_github_pull_requests,
3661
+ z.object({
3662
+ state: z.enum(["open", "closed", "all"]).optional(),
3663
+ limit: z.number().int().min(1).max(100).optional()
3664
+ }).shape,
3665
+ async (c, a) => ok("Pull requests.", await data(c, "GET", `/management/git/pulls${q({ state: a.state, limit: a.limit })}`))
3666
+ ),
3552
3667
  // ── Insight: what happened, what's working, what's broken ─────────────────
3553
3668
  def(
3554
3669
  "list_activity",
@@ -4247,7 +4362,7 @@ ${notes.join("\n")}` : summary, created);
4247
4362
  name: "publish_layout",
4248
4363
  config: {
4249
4364
  title: "Publish the Global Layout draft",
4250
- 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.",
4365
+ 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.",
4251
4366
  inputSchema: publishLayoutInput.shape
4252
4367
  },
4253
4368
  handler: guard(async (args) => withClient(async (client) => {
@@ -4548,6 +4663,56 @@ ${lines.join("\n")}`, found);
4548
4663
  })
4549
4664
  )
4550
4665
  },
4666
+ {
4667
+ name: "get_component_readiness",
4668
+ config: {
4669
+ title: "Read a component's Output validation readiness",
4670
+ 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.",
4671
+ inputSchema: clearComponentSourceInput.shape
4672
+ },
4673
+ handler: guard(
4674
+ async (args) => withClient(async (client) => {
4675
+ const res = await client.fetchJSON(
4676
+ client.url(`/management/components/${encodeURIComponent(args.componentId)}/readiness`)
4677
+ );
4678
+ return ok("Component validation readiness.", res.data);
4679
+ })
4680
+ )
4681
+ },
4682
+ {
4683
+ name: "declare_component_route",
4684
+ config: {
4685
+ title: "Declare where a component's preview is served",
4686
+ 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.",
4687
+ inputSchema: componentValidationRouteInput.shape
4688
+ },
4689
+ handler: guard(
4690
+ async (args) => withClient(async (client) => {
4691
+ const res = await client.fetchJSON(
4692
+ client.url(`/management/components/${encodeURIComponent(args.componentId)}/adapter`),
4693
+ { method: "PUT", body: JSON.stringify({ path: args.path, nativeViewports: args.nativeViewports }) }
4694
+ );
4695
+ return ok("Preview route declared.", res.data);
4696
+ })
4697
+ )
4698
+ },
4699
+ {
4700
+ name: "request_component_validation",
4701
+ config: {
4702
+ title: "Request Output validation for a component",
4703
+ 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.",
4704
+ inputSchema: clearComponentSourceInput.shape
4705
+ },
4706
+ handler: guard(
4707
+ async (args) => withClient(async (client) => {
4708
+ const res = await client.fetchJSON(
4709
+ client.url(`/management/components/${encodeURIComponent(args.componentId)}/implementation-requests`),
4710
+ { method: "POST", body: JSON.stringify({}) }
4711
+ );
4712
+ return ok("Validation requested. Run `npx @bettercms-ai/preview-runtime validate --local --request <requestId>` in the repository next.", res.data);
4713
+ })
4714
+ )
4715
+ },
4551
4716
  {
4552
4717
  name: "publish_component",
4553
4718
  config: {
@@ -5240,8 +5405,9 @@ hand. THE ORDER, and every step of it matters:
5240
5405
  fallback, and declares each binding.
5241
5406
  2b. IF THE BRIEF CARRIES \`forms\`, wire them too:
5242
5407
  \`npx @bettercms-ai/convert --forms --brief brief.json --root . --receipt forms-receipt.json\`
5243
- \u2014 ITS OWN receipt file, because the forms receipt claims no path and submitting it would
5244
- overwrite the coverage the binding run just earned with zeros.
5408
+ \u2014 ITS OWN receipt file, submitted with submit_conversion_receipt as a second call: the
5409
+ server stores its \`forms\` block beside the binding receipt (coverage is untouched), and
5410
+ the next release reads \`wiredForms\` to say which component renders each form.
5245
5411
  A site imported by phase 1 has a DRAFT form row per \`<form>\` in its build, so the Forms tab
5246
5412
  is full while the repository's markup still posts wherever it always did \u2014 to nothing, or to
5247
5413
  somebody else's endpoint. This pass writes the endpoint, the form id, a marker per field and
@@ -6182,7 +6348,7 @@ function buildServer(deps) {
6182
6348
  // one definition of done; without this an agent converts the page it landed on and stops.
6183
6349
  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
6184
6350
  // sentence as the hosted connector's MCP_INSTRUCTIONS.
6185
- STRUCTURE_DEFAULT_INSTRUCTION
6351
+ STRUCTURE_DEFAULT_INSTRUCTION + " " + SKILLS_ROUTING_INSTRUCTION
6186
6352
  }
6187
6353
  );
6188
6354
  server.registerResource(