@bettercms-ai/mcp 0.22.1 → 0.24.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 +2 -2
- package/dist/index.js +388 -15
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -57,9 +57,9 @@ On the first tool call the server prints an authorization prompt to **stderr**
|
|
|
57
57
|
|
|
58
58
|
```
|
|
59
59
|
┌─ BetterCMS authorization required ─────────────────────────
|
|
60
|
-
│ Visit: https://
|
|
60
|
+
│ Visit: https://bettercms.ai/activate
|
|
61
61
|
│ Enter code: WDJB-MJHT
|
|
62
|
-
│ Or open: https://
|
|
62
|
+
│ Or open: https://bettercms.ai/activate?code=WDJB-MJHT
|
|
63
63
|
└────────────────────────────────────────────────────────────
|
|
64
64
|
```
|
|
65
65
|
|
package/dist/index.js
CHANGED
|
@@ -316,7 +316,7 @@ import { BetterCMS } from "@bettercms-ai/sdk";
|
|
|
316
316
|
import { z } from "zod";
|
|
317
317
|
|
|
318
318
|
// ../types/src/component.ts
|
|
319
|
-
var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: a component's children render WITHOUT field bindings, so their text is NOT click-to-edit on the canvas \u2014 it is edited through the component's declared `props` in the section dock. A `section` block's children stay click-to-edit. So when you choose a component, DECLARE A PROP for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section
|
|
319
|
+
var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: a component's children render WITHOUT field bindings, so their text is NOT click-to-edit on the canvas \u2014 it is edited through the component's declared `props` in the section dock. A `section` block's children stay click-to-edit. So when you choose a component, DECLARE A PROP for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section blueprint (list_components returns locked `builtin:*` blueprints with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and nine more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; for a recurring band, materialize the blueprint with create_component so it becomes project-scoped before implementation validation, Output or publication. Direct `builtin:*` component references exist only for legacy delivery compatibility. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
|
|
320
320
|
|
|
321
321
|
// ../types/src/layout-lucide-icons.ts
|
|
322
322
|
var LAYOUT_SECTION_ICONS = Object.freeze([
|
|
@@ -2408,10 +2408,15 @@ var fieldType = z.enum([
|
|
|
2408
2408
|
var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
|
|
2409
2409
|
var fieldKey = z.string().regex(/^[a-zA-Z0-9_]+$/, "letters, numbers, and underscores only");
|
|
2410
2410
|
var fieldShape = {
|
|
2411
|
-
key: fieldKey.describe(
|
|
2411
|
+
key: fieldKey.describe(
|
|
2412
|
+
"machine field key. \u{1F534} UNIQUE ACROSS THE WHOLE MODEL \u2014 API IDs share ONE FLAT NAMESPACE, so a field nested inside a group must NOT reuse a key used by another field or group. Every section's heading cannot be 'title'. Prefix with the section: 'hero_title', 'pricing_title', 'faq_title'. Reusing a key is refused, and if it slips through it leaves permanent errors in the editor and breaks conditional-visibility rules, which reference fields by bare key."
|
|
2413
|
+
),
|
|
2412
2414
|
label: z.string().min(1).describe("human label shown in the editor"),
|
|
2413
2415
|
type: fieldType,
|
|
2414
2416
|
required: z.boolean().optional(),
|
|
2417
|
+
richText: z.boolean().optional().describe(
|
|
2418
|
+
"prose formatting. DEFAULTS TO TRUE for 'text': the field is created as rich text so editors can bold, link and format it on the canvas, and the API returns rich text (render with rich() from @bettercms-ai/sdk; plain() for titles and meta). Pass false for a value that is NOT prose and must stay a bare string \u2014 a URL/href, a slug, an id, an email, a phone number, a CSS class, an icon name. A link stored as rich text will not work as an href."
|
|
2419
|
+
),
|
|
2415
2420
|
options: z.array(z.string()).optional().describe("choices when type is 'select'"),
|
|
2416
2421
|
config: z.record(z.string(), z.unknown()).optional().describe(
|
|
2417
2422
|
"per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, array {itemType: 'text'|'number'|'date'}, date {includeTime}, modular {blockSlugs: ['quote','gallery'], minItems?, maxItems?} \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model"
|
|
@@ -2421,9 +2426,39 @@ var fieldShape = {
|
|
|
2421
2426
|
)
|
|
2422
2427
|
};
|
|
2423
2428
|
var fieldObject = z.object(fieldShape);
|
|
2429
|
+
function flatFieldKeys(fields) {
|
|
2430
|
+
const out = [];
|
|
2431
|
+
const childrenOf = (f) => {
|
|
2432
|
+
const zones = f.config?.zones;
|
|
2433
|
+
if (zones?.nonRepeatable) return zones.nonRepeatable;
|
|
2434
|
+
if (zones?.repeatable?.fields) return zones.repeatable.fields;
|
|
2435
|
+
return f.fields ?? [];
|
|
2436
|
+
};
|
|
2437
|
+
for (const raw of Array.isArray(fields) ? fields : []) {
|
|
2438
|
+
const f = raw;
|
|
2439
|
+
if (typeof f?.key === "string" && f.key.trim()) out.push(f.key.trim());
|
|
2440
|
+
for (const raw2 of childrenOf(f)) {
|
|
2441
|
+
const c = raw2;
|
|
2442
|
+
if (typeof c?.key === "string" && c.key.trim()) out.push(c.key.trim());
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
return out;
|
|
2446
|
+
}
|
|
2447
|
+
function duplicateKeys(keys) {
|
|
2448
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2449
|
+
for (const k of keys) seen.set(k, (seen.get(k) ?? 0) + 1);
|
|
2450
|
+
return [...new Set(keys)].filter((k) => (seen.get(k) ?? 0) > 1);
|
|
2451
|
+
}
|
|
2452
|
+
function duplicateKeyFailure(dupes) {
|
|
2453
|
+
return `Duplicate API ID${dupes.length === 1 ? "" : "s"}: ${dupes.map((k) => `'${k}'`).join(", ")}. API IDs share ONE flat namespace across the whole model \u2014 a field inside a group must not reuse a key used by another field or group. Prefix each key with its section and retry (e.g. 'hero_title' and 'pricing_title', never 'title' in both).`;
|
|
2454
|
+
}
|
|
2424
2455
|
function toFields(fs) {
|
|
2425
2456
|
return (fs ?? []).map(toField);
|
|
2426
2457
|
}
|
|
2458
|
+
function proseType(f) {
|
|
2459
|
+
if (f.type !== "text") return f.type;
|
|
2460
|
+
return f.richText === false ? "text" : "richtext";
|
|
2461
|
+
}
|
|
2427
2462
|
function toField(f) {
|
|
2428
2463
|
const base = {
|
|
2429
2464
|
key: f.key,
|
|
@@ -2457,7 +2492,7 @@ function toField(f) {
|
|
|
2457
2492
|
}
|
|
2458
2493
|
return {
|
|
2459
2494
|
...base,
|
|
2460
|
-
type: f
|
|
2495
|
+
type: proseType(f),
|
|
2461
2496
|
...f.options ? { options: f.options } : {},
|
|
2462
2497
|
...f.config ? { config: f.config } : {}
|
|
2463
2498
|
};
|
|
@@ -2717,6 +2752,7 @@ function buildToolDefs(deps) {
|
|
|
2717
2752
|
const getLayoutInput = z.object({
|
|
2718
2753
|
scope: z.enum(["global", "page"]).default("global"),
|
|
2719
2754
|
pageId: z.string().min(1).optional().describe("page id or slug; required for page scope"),
|
|
2755
|
+
copy: z.enum(["draft", "published"]).optional().describe("which copy to read; default draft. Verifying a publish MUST read copy:'published' and check the response's copy echo \u2014 the draft is never a publish receipt (FLO-1188)."),
|
|
2720
2756
|
projectId: z.string().min(1).optional().describe("required only for a workspace-scoped grant")
|
|
2721
2757
|
});
|
|
2722
2758
|
const layoutBinding = z.object({ inputId: z.string().min(1), fieldId: z.string().min(1) });
|
|
@@ -2803,6 +2839,83 @@ function buildToolDefs(deps) {
|
|
|
2803
2839
|
slug: slug.describe("url-safe unique slug (lowercase letters/numbers/hyphens)"),
|
|
2804
2840
|
projectId: z.string().min(1).optional().describe("only needed for a workspace-wide key")
|
|
2805
2841
|
});
|
|
2842
|
+
const sectionEvidenceProjectId = z.string().min(1).max(64).describe("exact project id from get_project (not a slug); Section evidence is project-local and never reusable across projects");
|
|
2843
|
+
const sectionId = z.string().uuid().describe("Section definition id from Section Studio");
|
|
2844
|
+
const sectionVersion = z.number().int().positive().describe("exact Section schema version implemented or tested");
|
|
2845
|
+
const sha256 = z.string().regex(/^[0-9a-f]{64}$/i, "Expected a SHA-256 hex digest");
|
|
2846
|
+
const sectionViewport = z.object({
|
|
2847
|
+
name: z.string().trim().min(1).max(64),
|
|
2848
|
+
width: z.number().int().min(240).max(7680),
|
|
2849
|
+
height: z.number().int().min(240).max(7680)
|
|
2850
|
+
});
|
|
2851
|
+
const sectionEvidenceDigest = z.string().regex(/^(?:sha256:)?[0-9a-f]{64}$/i, "Expected a SHA-256 digest");
|
|
2852
|
+
const nonEmptyDescriptor = z.record(z.string(), z.unknown()).refine((value) => Object.keys(value).length > 0, "Descriptor cannot be empty");
|
|
2853
|
+
const sectionRequestId = z.string().uuid().describe("durable Section validation request id returned by list_section_validation_requests or the dashboard");
|
|
2854
|
+
const sectionHttpUrl = z.string().url().max(2048).refine((value) => {
|
|
2855
|
+
try {
|
|
2856
|
+
const protocol = new URL(value).protocol;
|
|
2857
|
+
return protocol === "http:" || protocol === "https:";
|
|
2858
|
+
} catch {
|
|
2859
|
+
return false;
|
|
2860
|
+
}
|
|
2861
|
+
}, "Expected an HTTP or HTTPS URL");
|
|
2862
|
+
const sectionValidationViewport = sectionViewport.extend({
|
|
2863
|
+
status: z.enum(["passed", "failed"]),
|
|
2864
|
+
evidenceDigest: sectionEvidenceDigest
|
|
2865
|
+
});
|
|
2866
|
+
const submitSectionManifestInput = z.object({
|
|
2867
|
+
projectId: sectionEvidenceProjectId,
|
|
2868
|
+
requestId: sectionRequestId.optional().describe("include when fulfilling a claimed dashboard request so the evidence is bound to that exact run"),
|
|
2869
|
+
sectionId,
|
|
2870
|
+
version: sectionVersion,
|
|
2871
|
+
apiId: z.string().regex(/^[a-z][A-Za-z0-9]*$/, "Use lower camelCase").max(100),
|
|
2872
|
+
schemaHash: sha256.describe("exact SHA-256 hex hash shown for this Section version"),
|
|
2873
|
+
commitSha: z.string().regex(/^[0-9a-f]{7,64}$/i).describe("git commit containing the implementation"),
|
|
2874
|
+
loader: nonEmptyDescriptor.describe("non-empty serializable loader descriptor emitted by the repo build; never executable code"),
|
|
2875
|
+
previewAdapter: nonEmptyDescriptor.describe("non-empty serializable adapter descriptor for the customer's real app shell; never executable code"),
|
|
2876
|
+
nativeViewports: z.array(sectionViewport).max(20).optional().describe("the customer's named responsive viewports; BetterCMS also enforces Desktop 1440x900, Tablet 768x1024, and Mobile 390x844")
|
|
2877
|
+
});
|
|
2878
|
+
const submitSectionValidationInput = z.object({
|
|
2879
|
+
projectId: sectionEvidenceProjectId.describe("exact project id from get_project (not a slug); must match the manifest's project"),
|
|
2880
|
+
requestId: sectionRequestId.optional().describe("same claimed request id used for the manifest; binds and terminalizes that run"),
|
|
2881
|
+
sectionId: sectionId.describe("exact Section definition id"),
|
|
2882
|
+
version: sectionVersion,
|
|
2883
|
+
manifestId: z.string().uuid().describe("manifest id returned by submit_section_manifest; it pins the schema hash and commit SHA"),
|
|
2884
|
+
status: z.enum(["passed", "failed"]).describe("the actual result of the external repo/app-shell validation"),
|
|
2885
|
+
fixtureHash: sha256.describe("SHA-256 hex hash of the canonical preview dataset and stress fixtures used"),
|
|
2886
|
+
evidenceDigest: sectionEvidenceDigest.describe("SHA-256 digest of the immutable validation evidence bundle (screenshots/report/results)"),
|
|
2887
|
+
appShell: z.object({
|
|
2888
|
+
kind: z.literal("actual-app"),
|
|
2889
|
+
identifier: z.string().trim().min(1).max(255),
|
|
2890
|
+
url: sectionHttpUrl.optional()
|
|
2891
|
+
}).describe("the real customer application shell used for the validation run"),
|
|
2892
|
+
viewportResults: z.array(sectionValidationViewport).min(1).max(23).describe("one signed result for every required and native manifest viewport")
|
|
2893
|
+
});
|
|
2894
|
+
const listSectionValidationRequestsInput = z.object({
|
|
2895
|
+
projectId: sectionEvidenceProjectId,
|
|
2896
|
+
limit: z.number().int().min(1).max(100).optional()
|
|
2897
|
+
});
|
|
2898
|
+
const claimSectionValidationRequestInput = z.object({
|
|
2899
|
+
projectId: sectionEvidenceProjectId,
|
|
2900
|
+
requestId: sectionRequestId,
|
|
2901
|
+
commitSha: z.string().regex(/^[0-9a-f]{7,64}$/i).describe("exact git commit the agent will inspect and validate; evidence must use this same commit"),
|
|
2902
|
+
providerRunId: z.string().trim().min(1).max(255).optional(),
|
|
2903
|
+
providerRunUrl: sectionHttpUrl.optional().describe("optional HTTP(S) link to the user's external agent run")
|
|
2904
|
+
});
|
|
2905
|
+
const completeSectionValidationRequestInput = z.object({
|
|
2906
|
+
projectId: sectionEvidenceProjectId,
|
|
2907
|
+
requestId: sectionRequestId,
|
|
2908
|
+
manifestId: z.string().uuid().optional(),
|
|
2909
|
+
validationRunId: z.string().uuid().optional()
|
|
2910
|
+
});
|
|
2911
|
+
const failSectionValidationRequestInput = z.object({
|
|
2912
|
+
projectId: sectionEvidenceProjectId,
|
|
2913
|
+
requestId: sectionRequestId,
|
|
2914
|
+
errorCode: z.string().trim().regex(/^[A-Z][A-Z0-9_]*$/).max(100),
|
|
2915
|
+
errorMessage: z.string().trim().min(1).max(2e3),
|
|
2916
|
+
providerRunId: z.string().trim().min(1).max(255).optional(),
|
|
2917
|
+
providerRunUrl: sectionHttpUrl.optional().describe("optional HTTP(S) link to the user's external agent run")
|
|
2918
|
+
});
|
|
2806
2919
|
function lifecycleTools() {
|
|
2807
2920
|
const def = (name, title, description, shape, run) => ({
|
|
2808
2921
|
name,
|
|
@@ -3063,6 +3176,13 @@ function buildToolDefs(deps) {
|
|
|
3063
3176
|
return ok("Recorded the authoring architecture.", await data(c, "PATCH", `/management/projects/current/authoring-preference`, { preference }));
|
|
3064
3177
|
}
|
|
3065
3178
|
),
|
|
3179
|
+
def(
|
|
3180
|
+
"set_binding_mode",
|
|
3181
|
+
"Set how the site's bindings are resolved",
|
|
3182
|
+
"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 a project-scoped connection carrying the artifact:write scope \u2014 the same authority that deploys the site \u2014 because this decides what every future release does to every page; a workspace-wide grant is refused with 403. See section 13 of the bettercms://playbook/schema resource.",
|
|
3183
|
+
z.object({ declaredBindings: z.boolean().describe("true = trust the template's declared bindings; false = text-match (the default)") }).shape,
|
|
3184
|
+
async (c, a) => ok("Recorded the binding mode.", await data(c, "PATCH", `/management/projects/current/binding-mode`, { declaredBindings: a.declaredBindings }))
|
|
3185
|
+
),
|
|
3066
3186
|
def(
|
|
3067
3187
|
"clone_project",
|
|
3068
3188
|
"Clone a project",
|
|
@@ -3172,6 +3292,13 @@ function buildToolDefs(deps) {
|
|
|
3172
3292
|
z.object({}).shape,
|
|
3173
3293
|
async (c) => ok("Next steps.", await data(c, "GET", `/management/insights/next-steps`))
|
|
3174
3294
|
),
|
|
3295
|
+
def(
|
|
3296
|
+
"get_binding_report",
|
|
3297
|
+
"Check what on the live site is editable",
|
|
3298
|
+
"The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; this returns what that scan found, per slot: `mode` ('text-match' = bindings guessed from rendered text, 'declared' = the template declares them), `pagesInspected`, `bound` (elements carrying a binding), and `unmatched` \u2014 per page, each path with its kind and the reason it failed (not-declared / ambiguous-text / no-element). DEPLOY FIRST: before any release there is no report and this answers pages 0, mode null, refreshRequired true. It certifies exactly one thing \u2014 that every non-empty field of every page has SOME element carrying its path. It cannot see copy that was never modelled, so diff each route's visible text against its entry values yourself before calling a page done. Pass `slot` ('current' or 'staging') to read the other tree; the default is the slot this project's releases land in.",
|
|
3299
|
+
z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read; defaults to the one this project deploys to") }).shape,
|
|
3300
|
+
async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
|
|
3301
|
+
),
|
|
3175
3302
|
def(
|
|
3176
3303
|
"get_analytics_overview",
|
|
3177
3304
|
"Get traffic overview",
|
|
@@ -3241,6 +3368,102 @@ function buildToolDefs(deps) {
|
|
|
3241
3368
|
z.object({ jobId: z.string().min(1) }).shape,
|
|
3242
3369
|
async (c, a) => ok("Job rejected.", await data(c, "POST", `/management/bulk/jobs/${s(a.jobId)}/reject`))
|
|
3243
3370
|
),
|
|
3371
|
+
def(
|
|
3372
|
+
"list_section_validation_requests",
|
|
3373
|
+
"List queued Section validation requests",
|
|
3374
|
+
"Poll for user-agent Section implementation-validation requests in this exact project. BetterCMS cannot push work into an ordinary MCP client: call this explicitly, claim one request, inspect and run the customer's real repository/app shell, then submit request-bound evidence and complete it. Returns only unclaimed user-agent requests; it never exposes another project.",
|
|
3375
|
+
listSectionValidationRequestsInput.shape,
|
|
3376
|
+
async (c, a) => ok("Queued user-agent Section validation requests.", await data(
|
|
3377
|
+
c,
|
|
3378
|
+
"GET",
|
|
3379
|
+
`/projects/${s(a.projectId)}/section-evidence/implementation-requests${q({ limit: a.limit })}`
|
|
3380
|
+
))
|
|
3381
|
+
),
|
|
3382
|
+
def(
|
|
3383
|
+
"claim_section_validation_request",
|
|
3384
|
+
"Claim a Section validation request",
|
|
3385
|
+
"Atomically claim one queued user-agent request and pin the exact git commit you will inspect. Claim BEFORE submitting evidence. BetterCMS records coordination only; all customer code and responsive checks must run in the user's repository and real app shell.",
|
|
3386
|
+
claimSectionValidationRequestInput.shape,
|
|
3387
|
+
async (c, a) => ok("Section validation request claimed.", await data(
|
|
3388
|
+
c,
|
|
3389
|
+
"POST",
|
|
3390
|
+
`/projects/${s(a.projectId)}/section-evidence/implementation-requests/${s(a.requestId)}/claim`,
|
|
3391
|
+
{
|
|
3392
|
+
commitSha: a.commitSha,
|
|
3393
|
+
providerRunId: a.providerRunId,
|
|
3394
|
+
providerRunUrl: a.providerRunUrl
|
|
3395
|
+
}
|
|
3396
|
+
))
|
|
3397
|
+
),
|
|
3398
|
+
def(
|
|
3399
|
+
"complete_section_validation_request",
|
|
3400
|
+
"Complete a Section validation request",
|
|
3401
|
+
"Complete a claimed request only after submit_section_manifest and submit_section_validation have bound exact, passed evidence to the same requestId and pinned commit. This cannot grant the separate human Visual Approval.",
|
|
3402
|
+
completeSectionValidationRequestInput.shape,
|
|
3403
|
+
async (c, a) => ok("Section validation request completed.", await data(
|
|
3404
|
+
c,
|
|
3405
|
+
"POST",
|
|
3406
|
+
`/projects/${s(a.projectId)}/section-evidence/implementation-requests/${s(a.requestId)}/complete`,
|
|
3407
|
+
{ manifestId: a.manifestId, validationRunId: a.validationRunId }
|
|
3408
|
+
))
|
|
3409
|
+
),
|
|
3410
|
+
def(
|
|
3411
|
+
"fail_section_validation_request",
|
|
3412
|
+
"Fail a Section validation request",
|
|
3413
|
+
"Truthfully close a claimed request when the renderer, validation command, app shell, or required evidence is missing or fails. This path intentionally works without a manifest so 'not implemented' is representable instead of being reported as passed or left queued forever.",
|
|
3414
|
+
failSectionValidationRequestInput.shape,
|
|
3415
|
+
async (c, a) => ok("Section validation request failed.", await data(
|
|
3416
|
+
c,
|
|
3417
|
+
"POST",
|
|
3418
|
+
`/projects/${s(a.projectId)}/section-evidence/implementation-requests/${s(a.requestId)}/fail`,
|
|
3419
|
+
{
|
|
3420
|
+
errorCode: a.errorCode,
|
|
3421
|
+
errorMessage: a.errorMessage,
|
|
3422
|
+
providerRunId: a.providerRunId,
|
|
3423
|
+
providerRunUrl: a.providerRunUrl
|
|
3424
|
+
}
|
|
3425
|
+
))
|
|
3426
|
+
),
|
|
3427
|
+
def(
|
|
3428
|
+
"submit_section_manifest",
|
|
3429
|
+
"Submit a Section implementation manifest",
|
|
3430
|
+
"Record the implementation manifest produced for one exact Section schema version by CI or an agent running INSIDE the user's repository. BetterCMS does not execute customer code: inspect/build the real renderer in the user's app shell first, then submit its exact API ID, schema hash, commit SHA, loader, preview adapter, and native responsive viewports. Requires a project-scoped artifact:write credential. This records evidence only; it does not validate the implementation and cannot create human Visual Approval.",
|
|
3431
|
+
submitSectionManifestInput.shape,
|
|
3432
|
+
async (c, a) => ok("Section implementation manifest recorded.", await data(
|
|
3433
|
+
c,
|
|
3434
|
+
"POST",
|
|
3435
|
+
`/projects/${s(a.projectId)}/section-evidence/sections/${s(a.sectionId)}/versions/${a.version}/manifests`,
|
|
3436
|
+
{
|
|
3437
|
+
requestId: a.requestId,
|
|
3438
|
+
apiId: a.apiId,
|
|
3439
|
+
schemaHash: a.schemaHash,
|
|
3440
|
+
commitSha: a.commitSha,
|
|
3441
|
+
loader: a.loader,
|
|
3442
|
+
previewAdapter: a.previewAdapter,
|
|
3443
|
+
nativeViewports: a.nativeViewports
|
|
3444
|
+
}
|
|
3445
|
+
))
|
|
3446
|
+
),
|
|
3447
|
+
def(
|
|
3448
|
+
"submit_section_validation",
|
|
3449
|
+
"Submit a Section validation result",
|
|
3450
|
+
"Record a real validation result for one exact Section schema version and manifest. Run the component in the user's repository and real app shell across the canonical preview dataset, AI stress fixtures, and required/native viewports BEFORE calling this tool; BetterCMS never runs that customer code. Pass status 'passed' only when those checks actually passed, otherwise 'failed'. The manifest pins schemaHash and commitSha; fixtureHash and evidenceDigest pin the tested data and immutable evidence bundle. Requires project-scoped artifact:write and cannot create the separate human Visual Approval required for publication.",
|
|
3451
|
+
submitSectionValidationInput.shape,
|
|
3452
|
+
async (c, a) => ok("Section validation result recorded.", await data(
|
|
3453
|
+
c,
|
|
3454
|
+
"POST",
|
|
3455
|
+
`/projects/${s(a.projectId)}/section-evidence/sections/${s(a.sectionId)}/versions/${a.version}/validations`,
|
|
3456
|
+
{
|
|
3457
|
+
requestId: a.requestId,
|
|
3458
|
+
manifestId: a.manifestId,
|
|
3459
|
+
status: a.status,
|
|
3460
|
+
fixtureHash: a.fixtureHash,
|
|
3461
|
+
evidenceDigest: a.evidenceDigest,
|
|
3462
|
+
appShell: a.appShell,
|
|
3463
|
+
viewportResults: a.viewportResults
|
|
3464
|
+
}
|
|
3465
|
+
))
|
|
3466
|
+
),
|
|
3244
3467
|
def(
|
|
3245
3468
|
"get_deploy_status",
|
|
3246
3469
|
"Get deploy/build status",
|
|
@@ -3296,11 +3519,13 @@ function buildToolDefs(deps) {
|
|
|
3296
3519
|
name: "create_page",
|
|
3297
3520
|
config: {
|
|
3298
3521
|
title: "Create a page",
|
|
3299
|
-
description: "Create a page with its own typed schema. Supports pageType 'singleton' (exactly one entry \u2014 Home, About, Contact) and 'dynamic' (many entries sharing the schema \u2014 Blog posts, Products). Project-scoped from the key. Additive \u2014 does not delete or overwrite existing pages. FIRST read the page's real markup and DECOMPOSE it into a destructured tree: each visual section becomes a nested field \u2014 a fixed grouped block \u2192 type 'group', a repeating list of items (cards, testimonials, features, FAQs) \u2192 type 'repeater' \u2014 each carrying its own child `fields`. Do NOT flatten sections into many flat top-level fields. Build the full nested tree, then call this once. That tree is the page's SCHEMA \u2014 what it holds. " + SECTION_DOCTRINE,
|
|
3522
|
+
description: "Create a page with its own typed schema. Supports pageType 'singleton' (exactly one entry \u2014 Home, About, Contact) and 'dynamic' (many entries sharing the schema \u2014 Blog posts, Products). Project-scoped from the key. Additive \u2014 does not delete or overwrite existing pages. FIRST read the page's real markup and DECOMPOSE it into a destructured tree: each visual section becomes a nested field \u2014 a fixed grouped block \u2192 type 'group', a repeating list of items (cards, testimonials, features, FAQs) \u2192 type 'repeater' \u2014 each carrying its own child `fields`. Do NOT flatten sections into many flat top-level fields. Build the full nested tree, then call this once. \u{1F534} Every field key must be UNIQUE ACROSS THE WHOLE TREE \u2014 the namespace is flat, so 'title' cannot appear in two sections; prefix each with its section ('hero_title', 'faq_title'). Prose fields (headings, body copy, descriptions) are created as RICH TEXT by default; pass richText:false for values that must stay bare strings \u2014 hrefs, slugs, ids, emails. That tree is the page's SCHEMA \u2014 what it holds. " + SECTION_DOCTRINE,
|
|
3300
3523
|
inputSchema: createPageInput.shape
|
|
3301
3524
|
},
|
|
3302
3525
|
handler: guard(
|
|
3303
3526
|
async (args) => withClient(async (client) => {
|
|
3527
|
+
const dupes = duplicateKeys(flatFieldKeys(args.fields));
|
|
3528
|
+
if (dupes.length > 0) return fail(duplicateKeyFailure(dupes));
|
|
3304
3529
|
const page = await client.createPage({
|
|
3305
3530
|
title: args.title,
|
|
3306
3531
|
slug: args.slug,
|
|
@@ -3326,6 +3551,8 @@ function buildToolDefs(deps) {
|
|
|
3326
3551
|
},
|
|
3327
3552
|
handler: guard(
|
|
3328
3553
|
async (args) => withClient(async (client) => {
|
|
3554
|
+
const modelDupes = duplicateKeys(flatFieldKeys(args.fields));
|
|
3555
|
+
if (modelDupes.length > 0) return fail(duplicateKeyFailure(modelDupes));
|
|
3329
3556
|
const model = await client.createModel({
|
|
3330
3557
|
name: args.name,
|
|
3331
3558
|
slug: args.slug,
|
|
@@ -3350,9 +3577,8 @@ function buildToolDefs(deps) {
|
|
|
3350
3577
|
handler: guard(
|
|
3351
3578
|
async (args) => withClient(async (client) => {
|
|
3352
3579
|
const model = await client.getModel(args.modelId);
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
}
|
|
3580
|
+
const dupes = duplicateKeys([...flatFieldKeys(model.fields), args.key]);
|
|
3581
|
+
if (dupes.length > 0) return fail(duplicateKeyFailure(dupes));
|
|
3356
3582
|
const updated = await client.updateModel(args.modelId, {
|
|
3357
3583
|
fields: [...model.fields, toField(args)]
|
|
3358
3584
|
});
|
|
@@ -3596,13 +3822,13 @@ function buildToolDefs(deps) {
|
|
|
3596
3822
|
{
|
|
3597
3823
|
name: "get_layout",
|
|
3598
3824
|
config: {
|
|
3599
|
-
title: "Get the project or page Layout
|
|
3600
|
-
description: "Read the Global Layout or one page's Layout override, including its optimistic revision. Always read this before update_layout.",
|
|
3825
|
+
title: "Get the project or page Layout",
|
|
3826
|
+
description: "Read the Global Layout or one page's Layout override, including its optimistic revision. Always read this before update_layout. copy:'published' reads the published copy \u2014 the only honest receipt for a publish claim.",
|
|
3601
3827
|
inputSchema: getLayoutInput.shape
|
|
3602
3828
|
},
|
|
3603
3829
|
handler: guard(async (args) => withClient(async (client) => {
|
|
3604
3830
|
const layout = await client.getManagedLayout(args);
|
|
3605
|
-
return ok(`${layout.scope === "global" ? "Global" : `Page ${layout.pageSlug}`} Layout draft at revision ${layout.revision}.`, layout);
|
|
3831
|
+
return ok(`${layout.scope === "global" ? "Global" : `Page ${layout.pageSlug}`} Layout ${args.copy === "published" ? "PUBLISHED copy" : "draft"} at revision ${layout.revision}.`, layout);
|
|
3606
3832
|
}))
|
|
3607
3833
|
},
|
|
3608
3834
|
{
|
|
@@ -3980,19 +4206,163 @@ renders. Three consequences, each load-bearing:
|
|
|
3980
4206
|
c. set_page_content values EXACTLY equal to the text the site renders \u2014
|
|
3981
4207
|
binding matches by value, so a paraphrase binds nothing
|
|
3982
4208
|
d. update_page status 'published' \u2014 the canvas binds the PUBLISHED copy
|
|
3983
|
-
2. A value that renders in more than one place
|
|
3984
|
-
|
|
3985
|
-
|
|
4209
|
+
2. A value that renders in more than one place is still ONE field: bind every element that
|
|
4210
|
+
renders it, and the editor keeps them in sync \u2014 an edit patches every copy at once.
|
|
4211
|
+
Binding only one copy leaves the others showing the old text until the next rebuild.
|
|
4212
|
+
3. Make the chrome ITSELF editable by speaking the layout grammar: the nav/footer
|
|
4213
|
+
elements declare \`data-bcms-layout-section="navigation"\` / \`"footer"\`, and each
|
|
4214
|
+
CMS-backed text inside them a \`data-bcms-layout-field="layout:<sectionId>:<fieldId>"\`
|
|
4215
|
+
marker (fieldId is the field's REAL id, verbatim \u2014 production layout ids are
|
|
4216
|
+
section-prefixed and dotted, e.g. \`footer.tagline\`, so the marker reads
|
|
4217
|
+
\`layout:footer:footer.tagline\`) \u2014 the canvas then gives them hover chrome, the
|
|
4218
|
+
Layout side panel, and
|
|
4219
|
+
double-click editing, writing to the project Layout store (never the page).
|
|
4220
|
+
The address reaches INSIDE structured fields by walking the schema: a group's text
|
|
4221
|
+
sub appends its slug (\`layout:navigation:navigation.cta.label\`), a repeater row its
|
|
4222
|
+
STORED index then the slug (\`layout:navigation:navigation.links.0.label\`), nesting
|
|
4223
|
+
as deep as the schema goes (\`layout:footer:footer.link-groups.0.links.1.label\`).
|
|
4224
|
+
Three rules, each one a measured defect when broken:
|
|
4225
|
+
a. a field rendered by TWO elements gets a marker on BOTH \u2014 the editor keeps the copies
|
|
4226
|
+
in sync, and a marker on only one leaves the other stale;
|
|
4227
|
+
b. PROVENANCE \u2014 mark an element only when the LAYOUT supplied its value; a marker
|
|
4228
|
+
over a fallback/singleton-sourced string opens an editor for a row that does not
|
|
4229
|
+
exist;
|
|
4230
|
+
c. row markers use the value's STORED index (a row you filtered out of the render
|
|
4231
|
+
still occupies its slot), or the edit lands on the wrong row.
|
|
4232
|
+
Text/longtext leaves edit inline; link/select/image leaves are side-panel-only by
|
|
4233
|
+
design (their formats need a real control). THE DOCTRINE: every string a marketer
|
|
4234
|
+
can see must be addressable \u2014 no marker means read-only on the canvas, so an
|
|
4235
|
+
unmarked CMS-backed string is a defect, not a style choice.
|
|
4236
|
+
4. Keep chrome semantic \u2014 \`<nav>\`, \`<footer>\`, page content inside \`<main>\`, mastheads
|
|
3986
4237
|
as a top-level \`<header>\`. Chrome is edited through the project Layout, not the page,
|
|
3987
4238
|
and semantic landmarks are how the editor keeps a nav edit from being written into page
|
|
3988
4239
|
content. Div-built chrome outside \`<main>\` is still excluded; div-built chrome with no
|
|
3989
4240
|
\`<main>\` anywhere loses that protection.
|
|
3990
4241
|
|
|
4242
|
+
**Structural drafts can render on the real site too \u2014 the draft bridge.** Wrap your page's
|
|
4243
|
+
blocks in \`BcmsDraftBridge\` (\`@bettercms-ai/next/draft-bridge\`) instead of calling
|
|
4244
|
+
\`BcmsBlocks\` directly: standalone it renders identically, and inside the visual editor it
|
|
4245
|
+
receives the DRAFT block tree over a same-origin postMessage handshake and re-renders it with
|
|
4246
|
+
the site's own components \u2014 so adding, removing or reordering sections previews in the site's
|
|
4247
|
+
real design instead of the platform's approximate renderer. Sites without the bridge keep the
|
|
4248
|
+
approximate fallback; unpublished ROUTES always fall back (a static build has no file to frame).
|
|
4249
|
+
|
|
3991
4250
|
**Hosting decides whether a canvas exists at all.** A site deployed here is framed through a
|
|
3992
4251
|
same-origin proxy \u2014 that is what the canvas requires. A site hosted elsewhere (your own Vercel,
|
|
3993
4252
|
your own server) has NO canvas today: the SDK's draft mode with \`stega: true\` embeds invisible
|
|
3994
4253
|
per-field provenance in fetched strings, which prepares the content for editing surfaces, but do
|
|
3995
4254
|
not promise a canvas for an externally-hosted site.
|
|
4255
|
+
|
|
4256
|
+
**\xA712 \u2014 RECEIPTS: a claim about published state needs a read of the PUBLISHED copy.** The
|
|
4257
|
+
doctrine this encodes cost a real incident (FLO-1188): a publish was verified against the
|
|
4258
|
+
draft for ~50 minutes because the reader silently returned the draft, and every check was
|
|
4259
|
+
green on the wrong document. Three rules, none optional:
|
|
4260
|
+
1. NEVER verify a write by re-reading the store you wrote. Draft writes verify against
|
|
4261
|
+
the draft; a PUBLISH claim verifies ONLY via \`get_layout copy:'published'\` (or the
|
|
4262
|
+
entry/page's published copy) \u2014 and check the response's \`copy\` echo says
|
|
4263
|
+
'published'. A reader that ignores your copy selector hands you the draft and a
|
|
4264
|
+
false green; the echo is how you catch it.
|
|
4265
|
+
2. Publish and deploy are SEPARATE claims. "Published" means the published copy changed;
|
|
4266
|
+
the LIVE SITE changes only after its next deploy/rebuild. Never report "it's live"
|
|
4267
|
+
from a publish receipt \u2014 fetch the live URL (cache-busted) for that claim.
|
|
4268
|
+
3. A tool param the schema does not declare is SILENTLY DROPPED, not rejected. If a
|
|
4269
|
+
call's behavior doesn't change when you change a param, treat the param as dead and
|
|
4270
|
+
verify through an independent channel before trusting any result built on it.
|
|
4271
|
+
|
|
4272
|
+
## 13. Convert an imported repo into a CMS-backed, editable site
|
|
4273
|
+
|
|
4274
|
+
\xA711 says a deploy does not make a site editable. This is the recipe that does, and it ends in a
|
|
4275
|
+
receipt you can read: \`get_binding_report\` says \`mode: "declared"\` with zero unmatched paths.
|
|
4276
|
+
|
|
4277
|
+
**Scope, before you start.**
|
|
4278
|
+
|
|
4279
|
+
(a) This is the FIELD-driven conversion \u2014 page fields and collections. If the human answered
|
|
4280
|
+
\`components\` at the \xA710 gate, go to \xA710's recipe instead: a component's editing surface is its
|
|
4281
|
+
declared \`props\`, and those bindings are NOT what \`get_binding_report\` walks.
|
|
4282
|
+
|
|
4283
|
+
(b) It works for any framework that emits STATIC HTML, because the binding contract is plain
|
|
4284
|
+
HTML attributes \u2014 the annotator, the injector and the canvas stamper never see your source.
|
|
4285
|
+
There are SDK helpers for Astro and Next. A Node-runtime site (\`bcms-runtime.json\`) edits on
|
|
4286
|
+
the canvas but skips release annotation and publish-time injection, because there is no HTML on
|
|
4287
|
+
disk to annotate. Copy rendered on the client must carry the attributes in the HYDRATED DOM,
|
|
4288
|
+
and only the canvas sees it \u2014 a release scan cannot.
|
|
4289
|
+
|
|
4290
|
+
**If you know Sanity, this is the same shape under different names:**
|
|
4291
|
+
|
|
4292
|
+
defineType schema in code -> content models / page fields (create_content_model,
|
|
4293
|
+
add_page_field, or bcms-content.json "schema")
|
|
4294
|
+
TypeGen -> @bettercms-ai/codegen
|
|
4295
|
+
GROQ query in the page -> @bettercms-ai/sdk read client, or the bcms-content.json
|
|
4296
|
+
build snapshot
|
|
4297
|
+
<PortableText> body -> @bettercms-ai/richtext portableTextToHtml, field type
|
|
4298
|
+
'document'
|
|
4299
|
+
data-sanity / stega -> data-bcms-field + data-bcms-kind (<BcmsField>), stega on
|
|
4300
|
+
draft reads
|
|
4301
|
+
Presentation tool overlays -> the visual editor canvas, framing your own build
|
|
4302
|
+
|
|
4303
|
+
**The steps.**
|
|
4304
|
+
|
|
4305
|
+
1. \`pull_project_source\` (or clone the \`github\` remote it returns). Read the SOURCE. Never
|
|
4306
|
+
reconstruct content from the deployed HTML \u2014 that is how a site ends up bound to a copy of
|
|
4307
|
+
its own stale build.
|
|
4308
|
+
2. Decide the architecture WITH the human (\xA710) and record it: \`set_authoring_preference\`.
|
|
4309
|
+
3. Register the schema. Per route: \`create_page\` + \`add_page_field\` (text / longtext /
|
|
4310
|
+
richtext / image / array groups). Per repeated content type: \`create_content_model\`, with a
|
|
4311
|
+
\`document\` body for articles. Site chrome goes through \`update_layout\`; images through
|
|
4312
|
+
\`create_media_upload\` / \`upload_asset\`, then reference the CMS URL. The repo-owned
|
|
4313
|
+
alternative to calling these one by one is a committed \`bcms-content.json\` carrying a
|
|
4314
|
+
\`schema\` block, which seeds models, pages and entries at connect time \u2014 the \`defineType\`
|
|
4315
|
+
analogue.
|
|
4316
|
+
4. Seed the entries with the copy EXACTLY as the source renders it (\`set_page_content\`,
|
|
4317
|
+
\`create_content_entry\`). Prose goes in as Portable Text arrays (\`_type: "block"\`); never
|
|
4318
|
+
markup inside a \`text\` field.
|
|
4319
|
+
**Prose MIGRATED from another CMS needs one extra check.** Every block's \`_type\` must be one
|
|
4320
|
+
this platform stores \u2014 \`block\`, or \`bcmsBlock\` carrying a \`schemaKey\` such as
|
|
4321
|
+
\`builtin:table\` \u2014 and a foreign node (a Sanity-shaped \`{_type:"table", rows:[{cells}]}\` is
|
|
4322
|
+
the one that has actually happened) is not merely unrendered: it has no component here, so it
|
|
4323
|
+
vanishes from the HTML every delivery surface reads while the stored value still looks whole.
|
|
4324
|
+
Verify each \`_type\` against the schema before you write, not after.
|
|
4325
|
+
5. Codemod the templates. Read each value from the CMS (\`@bettercms-ai/astro\` /
|
|
4326
|
+
\`@bettercms-ai/next\` client, or the \`bcms-content.json\` build snapshot), KEEP the in-code
|
|
4327
|
+
copy as the fallback, and declare the binding on the element that already renders it.
|
|
4328
|
+
Prefer schema-derived bindings \u2014 the TypeGen analogue:
|
|
4329
|
+
\`npx @bettercms-ai/codegen --bindings-out src/bettercms.bindings.generated.ts\`, then spread
|
|
4330
|
+
\`{...bcms.home.hero.title}\` / \`{...bcms.blog.features.$(i)}\`. The hand form is
|
|
4331
|
+
\`data-bcms-field="<path>"\` (plus \`data-bcms-kind="richtext"|"image"\`), \`data-bcms-props\`
|
|
4332
|
+
for an \`href\` / \`alt\` / \`src\`, the \xA711 layout markers for nav and footer, and
|
|
4333
|
+
\`<div data-bcms-field="body" data-bcms-kind="document">\` around a Portable Text render.
|
|
4334
|
+
Bind CONDITIONALLY (\`fromCms ? path : undefined\`) so a fallback row is never bound. A value
|
|
4335
|
+
rendered in N places carries the binding on ALL N \u2014 the editor keeps the copies in sync.
|
|
4336
|
+
**Read the LIVE SCHEMA before you bind \u2014 \`get_page\` / \`get_content_model\`, never the
|
|
4337
|
+
delivery snapshot.** A field nobody has authored yet is simply ABSENT from the payload, so a
|
|
4338
|
+
snapshot cannot tell "this field does not exist" from "this field is empty": bind against it
|
|
4339
|
+
and you declare a path for a \`cover\` field that was never created, which the report then
|
|
4340
|
+
reports as broken forever. The schema is the list of what exists; the snapshot is only what
|
|
4341
|
+
currently has a value.
|
|
4342
|
+
**An index or listing route binds NOTHING.** The editor loads ONE entry per route, and an
|
|
4343
|
+
index renders many, so a binding there addresses whichever entry the editor happened to
|
|
4344
|
+
load. Bind each item's fields on that item's OWN route (\`/blog/<slug>\`); on the index,
|
|
4345
|
+
render from the CMS and declare nothing.
|
|
4346
|
+
5b. **Where the content comes from at build time.** Two lanes, and they differ:
|
|
4347
|
+
- GIT-CONNECTED (recommended for a converted site): the platform's provisioned workflow
|
|
4348
|
+
writes \`bcms-content.json\` into the repo root before \`build\`, using the repo's
|
|
4349
|
+
\`BCMS_API_KEY\` secret. Read that file, with in-code fallbacks so a local or CI build
|
|
4350
|
+
without it still renders.
|
|
4351
|
+
- ARCHIVE (\`deploy_project\` / \`deploy_from_upload\`): the sandbox build runs with no env
|
|
4352
|
+
and no network content step, by design. So the archive MUST SHIP its own
|
|
4353
|
+
\`bcms-content.json\` \u2014 generate it locally with a delivery key and commit it. Without
|
|
4354
|
+
one the site renders its fallbacks and the report says \`no-element\` for every path.
|
|
4355
|
+
6. Push, or \`deploy_project\`; poll \`get_deploy_status\` until it is live. Then
|
|
4356
|
+
\`get_binding_report\` \u2014 still \`text-match\`, and \`unmatched\` should be EMPTY because the
|
|
4357
|
+
values are byte-equal to what the build renders. Now \`set_binding_mode
|
|
4358
|
+
{declaredBindings: true}\` and release again (an empty commit is enough). Do not flip before
|
|
4359
|
+
the report is clean: in declared mode an undeclared field simply stops being editable.
|
|
4360
|
+
7. **Receipts.** \`get_binding_report\` reads \`mode: "declared"\`, \`unmatched: []\`, and
|
|
4361
|
+
\`bound > 0\`. That certifies one thing only \u2014 that every non-empty field has SOME element
|
|
4362
|
+
carrying its path. It CANNOT see copy that was never modelled, so diff each route's visible
|
|
4363
|
+
text against its entry values yourself before you call the page done. Then publish, and
|
|
4364
|
+
fetch the live URL cache-busted (\xA712: publish and deploy are separate claims).
|
|
4365
|
+
\`get_next_steps\` keeps reporting the gap until every one of these holds.
|
|
3996
4366
|
`;
|
|
3997
4367
|
|
|
3998
4368
|
// src/prompts.ts
|
|
@@ -4434,7 +4804,7 @@ On 401/403, the MCP key needs (re)authorizing.`
|
|
|
4434
4804
|
|
|
4435
4805
|
// src/server.ts
|
|
4436
4806
|
var SERVER_NAME = "bettercms";
|
|
4437
|
-
var SERVER_VERSION = "1.
|
|
4807
|
+
var SERVER_VERSION = "1.4.0";
|
|
4438
4808
|
var SERVER_DISPLAY = {
|
|
4439
4809
|
title: "BetterCMS",
|
|
4440
4810
|
websiteUrl: "https://bettercms.ai",
|
|
@@ -4446,7 +4816,10 @@ var SERVER_DISPLAY = {
|
|
|
4446
4816
|
function buildServer(deps) {
|
|
4447
4817
|
const server = new McpServer(
|
|
4448
4818
|
{ name: SERVER_NAME, version: SERVER_VERSION, ...SERVER_DISPLAY },
|
|
4449
|
-
{
|
|
4819
|
+
{
|
|
4820
|
+
capabilities: { tools: {}, prompts: {}, resources: {} },
|
|
4821
|
+
instructions: "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."
|
|
4822
|
+
}
|
|
4450
4823
|
);
|
|
4451
4824
|
server.registerResource(
|
|
4452
4825
|
"schema-playbook",
|