@bettercms-ai/mcp 0.33.0 → 0.35.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 +79 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
@@ -2217,19 +2218,32 @@ function authPrompt(err) {
|
|
|
2217
2218
|
].join("\n");
|
|
2218
2219
|
return { content: [{ type: "text", text }], isError: true };
|
|
2219
2220
|
}
|
|
2221
|
+
var currentProject = new AsyncLocalStorage();
|
|
2222
|
+
var projectIdArg = z.string().min(1).optional().describe(
|
|
2223
|
+
"only needed when your grant covers the whole WORKSPACE: the project to act on (from list_projects). A project-scoped key ignores it."
|
|
2224
|
+
);
|
|
2225
|
+
var withProjectId = (def) => ({
|
|
2226
|
+
...def,
|
|
2227
|
+
config: def.config.inputSchema.projectId ? def.config : { ...def.config, inputSchema: { ...def.config.inputSchema, projectId: projectIdArg } },
|
|
2228
|
+
handler: (args) => {
|
|
2229
|
+
const project = typeof args?.projectId === "string" && args.projectId ? args.projectId : void 0;
|
|
2230
|
+
return currentProject.run(project, () => def.handler(args));
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2220
2233
|
function buildToolDefs(deps) {
|
|
2221
2234
|
async function withClient(fn) {
|
|
2222
2235
|
const token = await deps.auth.getAccessToken();
|
|
2236
|
+
const project = currentProject.getStore();
|
|
2223
2237
|
try {
|
|
2224
|
-
return await fn(deps.createClient(token));
|
|
2238
|
+
return await fn(deps.createClient(token, project));
|
|
2225
2239
|
} catch (err) {
|
|
2226
2240
|
if (err instanceof BetterCMSError && err.status === 401) {
|
|
2227
2241
|
const next = await deps.auth.refresh() ?? await deps.auth.getAccessToken();
|
|
2228
|
-
return await fn(deps.createClient(next));
|
|
2242
|
+
return await fn(deps.createClient(next, project));
|
|
2229
2243
|
}
|
|
2230
2244
|
if (err instanceof BetterCMSError && err.status === 409 && err.bodyCode === "PROJECT_DELETED") {
|
|
2231
2245
|
const next = await deps.auth.resetAndReauthorize();
|
|
2232
|
-
return await fn(deps.createClient(next));
|
|
2246
|
+
return await fn(deps.createClient(next, project));
|
|
2233
2247
|
}
|
|
2234
2248
|
throw err;
|
|
2235
2249
|
}
|
|
@@ -2882,14 +2896,14 @@ function buildToolDefs(deps) {
|
|
|
2882
2896
|
def(
|
|
2883
2897
|
"set_binding_mode",
|
|
2884
2898
|
"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
|
|
2899
|
+
"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
2900
|
z.object({ declaredBindings: z.boolean().describe("true = trust the template's declared bindings; false = text-match (the default)") }).shape,
|
|
2887
2901
|
async (c, a) => ok("Recorded the binding mode.", await data(c, "PATCH", `/management/projects/current/binding-mode`, { declaredBindings: a.declaredBindings }))
|
|
2888
2902
|
),
|
|
2889
2903
|
def(
|
|
2890
2904
|
"submit_conversion_receipt",
|
|
2891
2905
|
"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
|
|
2906
|
+
"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
2907
|
z.object({
|
|
2894
2908
|
briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
|
|
2895
2909
|
receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim.")
|
|
@@ -2955,17 +2969,19 @@ function buildToolDefs(deps) {
|
|
|
2955
2969
|
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
2970
|
),
|
|
2957
2971
|
// ── Code + deploy (parity with remote /mcp; needs artifact:write) ──
|
|
2972
|
+
// Both grant shapes carry that scope now; a workspace-wide one names its target per
|
|
2973
|
+
// call via `projectId` and is re-checked against it. @see management/projects.ts
|
|
2958
2974
|
def(
|
|
2959
2975
|
"pull_project_source",
|
|
2960
2976
|
"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,
|
|
2977
|
+
"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
2978
|
z.object({}).shape,
|
|
2963
2979
|
async (c) => ok("Project source.", await data(c, "GET", `/management/projects/source`))
|
|
2964
2980
|
),
|
|
2965
2981
|
def(
|
|
2966
2982
|
"deploy_project",
|
|
2967
2983
|
"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.",
|
|
2984
|
+
"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
2985
|
z.object({ data: z.string().min(1).describe("base64 .tgz/.zip of the project"), mimeType: z.string().optional() }).shape,
|
|
2970
2986
|
async (c, a) => ok("Deploy queued.", await raw(c, `/management/projects/deploy`, s(a.data), a.mimeType))
|
|
2971
2987
|
),
|
|
@@ -3025,7 +3041,7 @@ function buildToolDefs(deps) {
|
|
|
3025
3041
|
def(
|
|
3026
3042
|
"get_conversion_plan",
|
|
3027
3043
|
"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
|
|
3044
|
+
"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
3045
|
z.object({}).shape,
|
|
3030
3046
|
async (c) => ok("Approved conversion plan.", await data(c, "GET", `/management/projects/current/conversion-plan`))
|
|
3031
3047
|
),
|
|
@@ -3757,7 +3773,7 @@ ${lines.join("\n")}`, found);
|
|
|
3757
3773
|
// through the client's request plumbing — no bespoke SDK method per endpoint.
|
|
3758
3774
|
...lifecycleTools()
|
|
3759
3775
|
];
|
|
3760
|
-
return defs;
|
|
3776
|
+
return defs.map(withProjectId);
|
|
3761
3777
|
}
|
|
3762
3778
|
function registerTools(server, deps) {
|
|
3763
3779
|
const withElicit = {
|
|
@@ -3949,11 +3965,18 @@ in preview and is blank in production. Always pass the project's id.
|
|
|
3949
3965
|
accepts it and ignores it**, silently. No error, no warning, wrong project.
|
|
3950
3966
|
|
|
3951
3967
|
So call \`get_project\` (no arguments) first \u2014 it reports the project you are actually
|
|
3952
|
-
writing to.
|
|
3953
|
-
|
|
3954
|
-
**
|
|
3955
|
-
|
|
3956
|
-
|
|
3968
|
+
writing to.
|
|
3969
|
+
|
|
3970
|
+
**If the grant covers the WHOLE WORKSPACE, that is not a blocker and there is nothing to
|
|
3971
|
+
stop for.** Pass \`projectId\` on every project tool \u2014 \`list_projects\` gives you the id, and
|
|
3972
|
+
\`get_project { projectId }\` confirms you are addressing the right site. Every tool takes it,
|
|
3973
|
+
including the code and deploy ones. Do NOT send the user to re-scope the connection; the only
|
|
3974
|
+
thing that genuinely blocks you is a grant on a different WORKSPACE, which is what an empty or
|
|
3975
|
+
foreign \`list_projects\` tells you.
|
|
3976
|
+
|
|
3977
|
+
If the connected project is simply the WRONG one for the work \u2014 a project-scoped grant pointed
|
|
3978
|
+
elsewhere \u2014 that is the case the user has to fix, in the BetterCMS dashboard under
|
|
3979
|
+
**Settings \u2192 Connected AI clients**. No tool can switch it.
|
|
3957
3980
|
|
|
3958
3981
|
If you must probe, probe with a \`create_content_model\` \u2014 models are deletable
|
|
3959
3982
|
(\`delete_content_model\`, soft-delete) and **there is no \`delete_component\`**. A component
|
|
@@ -4135,6 +4158,17 @@ the canvas but skips release annotation and publish-time injection, because ther
|
|
|
4135
4158
|
disk to annotate. Copy rendered on the client must carry the attributes in the HYDRATED DOM,
|
|
4136
4159
|
and only the canvas sees it \u2014 a release scan cannot.
|
|
4137
4160
|
|
|
4161
|
+
**On a workspace-wide connection, pass \`projectId\` on every call in this section** \u2014 that is
|
|
4162
|
+
all it takes; the code and deploy tools work from such a connection and nobody needs to re-scope
|
|
4163
|
+
anything. See \xA79.
|
|
4164
|
+
|
|
4165
|
+
**Which recipe.** There are TWO below and they are not alternatives you pick by taste \u2014 call
|
|
4166
|
+
\`get_binding_report\` and \`get_conversion_brief\` first and let the answer choose. A brief that
|
|
4167
|
+
comes back WITH PAGES means this project was imported and deployed, so its schema and values were
|
|
4168
|
+
derived for you already \u2192 **Recipe A**. A 404 or an empty brief means the site is not in the CMS
|
|
4169
|
+
yet \u2192 **Recipe B**. \`get_next_steps\` works on every plan and names the next unfinished step
|
|
4170
|
+
either way.
|
|
4171
|
+
|
|
4138
4172
|
**If you know Sanity, this is the same shape under different names:**
|
|
4139
4173
|
|
|
4140
4174
|
defineType schema in code -> content models / page fields (create_content_model,
|
|
@@ -4152,11 +4186,12 @@ and only the canvas sees it \u2014 a release scan cannot.
|
|
|
4152
4186
|
its build, the schema and the values already exist \u2014 a page per route, a field per element, and
|
|
4153
4187
|
the original copy carried on each field as its \`defaultValue\`. Call \`get_conversion_brief\`
|
|
4154
4188
|
first: it lists those pages, their routes, every bindable path with its current and original
|
|
4155
|
-
value, and the attributes to declare. SKIP steps 3 and 4 below and bind the keys it
|
|
4156
|
-
registering the schema again builds a second one over the first.
|
|
4189
|
+
value, and the attributes to declare. SKIP Recipe B's steps 3 and 4 below and bind the keys it
|
|
4190
|
+
names \u2014 registering the schema again builds a second one over the first.
|
|
4157
4191
|
|
|
4158
|
-
**
|
|
4159
|
-
mechanical, and there is a tool that does it.
|
|
4192
|
+
**Recipe A \u2014 imported and deployed: the codemod order.** For a derived site the whole of Recipe
|
|
4193
|
+
B's step 5 is mechanical, and there is a tool that does it. Run the codemod rather than editing by
|
|
4194
|
+
hand. THE ORDER, and every step of it matters:
|
|
4160
4195
|
|
|
4161
4196
|
1. \`get_conversion_brief { complete: true }\` \u2014 the COMPLETE brief, not the capped one. It comes
|
|
4162
4197
|
back uncapped and paged (50 pages at a time): follow \`cursor\` until it stops coming back and
|
|
@@ -4176,7 +4211,11 @@ mechanical, and there is a tool that does it. THE ORDER, and every step of it ma
|
|
|
4176
4211
|
what lets the coverage meter say WHY a path is undeclared instead of only that it is; without
|
|
4177
4212
|
it every one of them reads \`not-declared\`, which looks like a broken site rather than work
|
|
4178
4213
|
with a reason. It records and releases nothing.
|
|
4179
|
-
5. Push, or \`deploy_project
|
|
4214
|
+
5. Push, or \`deploy_project\`. A PLAIN HTML repo \u2014 no \`package.json\` \u2014 deploys by PUSH ONLY:
|
|
4215
|
+
\`deploy_project\` refuses such an archive by design, so pushing is the whole lane. Either way,
|
|
4216
|
+
poll \`get_deploy_status\` until \`slot\` is \`current\` (or \`awaitingPromote\` is false) before you
|
|
4217
|
+
read the report in step 6 \u2014 a report read against the previous release is the previous
|
|
4218
|
+
release's verdict.
|
|
4180
4219
|
6. \`get_binding_report\`. Alongside \`unmatched\` it now carries \`coverage\` \u2014 \`declared\` (what the
|
|
4181
4220
|
brief listed), \`bound\` (what the build declares) and \`pending\` with the reasons. Fix what it
|
|
4182
4221
|
names and release again until \`coverage.pending\` is empty.
|
|
@@ -4185,14 +4224,15 @@ mechanical, and there is a tool that does it. THE ORDER, and every step of it ma
|
|
|
4185
4224
|
|
|
4186
4225
|
**Or let BetterCMS propose the edit.** \`get_conversion_plan\` returns an APPROVED conversion \u2014 the
|
|
4187
4226
|
exact new contents of each template file, reviewed by a human in the dashboard and already checked
|
|
4188
|
-
against this project's real field paths. When there is one, apply it instead of
|
|
4189
|
-
|
|
4190
|
-
carry on from step
|
|
4227
|
+
against this project's real field paths. When there is one, apply it instead of Recipe A's step 2
|
|
4228
|
+
(the codemod): check out its \`baseHeadOid\`, branch from there, write each file's \`content\`
|
|
4229
|
+
verbatim, and carry on from Recipe A's step 5. A 404 with \`code: "no-approved-plan"\` means nobody approved one, so the
|
|
4191
4230
|
conversion is yours to write.
|
|
4192
4231
|
|
|
4193
|
-
**
|
|
4232
|
+
**Recipe B \u2014 not in the CMS yet: the steps.**
|
|
4194
4233
|
|
|
4195
|
-
1. \`pull_project_source\` (or clone the \`github\` remote it returns
|
|
4234
|
+
1. \`pull_project_source\` (or clone the \`github\` remote it returns \u2014 \`cloneUrl\` on the \`branch\`
|
|
4235
|
+
it names, which is the branch the provisioned Action builds). Read the SOURCE. Never
|
|
4196
4236
|
reconstruct content from the deployed HTML \u2014 that is how a site ends up bound to a copy of
|
|
4197
4237
|
its own stale build.
|
|
4198
4238
|
2. Decide the architecture WITH the human (\xA710) and record it: \`set_authoring_preference\`.
|
|
@@ -4266,7 +4306,9 @@ conversion is yours to write.
|
|
|
4266
4306
|
then switch the project to "Run as a server app" on the Hosting page. Never migrate a
|
|
4267
4307
|
static site to SSR without asking the human first.
|
|
4268
4308
|
Then \`get_binding_report.canvas.lane\` reads \`bridge\` or \`draft-route\` instead of \`none\`.
|
|
4269
|
-
6. Push, or \`deploy_project
|
|
4309
|
+
6. Push, or \`deploy_project\`. A PLAIN HTML repo \u2014 no \`package.json\` \u2014 deploys by PUSH ONLY:
|
|
4310
|
+
\`deploy_project\` refuses such an archive by design. Poll \`get_deploy_status\` until \`slot\` is
|
|
4311
|
+
\`current\` (or \`awaitingPromote\` is false). Then
|
|
4270
4312
|
\`get_binding_report\` \u2014 still \`text-match\`, and \`unmatched\` should be EMPTY because the
|
|
4271
4313
|
values are byte-equal to what the build renders. On a converted site read \`coverage\` too: it
|
|
4272
4314
|
counts the PINNED brief's paths, which \`unmatched\` cannot, because \`unmatched\` only ever
|
|
@@ -4279,6 +4321,10 @@ conversion is yours to write.
|
|
|
4279
4321
|
text against its entry values yourself before you call the page done. Then publish, and
|
|
4280
4322
|
fetch the live URL cache-busted (\xA712: publish and deploy are separate claims).
|
|
4281
4323
|
\`get_next_steps\` keeps reporting the gap until every one of these holds.
|
|
4324
|
+
|
|
4325
|
+
**Done means.** Whichever recipe you ran: \`get_binding_report\` reads \`mode "declared"\`,
|
|
4326
|
+
\`unmatched\` empty, \`bound\` above 0, \`coverage.pending\` empty, and \`canvas.lane\` is \`bridge\` or
|
|
4327
|
+
\`draft-route\` when the site should show structural drafts.
|
|
4282
4328
|
`;
|
|
4283
4329
|
|
|
4284
4330
|
// src/prompts.ts
|
|
@@ -4735,7 +4781,10 @@ function buildServer(deps) {
|
|
|
4735
4781
|
{ name: SERVER_NAME, version: SERVER_VERSION, ...SERVER_DISPLAY },
|
|
4736
4782
|
{
|
|
4737
4783
|
capabilities: { tools: {}, prompts: {}, resources: {} },
|
|
4738
|
-
|
|
4784
|
+
// 🔴 The plain-language ask, routed — the stdio twin of the hosted connector's line
|
|
4785
|
+
// (src/routes/mcp/index.ts MCP_INSTRUCTIONS). "Make my site editable" has one recipe and
|
|
4786
|
+
// one definition of done; without this an agent converts the page it landed on and stops.
|
|
4787
|
+
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."
|
|
4739
4788
|
}
|
|
4740
4789
|
);
|
|
4741
4790
|
server.registerResource(
|
|
@@ -4752,7 +4801,10 @@ function buildServer(deps) {
|
|
|
4752
4801
|
);
|
|
4753
4802
|
registerTools(server, {
|
|
4754
4803
|
auth: deps.auth,
|
|
4755
|
-
|
|
4804
|
+
// `project` is the per-call target of a workspace-wide grant; the SDK sends it as
|
|
4805
|
+
// X-BCMS-Project. Omitted entirely when absent so a project-scoped key's request is
|
|
4806
|
+
// byte-identical to before.
|
|
4807
|
+
createClient: (apiKey, project) => BetterCMS.management({ apiKey, baseUrl: deps.managementBaseUrl, ...project ? { project } : {} })
|
|
4756
4808
|
});
|
|
4757
4809
|
registerPrompts(server);
|
|
4758
4810
|
return server;
|