@bettercms-ai/mcp 0.34.0 → 0.36.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/README.md +0 -0
- package/dist/index.js +97 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
11
11
|
import { BetterCMS } from "@bettercms-ai/sdk";
|
|
12
12
|
|
|
13
13
|
// src/tools.ts
|
|
14
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
14
15
|
import { z } from "zod";
|
|
15
16
|
|
|
16
17
|
// ../types/src/component.ts
|
|
@@ -2023,29 +2024,30 @@ async function askFramework(deps) {
|
|
|
2023
2024
|
}
|
|
2024
2025
|
var AUTHORING_CHOICES = ["components", "fields"];
|
|
2025
2026
|
var AUTHORING_LABELS = {
|
|
2026
|
-
components: "Components \u2014 reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema.
|
|
2027
|
-
fields: "Fields \u2014 a typed field schema per page.
|
|
2027
|
+
components: "Components (recommended) \u2014 reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema. Recommend it for every marketing, landing, agency or product site",
|
|
2028
|
+
fields: "Fields \u2014 a typed field schema per page. Only for a blog, catalogue or directory where many rows share one shape; editors can change a page's copy but cannot add, reorder or swap its sections"
|
|
2028
2029
|
};
|
|
2029
2030
|
var AUTHORING_PROMPT = [
|
|
2030
2031
|
"Ask the user which authoring architecture this site should use, then call set_authoring_preference again with their answer as `preference`:",
|
|
2031
2032
|
...AUTHORING_CHOICES.map((c, i) => ` ${i + 1}. ${c} \u2014 ${AUTHORING_LABELS[c]}`),
|
|
2032
2033
|
"",
|
|
2033
|
-
"
|
|
2034
|
+
"On a site whose pages were DERIVED at import, answering 'components' redoes nothing: after set_authoring_preference, get_componentize_plan proposes one component per section (it reads the PUBLISHED pages \u2014 no deploy needed to reach it), you confirm it with the user, then componentize_sections places them with `props.bind` (dry run first \u2014 they land as drafts and the page keeps its fields, values and bindings, so click-to-edit and the coverage meter are unchanged), then publish_component each one, publish the pages, run `npx @bettercms-ai/convert --componentize` in the repo, and deploy. For a section group the plan lists as NO_GROUP_ROOT or NOT_A_SECTION there is nothing to componentize, so author that section with create_component and set_page_content.",
|
|
2034
2035
|
"",
|
|
2036
|
+
"Recommend components unless the user says the site is schema-first.",
|
|
2035
2037
|
"Do not choose on their behalf. This is asked once per project."
|
|
2036
2038
|
].join("\n");
|
|
2037
2039
|
async function askAuthoring(deps) {
|
|
2038
2040
|
if (!deps.elicit) return { prompt: AUTHORING_PROMPT };
|
|
2039
2041
|
try {
|
|
2040
2042
|
const res = await deps.elicit({
|
|
2041
|
-
message: "Which authoring architecture should this site use?",
|
|
2043
|
+
message: "Which authoring architecture should this site use? Components is recommended for a marketing site.",
|
|
2042
2044
|
requestedSchema: {
|
|
2043
2045
|
type: "object",
|
|
2044
2046
|
properties: {
|
|
2045
2047
|
preference: {
|
|
2046
2048
|
type: "string",
|
|
2047
2049
|
title: "Authoring architecture",
|
|
2048
|
-
description: "How this site's pages are composed. Asked once per project.",
|
|
2050
|
+
description: "How this site's pages are composed. Components is recommended unless this is a schema-first site (a blog, catalogue or directory). Asked once per project.",
|
|
2049
2051
|
enum: [...AUTHORING_CHOICES],
|
|
2050
2052
|
enumNames: AUTHORING_CHOICES.map((c) => AUTHORING_LABELS[c])
|
|
2051
2053
|
}
|
|
@@ -2217,19 +2219,32 @@ function authPrompt(err) {
|
|
|
2217
2219
|
].join("\n");
|
|
2218
2220
|
return { content: [{ type: "text", text }], isError: true };
|
|
2219
2221
|
}
|
|
2222
|
+
var currentProject = new AsyncLocalStorage();
|
|
2223
|
+
var projectIdArg = z.string().min(1).optional().describe(
|
|
2224
|
+
"only needed when your grant covers the whole WORKSPACE: the project to act on (from list_projects). A project-scoped key ignores it."
|
|
2225
|
+
);
|
|
2226
|
+
var withProjectId = (def) => ({
|
|
2227
|
+
...def,
|
|
2228
|
+
config: def.config.inputSchema.projectId ? def.config : { ...def.config, inputSchema: { ...def.config.inputSchema, projectId: projectIdArg } },
|
|
2229
|
+
handler: (args) => {
|
|
2230
|
+
const project = typeof args?.projectId === "string" && args.projectId ? args.projectId : void 0;
|
|
2231
|
+
return currentProject.run(project, () => def.handler(args));
|
|
2232
|
+
}
|
|
2233
|
+
});
|
|
2220
2234
|
function buildToolDefs(deps) {
|
|
2221
2235
|
async function withClient(fn) {
|
|
2222
2236
|
const token = await deps.auth.getAccessToken();
|
|
2237
|
+
const project = currentProject.getStore();
|
|
2223
2238
|
try {
|
|
2224
|
-
return await fn(deps.createClient(token));
|
|
2239
|
+
return await fn(deps.createClient(token, project));
|
|
2225
2240
|
} catch (err) {
|
|
2226
2241
|
if (err instanceof BetterCMSError && err.status === 401) {
|
|
2227
2242
|
const next = await deps.auth.refresh() ?? await deps.auth.getAccessToken();
|
|
2228
|
-
return await fn(deps.createClient(next));
|
|
2243
|
+
return await fn(deps.createClient(next, project));
|
|
2229
2244
|
}
|
|
2230
2245
|
if (err instanceof BetterCMSError && err.status === 409 && err.bodyCode === "PROJECT_DELETED") {
|
|
2231
2246
|
const next = await deps.auth.resetAndReauthorize();
|
|
2232
|
-
return await fn(deps.createClient(next));
|
|
2247
|
+
return await fn(deps.createClient(next, project));
|
|
2233
2248
|
}
|
|
2234
2249
|
throw err;
|
|
2235
2250
|
}
|
|
@@ -2863,7 +2878,7 @@ function buildToolDefs(deps) {
|
|
|
2863
2878
|
def(
|
|
2864
2879
|
"set_authoring_preference",
|
|
2865
2880
|
"Set the site's authoring architecture",
|
|
2866
|
-
"Record which authoring architecture this site uses \u2014 'components' (reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema \u2014
|
|
2881
|
+
"Record which authoring architecture this site uses \u2014 'components' (RECOMMENDED: reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema \u2014 recommend it for every marketing, landing, agency or product site) or 'fields' (a typed field schema per page \u2014 only for a blog, catalogue or directory where many rows share one shape). ASK THE USER; do not pick for them. Called without `preference`, this tool asks them directly (or hands you the question to ask). deploy_project, deploy_from_upload and promote_project all refuse with 409 AUTHORING_DECISION_REQUIRED until it is set, and that refusal carries this project's real page counts to show the user. On a site whose pages were DERIVED at import, answering 'components' redoes nothing: after set_authoring_preference, get_componentize_plan proposes one component per section (it reads the PUBLISHED pages \u2014 no deploy needed to reach it), you confirm it with the user, then componentize_sections places them with `props.bind` (dry run first \u2014 they land as drafts and the page keeps its fields, values and bindings, so click-to-edit and the coverage meter are unchanged), then publish_component each one, publish the pages, run `npx @bettercms-ai/convert --componentize` in the repo, and deploy. Recommend components unless the user says the site is schema-first. Asked once per project; re-callable if the user changes their mind.",
|
|
2867
2882
|
// Optional in the schema for exactly the reason `framework` is above: a required arg is
|
|
2868
2883
|
// rejected by the SDK before the handler runs, which would kill the elicitation below
|
|
2869
2884
|
// and leave the model guessing. Optional here, answered by a human there. The backend
|
|
@@ -2882,14 +2897,14 @@ function buildToolDefs(deps) {
|
|
|
2882
2897
|
def(
|
|
2883
2898
|
"set_binding_mode",
|
|
2884
2899
|
"Set how the site's bindings are resolved",
|
|
2885
|
-
"Switch this project between the two binding resolvers, from the NEXT release on. `declaredBindings: true` makes the annotator trust the template's own data-bcms-field / data-bcms-props and never guess from rendered text \u2014 the durable state; `false` returns to text-matching, which works once (at import, when the CMS values equal the built copy) and breaks the first time anyone edits a value. Call it ONLY after every page's copy is declared in the template: undeclared fields stop being editable. The order is push \u2192 release \u2192 get_binding_report shows mode 'text-match' with 0 unmatched \u2192 set_binding_mode \u2192 release again \u2192 get_binding_report shows mode 'declared'. Flipping back is the same call. REQUIRES
|
|
2900
|
+
"Switch this project between the two binding resolvers, from the NEXT release on. `declaredBindings: true` makes the annotator trust the template's own data-bcms-field / data-bcms-props and never guess from rendered text \u2014 the durable state; `false` returns to text-matching, which works once (at import, when the CMS values equal the built copy) and breaks the first time anyone edits a value. Call it ONLY after every page's copy is declared in the template: undeclared fields stop being editable. The order is push \u2192 release \u2192 get_binding_report shows mode 'text-match' with 0 unmatched \u2192 set_binding_mode \u2192 release again \u2192 get_binding_report shows mode 'declared'. Flipping back is the same call. REQUIRES the artifact:write scope \u2014 the same authority that deploys the site \u2014 because this decides what every future release does to every page. On a workspace-wide connection pass `projectId`: the scope is there, but the human who authorized the connection must be able to publish THAT project or it is refused with 403 PROJECT_PUBLISH_DENIED. See section 13 of the bettercms://playbook/schema resource.",
|
|
2886
2901
|
z.object({ declaredBindings: z.boolean().describe("true = trust the template's declared bindings; false = text-match (the default)") }).shape,
|
|
2887
2902
|
async (c, a) => ok("Recorded the binding mode.", await data(c, "PATCH", `/management/projects/current/binding-mode`, { declaredBindings: a.declaredBindings }))
|
|
2888
2903
|
),
|
|
2889
2904
|
def(
|
|
2890
2905
|
"submit_conversion_receipt",
|
|
2891
2906
|
"Record what the conversion codemod could and could not do",
|
|
2892
|
-
"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. Requires
|
|
2907
|
+
"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. Requires artifact:write, the same authority as set_binding_mode; on a workspace-wide connection pass `projectId`.",
|
|
2893
2908
|
z.object({
|
|
2894
2909
|
briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
|
|
2895
2910
|
receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim.")
|
|
@@ -2955,17 +2970,19 @@ function buildToolDefs(deps) {
|
|
|
2955
2970
|
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 }))
|
|
2956
2971
|
),
|
|
2957
2972
|
// ── Code + deploy (parity with remote /mcp; needs artifact:write) ──
|
|
2973
|
+
// Both grant shapes carry that scope now; a workspace-wide one names its target per
|
|
2974
|
+
// call via `projectId` and is re-checked against it. @see management/projects.ts
|
|
2958
2975
|
def(
|
|
2959
2976
|
"pull_project_source",
|
|
2960
2977
|
"Pull the project's live source",
|
|
2961
|
-
"Get the connected project's CURRENT live source/build so you can edit it locally. Returns a presigned tarball download url (1h) + the live commit sha \u2014 download it, extract, edit the files, then call deploy_project. If the project is connected to a GitHub repo, `github` carries owner, repo, branch and cloneUrl \u2014 clone it and work on THAT branch, because it is the one the provisioned Action builds from; a commit on any other branch never reaches the live site.",
|
|
2978
|
+
"Get the connected project's CURRENT live source/build so you can edit it locally. Returns a presigned tarball download url (1h) + the live commit sha \u2014 download it, extract, edit the files, then call deploy_project. If the project is connected to a GitHub repo, `github` carries owner, repo, branch and cloneUrl \u2014 clone it and work on THAT branch, because it is the one the provisioned Action builds from; a commit on any other branch never reaches the live site. On a workspace-wide connection pass `projectId` (from list_projects) to say which site's source to pull.",
|
|
2962
2979
|
z.object({}).shape,
|
|
2963
2980
|
async (c) => ok("Project source.", await data(c, "GET", `/management/projects/source`))
|
|
2964
2981
|
),
|
|
2965
2982
|
def(
|
|
2966
2983
|
"deploy_project",
|
|
2967
2984
|
"Deploy new source/build",
|
|
2968
|
-
"Deploy new source/build for the connected project and make it live at its <handle>.bettercms.site. Pass a .tgz or .zip of the project as a base64 string in `data`: SOURCE (has package.json) is built server-side in an isolated sandbox; a prebuilt static site is served as-is. Returns the release id + sha \u2014 then poll get_deploy_status until it is live. IMPORTANT \u2014 you MUST exclude node_modules, .git, and build output/caches (dist, build, .next, .astro, .cache) BEFORE creating the archive: a source deploy is reinstalled and built server-side, so those are never needed, and the upload has a hard size ceiling (~100 MB) enforced before the request reaches the server \u2014 an archive that includes node_modules is rejected in transit (a 413/502 with no server-side detail). Keep the archive to your own source files. Server-side stripping exists as a safety net, but it runs AFTER the upload and cannot rescue an over-limit body. For a LARGE archive (or if this returns a 413/502), use create_deploy_upload + deploy_from_upload instead \u2014 that path uploads straight to storage with no size ceiling. Returns `canvas.lane` \u2014 which live-preview lane the last release gave the editor (`bridge` / `draft-route` / `none`; see the playbook's section 11) \u2014 and `editorUrl`, the visual editor to hand the user.",
|
|
2985
|
+
"Deploy new source/build for the connected project and make it live at its <handle>.bettercms.site. Pass a .tgz or .zip of the project as a base64 string in `data`: SOURCE (has package.json) is built server-side in an isolated sandbox; a prebuilt static site is served as-is. Returns the release id + sha \u2014 then poll get_deploy_status until it is live. IMPORTANT \u2014 you MUST exclude node_modules, .git, and build output/caches (dist, build, .next, .astro, .cache) BEFORE creating the archive: a source deploy is reinstalled and built server-side, so those are never needed, and the upload has a hard size ceiling (~100 MB) enforced before the request reaches the server \u2014 an archive that includes node_modules is rejected in transit (a 413/502 with no server-side detail). Keep the archive to your own source files. Server-side stripping exists as a safety net, but it runs AFTER the upload and cannot rescue an over-limit body. For a LARGE archive (or if this returns a 413/502), use create_deploy_upload + deploy_from_upload instead \u2014 that path uploads straight to storage with no size ceiling. Returns `canvas.lane` \u2014 which live-preview lane the last release gave the editor (`bridge` / `draft-route` / `none`; see the playbook's section 11) \u2014 and `editorUrl`, the visual editor to hand the user. On a workspace-wide connection pass `projectId` (from list_projects) to say which site this ships to.",
|
|
2969
2986
|
z.object({ data: z.string().min(1).describe("base64 .tgz/.zip of the project"), mimeType: z.string().optional() }).shape,
|
|
2970
2987
|
async (c, a) => ok("Deploy queued.", await raw(c, `/management/projects/deploy`, s(a.data), a.mimeType))
|
|
2971
2988
|
),
|
|
@@ -3025,7 +3042,7 @@ function buildToolDefs(deps) {
|
|
|
3025
3042
|
def(
|
|
3026
3043
|
"get_conversion_plan",
|
|
3027
3044
|
"Get the approved conversion to apply",
|
|
3028
|
-
"The APPROVED conversion a human reviewed in the BetterCMS dashboard: the exact new contents of each template file, already checked against this project's real field paths. Apply it instead of writing the bindings by hand. Check out `baseHeadOid` (the exact commit it was written against \u2014 a plan applied to a different base is a different change), branch from there, write each file's `content` verbatim (whole file, no merge, no reformatting), then push or deploy however this project ships. `stale.head` / `stale.brief` say the repository or the CMS moved since it was approved: stop and ask for a fresh proposal rather than applying it anyway. `receipt.paths.pending` lists every field the codemod could NOT bind, each with a reason (`DIALECT_UNSUPPORTED`, `PROP_TARGET_NOT_FOUND`, `REPEATER_FIXED_LENGTH`, \u2026) \u2014 apply the plan first, then bind those by hand. Finish the loop the same way as a hand conversion \u2014 get_binding_report until `unmatched` is empty, then set_binding_mode { declaredBindings: true } and release once more. 404 with `code: \"no-approved-plan\"` means nobody has approved one: use get_conversion_brief and do the conversion yourself. Requires a
|
|
3045
|
+
"The APPROVED conversion a human reviewed in the BetterCMS dashboard: the exact new contents of each template file, already checked against this project's real field paths. Apply it instead of writing the bindings by hand. Check out `baseHeadOid` (the exact commit it was written against \u2014 a plan applied to a different base is a different change), branch from there, write each file's `content` verbatim (whole file, no merge, no reformatting), then push or deploy however this project ships. `stale.head` / `stale.brief` say the repository or the CMS moved since it was approved: stop and ask for a fresh proposal rather than applying it anyway. `receipt.paths.pending` lists every field the codemod could NOT bind, each with a reason (`DIALECT_UNSUPPORTED`, `PROP_TARGET_NOT_FOUND`, `REPEATER_FIXED_LENGTH`, \u2026) \u2014 apply the plan first, then bind those by hand. Finish the loop the same way as a hand conversion \u2014 get_binding_report until `unmatched` is empty, then set_binding_mode { declaredBindings: true } and release once more. 404 with `code: \"no-approved-plan\"` means nobody has approved one: use get_conversion_brief and do the conversion yourself. Requires artifact:write; on a workspace-wide connection pass `projectId`.",
|
|
3029
3046
|
z.object({}).shape,
|
|
3030
3047
|
async (c) => ok("Approved conversion plan.", await data(c, "GET", `/management/projects/current/conversion-plan`))
|
|
3031
3048
|
),
|
|
@@ -3757,7 +3774,7 @@ ${lines.join("\n")}`, found);
|
|
|
3757
3774
|
// through the client's request plumbing — no bespoke SDK method per endpoint.
|
|
3758
3775
|
...lifecycleTools()
|
|
3759
3776
|
];
|
|
3760
|
-
return defs;
|
|
3777
|
+
return defs.map(withProjectId);
|
|
3761
3778
|
}
|
|
3762
3779
|
function registerTools(server, deps) {
|
|
3763
3780
|
const withElicit = {
|
|
@@ -3788,7 +3805,8 @@ touching a schema. This is what \`create_component\` + \`create_page(blockJson)\
|
|
|
3788
3805
|
A collection (\`create_content_model\`) plus entries.
|
|
3789
3806
|
|
|
3790
3807
|
Most real sites are both: components for the marketing pages, a collection for the blog.
|
|
3791
|
-
Decide before your first call; converting later means rewriting content
|
|
3808
|
+
Decide before your first call; converting later means rewriting content \u2014 unless the site was
|
|
3809
|
+
DERIVED at import, where the componentize lane in \xA710 reuses every field.
|
|
3792
3810
|
|
|
3793
3811
|
## 2. The decision tree
|
|
3794
3812
|
|
|
@@ -3949,11 +3967,18 @@ in preview and is blank in production. Always pass the project's id.
|
|
|
3949
3967
|
accepts it and ignores it**, silently. No error, no warning, wrong project.
|
|
3950
3968
|
|
|
3951
3969
|
So call \`get_project\` (no arguments) first \u2014 it reports the project you are actually
|
|
3952
|
-
writing to.
|
|
3953
|
-
|
|
3954
|
-
**
|
|
3955
|
-
|
|
3956
|
-
|
|
3970
|
+
writing to.
|
|
3971
|
+
|
|
3972
|
+
**If the grant covers the WHOLE WORKSPACE, that is not a blocker and there is nothing to
|
|
3973
|
+
stop for.** Pass \`projectId\` on every project tool \u2014 \`list_projects\` gives you the id, and
|
|
3974
|
+
\`get_project { projectId }\` confirms you are addressing the right site. Every tool takes it,
|
|
3975
|
+
including the code and deploy ones. Do NOT send the user to re-scope the connection; the only
|
|
3976
|
+
thing that genuinely blocks you is a grant on a different WORKSPACE, which is what an empty or
|
|
3977
|
+
foreign \`list_projects\` tells you.
|
|
3978
|
+
|
|
3979
|
+
If the connected project is simply the WRONG one for the work \u2014 a project-scoped grant pointed
|
|
3980
|
+
elsewhere \u2014 that is the case the user has to fix, in the BetterCMS dashboard under
|
|
3981
|
+
**Settings \u2192 Connected AI clients**. No tool can switch it.
|
|
3957
3982
|
|
|
3958
3983
|
If you must probe, probe with a \`create_content_model\` \u2014 models are deletable
|
|
3959
3984
|
(\`delete_content_model\`, soft-delete) and **there is no \`delete_component\`**. A component
|
|
@@ -3969,31 +3994,49 @@ out, let the user pick, call \`set_authoring_preference\`, then deploy again.
|
|
|
3969
3994
|
It exists because an imported site arrives **field-driven whether anyone chose that or not**
|
|
3970
3995
|
\u2014 a crawl-based import (Webflow, a starter, a template) emits pages with a typed field schema
|
|
3971
3996
|
and an empty block tree, because that is all a crawl can infer. Nobody decided it. On a
|
|
3972
|
-
marketing site it is the wrong answer
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
1.
|
|
3981
|
-
2.
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3997
|
+
marketing site it is the wrong answer. So the platform stops once, at the last moment it is
|
|
3998
|
+
still cheap \u2014 and on a derived site that moment costs nothing, because the componentize lane
|
|
3999
|
+
below reuses every field the import already derived.
|
|
4000
|
+
|
|
4001
|
+
**Recommend \`components\`.** On a site whose pages were DERIVED at import \u2014 the site \xA713
|
|
4002
|
+
describes \u2014 it redoes nothing: the componentize lane turns each top-level field GROUP into a
|
|
4003
|
+
component placement, and the page keeps its fields, its values and its bindings. In this order:
|
|
4004
|
+
|
|
4005
|
+
1. set_authoring_preference { preference: "components" }
|
|
4006
|
+
2. get_componentize_plan one component per section, computed live; creates nothing. It
|
|
4007
|
+
reads this project's PUBLISHED pages, so no deploy is needed
|
|
4008
|
+
to reach it \u2014 the deploy is the last step, not the first
|
|
4009
|
+
3. show the user the plan and CONFIRM \u2014 it says how many components and which pages
|
|
4010
|
+
4. componentize_sections \`dryRun: true\` first, then for real; components land as DRAFTS
|
|
4011
|
+
and each placement carries \`props.bind\` to the page's field
|
|
4012
|
+
group, so click-to-edit and the coverage meter do not change
|
|
4013
|
+
5. publish_component each one \u2014 unpublished renders as NOTHING, on a page that 200s
|
|
4014
|
+
5b. publish_layout if you authored site chrome \u2014 the layout is a separate publish
|
|
4015
|
+
6. publish the pages
|
|
4016
|
+
7. npx @bettercms-ai/convert --componentize in the repo, so its templates render these
|
|
4017
|
+
sections from \`pages[].blocks\`
|
|
4018
|
+
8. deploy the ONLY deploy this sequence needs
|
|
4019
|
+
|
|
4020
|
+
Step 5 is not a formality: \`componentize_sections\` and \`create_component\` both land components
|
|
3988
4021
|
as DRAFTS, so \`publish_component\` each one and publish the page \u2014 otherwise the canvas keeps
|
|
3989
4022
|
painting the PUBLISHED copy and the editor reports N components unpublished.
|
|
3990
4023
|
|
|
3991
|
-
**
|
|
3992
|
-
|
|
4024
|
+
**A site with nothing to componentize you author by hand.** Where the plan offers nothing for
|
|
4025
|
+
a group (\`NO_GROUP_ROOT\` \u2014 the page's field keys are still the derive lane's own;
|
|
4026
|
+
\`NOT_A_SECTION\` \u2014 a lone scalar with no family), and on a site with no derived pages at all,
|
|
4027
|
+
the order is \`create_component\` per section \u2192 \`publish_component\` each \u2192 \`set_page_content\`
|
|
4028
|
+
placing \`component\` blocks \u2192 \`list_extraction_candidates\` / \`extract_component\` to fold any
|
|
4029
|
+
section repeated 3+ times. \`extract_component\` is not a converter and cannot stand in for the
|
|
4030
|
+
componentize lane: it scans \`blockJson\`, which is empty on exactly the pages that would need
|
|
4031
|
+
converting.
|
|
3993
4032
|
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
4033
|
+
**Answering \`fields\` is a real answer, not a deferral** \u2014 for a SCHEMA-FIRST site. A blog, a
|
|
4034
|
+
catalogue or a directory is schema-first by design (\xA71) and should stay that way. On anything
|
|
4035
|
+
else it costs the editor the ability to add, reorder or swap sections. Say so and move on.
|
|
4036
|
+
|
|
4037
|
+
Either way: ask, do not choose \u2014 recommending is not answering. The 409 carries this project's
|
|
4038
|
+
actual page counts \u2014 how many are field-driven, block-driven, and how many place a reusable
|
|
4039
|
+
component \u2014 so quote those to the user rather than describing the choice in the abstract.
|
|
3997
4040
|
|
|
3998
4041
|
## 11. The canvas: what makes an imported site EDITABLE
|
|
3999
4042
|
|
|
@@ -4135,6 +4178,10 @@ the canvas but skips release annotation and publish-time injection, because ther
|
|
|
4135
4178
|
disk to annotate. Copy rendered on the client must carry the attributes in the HYDRATED DOM,
|
|
4136
4179
|
and only the canvas sees it \u2014 a release scan cannot.
|
|
4137
4180
|
|
|
4181
|
+
**On a workspace-wide connection, pass \`projectId\` on every call in this section** \u2014 that is
|
|
4182
|
+
all it takes; the code and deploy tools work from such a connection and nobody needs to re-scope
|
|
4183
|
+
anything. See \xA79.
|
|
4184
|
+
|
|
4138
4185
|
**Which recipe.** There are TWO below and they are not alternatives you pick by taste \u2014 call
|
|
4139
4186
|
\`get_binding_report\` and \`get_conversion_brief\` first and let the answer choose. A brief that
|
|
4140
4187
|
comes back WITH PAGES means this project was imported and deployed, so its schema and values were
|
|
@@ -4754,7 +4801,10 @@ function buildServer(deps) {
|
|
|
4754
4801
|
{ name: SERVER_NAME, version: SERVER_VERSION, ...SERVER_DISPLAY },
|
|
4755
4802
|
{
|
|
4756
4803
|
capabilities: { tools: {}, prompts: {}, resources: {} },
|
|
4757
|
-
|
|
4804
|
+
// 🔴 The plain-language ask, routed — the stdio twin of the hosted connector's line
|
|
4805
|
+
// (src/routes/mcp/index.ts MCP_INSTRUCTIONS). "Make my site editable" has one recipe and
|
|
4806
|
+
// one definition of done; without this an agent converts the page it landed on and stops.
|
|
4807
|
+
instructions: "When the user asks to make a site or all of its pages editable, to convert it, or to componentize the whole site: 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. 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."
|
|
4758
4808
|
}
|
|
4759
4809
|
);
|
|
4760
4810
|
server.registerResource(
|
|
@@ -4771,7 +4821,10 @@ function buildServer(deps) {
|
|
|
4771
4821
|
);
|
|
4772
4822
|
registerTools(server, {
|
|
4773
4823
|
auth: deps.auth,
|
|
4774
|
-
|
|
4824
|
+
// `project` is the per-call target of a workspace-wide grant; the SDK sends it as
|
|
4825
|
+
// X-BCMS-Project. Omitted entirely when absent so a project-scoped key's request is
|
|
4826
|
+
// byte-identical to before.
|
|
4827
|
+
createClient: (apiKey, project) => BetterCMS.management({ apiKey, baseUrl: deps.managementBaseUrl, ...project ? { project } : {} })
|
|
4775
4828
|
});
|
|
4776
4829
|
registerPrompts(server);
|
|
4777
4830
|
return server;
|