@bettercms-ai/mcp 0.22.1 → 0.23.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 +275 -12
- 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
|
@@ -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,
|
|
@@ -3241,6 +3354,102 @@ function buildToolDefs(deps) {
|
|
|
3241
3354
|
z.object({ jobId: z.string().min(1) }).shape,
|
|
3242
3355
|
async (c, a) => ok("Job rejected.", await data(c, "POST", `/management/bulk/jobs/${s(a.jobId)}/reject`))
|
|
3243
3356
|
),
|
|
3357
|
+
def(
|
|
3358
|
+
"list_section_validation_requests",
|
|
3359
|
+
"List queued Section validation requests",
|
|
3360
|
+
"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.",
|
|
3361
|
+
listSectionValidationRequestsInput.shape,
|
|
3362
|
+
async (c, a) => ok("Queued user-agent Section validation requests.", await data(
|
|
3363
|
+
c,
|
|
3364
|
+
"GET",
|
|
3365
|
+
`/projects/${s(a.projectId)}/section-evidence/implementation-requests${q({ limit: a.limit })}`
|
|
3366
|
+
))
|
|
3367
|
+
),
|
|
3368
|
+
def(
|
|
3369
|
+
"claim_section_validation_request",
|
|
3370
|
+
"Claim a Section validation request",
|
|
3371
|
+
"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.",
|
|
3372
|
+
claimSectionValidationRequestInput.shape,
|
|
3373
|
+
async (c, a) => ok("Section validation request claimed.", await data(
|
|
3374
|
+
c,
|
|
3375
|
+
"POST",
|
|
3376
|
+
`/projects/${s(a.projectId)}/section-evidence/implementation-requests/${s(a.requestId)}/claim`,
|
|
3377
|
+
{
|
|
3378
|
+
commitSha: a.commitSha,
|
|
3379
|
+
providerRunId: a.providerRunId,
|
|
3380
|
+
providerRunUrl: a.providerRunUrl
|
|
3381
|
+
}
|
|
3382
|
+
))
|
|
3383
|
+
),
|
|
3384
|
+
def(
|
|
3385
|
+
"complete_section_validation_request",
|
|
3386
|
+
"Complete a Section validation request",
|
|
3387
|
+
"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.",
|
|
3388
|
+
completeSectionValidationRequestInput.shape,
|
|
3389
|
+
async (c, a) => ok("Section validation request completed.", await data(
|
|
3390
|
+
c,
|
|
3391
|
+
"POST",
|
|
3392
|
+
`/projects/${s(a.projectId)}/section-evidence/implementation-requests/${s(a.requestId)}/complete`,
|
|
3393
|
+
{ manifestId: a.manifestId, validationRunId: a.validationRunId }
|
|
3394
|
+
))
|
|
3395
|
+
),
|
|
3396
|
+
def(
|
|
3397
|
+
"fail_section_validation_request",
|
|
3398
|
+
"Fail a Section validation request",
|
|
3399
|
+
"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.",
|
|
3400
|
+
failSectionValidationRequestInput.shape,
|
|
3401
|
+
async (c, a) => ok("Section validation request failed.", await data(
|
|
3402
|
+
c,
|
|
3403
|
+
"POST",
|
|
3404
|
+
`/projects/${s(a.projectId)}/section-evidence/implementation-requests/${s(a.requestId)}/fail`,
|
|
3405
|
+
{
|
|
3406
|
+
errorCode: a.errorCode,
|
|
3407
|
+
errorMessage: a.errorMessage,
|
|
3408
|
+
providerRunId: a.providerRunId,
|
|
3409
|
+
providerRunUrl: a.providerRunUrl
|
|
3410
|
+
}
|
|
3411
|
+
))
|
|
3412
|
+
),
|
|
3413
|
+
def(
|
|
3414
|
+
"submit_section_manifest",
|
|
3415
|
+
"Submit a Section implementation manifest",
|
|
3416
|
+
"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.",
|
|
3417
|
+
submitSectionManifestInput.shape,
|
|
3418
|
+
async (c, a) => ok("Section implementation manifest recorded.", await data(
|
|
3419
|
+
c,
|
|
3420
|
+
"POST",
|
|
3421
|
+
`/projects/${s(a.projectId)}/section-evidence/sections/${s(a.sectionId)}/versions/${a.version}/manifests`,
|
|
3422
|
+
{
|
|
3423
|
+
requestId: a.requestId,
|
|
3424
|
+
apiId: a.apiId,
|
|
3425
|
+
schemaHash: a.schemaHash,
|
|
3426
|
+
commitSha: a.commitSha,
|
|
3427
|
+
loader: a.loader,
|
|
3428
|
+
previewAdapter: a.previewAdapter,
|
|
3429
|
+
nativeViewports: a.nativeViewports
|
|
3430
|
+
}
|
|
3431
|
+
))
|
|
3432
|
+
),
|
|
3433
|
+
def(
|
|
3434
|
+
"submit_section_validation",
|
|
3435
|
+
"Submit a Section validation result",
|
|
3436
|
+
"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.",
|
|
3437
|
+
submitSectionValidationInput.shape,
|
|
3438
|
+
async (c, a) => ok("Section validation result recorded.", await data(
|
|
3439
|
+
c,
|
|
3440
|
+
"POST",
|
|
3441
|
+
`/projects/${s(a.projectId)}/section-evidence/sections/${s(a.sectionId)}/versions/${a.version}/validations`,
|
|
3442
|
+
{
|
|
3443
|
+
requestId: a.requestId,
|
|
3444
|
+
manifestId: a.manifestId,
|
|
3445
|
+
status: a.status,
|
|
3446
|
+
fixtureHash: a.fixtureHash,
|
|
3447
|
+
evidenceDigest: a.evidenceDigest,
|
|
3448
|
+
appShell: a.appShell,
|
|
3449
|
+
viewportResults: a.viewportResults
|
|
3450
|
+
}
|
|
3451
|
+
))
|
|
3452
|
+
),
|
|
3244
3453
|
def(
|
|
3245
3454
|
"get_deploy_status",
|
|
3246
3455
|
"Get deploy/build status",
|
|
@@ -3296,11 +3505,13 @@ function buildToolDefs(deps) {
|
|
|
3296
3505
|
name: "create_page",
|
|
3297
3506
|
config: {
|
|
3298
3507
|
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,
|
|
3508
|
+
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
3509
|
inputSchema: createPageInput.shape
|
|
3301
3510
|
},
|
|
3302
3511
|
handler: guard(
|
|
3303
3512
|
async (args) => withClient(async (client) => {
|
|
3513
|
+
const dupes = duplicateKeys(flatFieldKeys(args.fields));
|
|
3514
|
+
if (dupes.length > 0) return fail(duplicateKeyFailure(dupes));
|
|
3304
3515
|
const page = await client.createPage({
|
|
3305
3516
|
title: args.title,
|
|
3306
3517
|
slug: args.slug,
|
|
@@ -3326,6 +3537,8 @@ function buildToolDefs(deps) {
|
|
|
3326
3537
|
},
|
|
3327
3538
|
handler: guard(
|
|
3328
3539
|
async (args) => withClient(async (client) => {
|
|
3540
|
+
const modelDupes = duplicateKeys(flatFieldKeys(args.fields));
|
|
3541
|
+
if (modelDupes.length > 0) return fail(duplicateKeyFailure(modelDupes));
|
|
3329
3542
|
const model = await client.createModel({
|
|
3330
3543
|
name: args.name,
|
|
3331
3544
|
slug: args.slug,
|
|
@@ -3350,9 +3563,8 @@ function buildToolDefs(deps) {
|
|
|
3350
3563
|
handler: guard(
|
|
3351
3564
|
async (args) => withClient(async (client) => {
|
|
3352
3565
|
const model = await client.getModel(args.modelId);
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
}
|
|
3566
|
+
const dupes = duplicateKeys([...flatFieldKeys(model.fields), args.key]);
|
|
3567
|
+
if (dupes.length > 0) return fail(duplicateKeyFailure(dupes));
|
|
3356
3568
|
const updated = await client.updateModel(args.modelId, {
|
|
3357
3569
|
fields: [...model.fields, toField(args)]
|
|
3358
3570
|
});
|
|
@@ -3596,13 +3808,13 @@ function buildToolDefs(deps) {
|
|
|
3596
3808
|
{
|
|
3597
3809
|
name: "get_layout",
|
|
3598
3810
|
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.",
|
|
3811
|
+
title: "Get the project or page Layout",
|
|
3812
|
+
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
3813
|
inputSchema: getLayoutInput.shape
|
|
3602
3814
|
},
|
|
3603
3815
|
handler: guard(async (args) => withClient(async (client) => {
|
|
3604
3816
|
const layout = await client.getManagedLayout(args);
|
|
3605
|
-
return ok(`${layout.scope === "global" ? "Global" : `Page ${layout.pageSlug}`} Layout draft at revision ${layout.revision}.`, layout);
|
|
3817
|
+
return ok(`${layout.scope === "global" ? "Global" : `Page ${layout.pageSlug}`} Layout ${args.copy === "published" ? "PUBLISHED copy" : "draft"} at revision ${layout.revision}.`, layout);
|
|
3606
3818
|
}))
|
|
3607
3819
|
},
|
|
3608
3820
|
{
|
|
@@ -3982,17 +4194,65 @@ renders. Three consequences, each load-bearing:
|
|
|
3982
4194
|
d. update_page status 'published' \u2014 the canvas binds the PUBLISHED copy
|
|
3983
4195
|
2. A value that renders in more than one place stays uneditable on the canvas (deliberate:
|
|
3984
4196
|
binding it would edit all of them at once). It remains editable in the side panel.
|
|
3985
|
-
3.
|
|
4197
|
+
3. Make the chrome ITSELF editable by speaking the layout grammar: the nav/footer
|
|
4198
|
+
elements declare \`data-bcms-layout-section="navigation"\` / \`"footer"\`, and each
|
|
4199
|
+
CMS-backed text inside them a \`data-bcms-layout-field="layout:<sectionId>:<fieldId>"\`
|
|
4200
|
+
marker (fieldId is the field's REAL id, verbatim \u2014 production layout ids are
|
|
4201
|
+
section-prefixed and dotted, e.g. \`footer.tagline\`, so the marker reads
|
|
4202
|
+
\`layout:footer:footer.tagline\`) \u2014 the canvas then gives them hover chrome, the
|
|
4203
|
+
Layout side panel, and
|
|
4204
|
+
double-click editing, writing to the project Layout store (never the page).
|
|
4205
|
+
The address reaches INSIDE structured fields by walking the schema: a group's text
|
|
4206
|
+
sub appends its slug (\`layout:navigation:navigation.cta.label\`), a repeater row its
|
|
4207
|
+
STORED index then the slug (\`layout:navigation:navigation.links.0.label\`), nesting
|
|
4208
|
+
as deep as the schema goes (\`layout:footer:footer.link-groups.0.links.1.label\`).
|
|
4209
|
+
Three rules, each one a measured defect when broken:
|
|
4210
|
+
a. a field rendered by TWO elements gets a marker on only one \u2014 the editor binds a
|
|
4211
|
+
twice-carried field to neither;
|
|
4212
|
+
b. PROVENANCE \u2014 mark an element only when the LAYOUT supplied its value; a marker
|
|
4213
|
+
over a fallback/singleton-sourced string opens an editor for a row that does not
|
|
4214
|
+
exist;
|
|
4215
|
+
c. row markers use the value's STORED index (a row you filtered out of the render
|
|
4216
|
+
still occupies its slot), or the edit lands on the wrong row.
|
|
4217
|
+
Text/longtext leaves edit inline; link/select/image leaves are side-panel-only by
|
|
4218
|
+
design (their formats need a real control). THE DOCTRINE: every string a marketer
|
|
4219
|
+
can see must be addressable \u2014 no marker means read-only on the canvas, so an
|
|
4220
|
+
unmarked CMS-backed string is a defect, not a style choice.
|
|
4221
|
+
4. Keep chrome semantic \u2014 \`<nav>\`, \`<footer>\`, page content inside \`<main>\`, mastheads
|
|
3986
4222
|
as a top-level \`<header>\`. Chrome is edited through the project Layout, not the page,
|
|
3987
4223
|
and semantic landmarks are how the editor keeps a nav edit from being written into page
|
|
3988
4224
|
content. Div-built chrome outside \`<main>\` is still excluded; div-built chrome with no
|
|
3989
4225
|
\`<main>\` anywhere loses that protection.
|
|
3990
4226
|
|
|
4227
|
+
**Structural drafts can render on the real site too \u2014 the draft bridge.** Wrap your page's
|
|
4228
|
+
blocks in \`BcmsDraftBridge\` (\`@bettercms-ai/next/draft-bridge\`) instead of calling
|
|
4229
|
+
\`BcmsBlocks\` directly: standalone it renders identically, and inside the visual editor it
|
|
4230
|
+
receives the DRAFT block tree over a same-origin postMessage handshake and re-renders it with
|
|
4231
|
+
the site's own components \u2014 so adding, removing or reordering sections previews in the site's
|
|
4232
|
+
real design instead of the platform's approximate renderer. Sites without the bridge keep the
|
|
4233
|
+
approximate fallback; unpublished ROUTES always fall back (a static build has no file to frame).
|
|
4234
|
+
|
|
3991
4235
|
**Hosting decides whether a canvas exists at all.** A site deployed here is framed through a
|
|
3992
4236
|
same-origin proxy \u2014 that is what the canvas requires. A site hosted elsewhere (your own Vercel,
|
|
3993
4237
|
your own server) has NO canvas today: the SDK's draft mode with \`stega: true\` embeds invisible
|
|
3994
4238
|
per-field provenance in fetched strings, which prepares the content for editing surfaces, but do
|
|
3995
4239
|
not promise a canvas for an externally-hosted site.
|
|
4240
|
+
|
|
4241
|
+
**\xA712 \u2014 RECEIPTS: a claim about published state needs a read of the PUBLISHED copy.** The
|
|
4242
|
+
doctrine this encodes cost a real incident (FLO-1188): a publish was verified against the
|
|
4243
|
+
draft for ~50 minutes because the reader silently returned the draft, and every check was
|
|
4244
|
+
green on the wrong document. Three rules, none optional:
|
|
4245
|
+
1. NEVER verify a write by re-reading the store you wrote. Draft writes verify against
|
|
4246
|
+
the draft; a PUBLISH claim verifies ONLY via \`get_layout copy:'published'\` (or the
|
|
4247
|
+
entry/page's published copy) \u2014 and check the response's \`copy\` echo says
|
|
4248
|
+
'published'. A reader that ignores your copy selector hands you the draft and a
|
|
4249
|
+
false green; the echo is how you catch it.
|
|
4250
|
+
2. Publish and deploy are SEPARATE claims. "Published" means the published copy changed;
|
|
4251
|
+
the LIVE SITE changes only after its next deploy/rebuild. Never report "it's live"
|
|
4252
|
+
from a publish receipt \u2014 fetch the live URL (cache-busted) for that claim.
|
|
4253
|
+
3. A tool param the schema does not declare is SILENTLY DROPPED, not rejected. If a
|
|
4254
|
+
call's behavior doesn't change when you change a param, treat the param as dead and
|
|
4255
|
+
verify through an independent channel before trusting any result built on it.
|
|
3996
4256
|
`;
|
|
3997
4257
|
|
|
3998
4258
|
// src/prompts.ts
|
|
@@ -4434,7 +4694,7 @@ On 401/403, the MCP key needs (re)authorizing.`
|
|
|
4434
4694
|
|
|
4435
4695
|
// src/server.ts
|
|
4436
4696
|
var SERVER_NAME = "bettercms";
|
|
4437
|
-
var SERVER_VERSION = "1.
|
|
4697
|
+
var SERVER_VERSION = "1.4.0";
|
|
4438
4698
|
var SERVER_DISPLAY = {
|
|
4439
4699
|
title: "BetterCMS",
|
|
4440
4700
|
websiteUrl: "https://bettercms.ai",
|
|
@@ -4446,7 +4706,10 @@ var SERVER_DISPLAY = {
|
|
|
4446
4706
|
function buildServer(deps) {
|
|
4447
4707
|
const server = new McpServer(
|
|
4448
4708
|
{ name: SERVER_NAME, version: SERVER_VERSION, ...SERVER_DISPLAY },
|
|
4449
|
-
{
|
|
4709
|
+
{
|
|
4710
|
+
capabilities: { tools: {}, prompts: {}, resources: {} },
|
|
4711
|
+
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."
|
|
4712
|
+
}
|
|
4450
4713
|
);
|
|
4451
4714
|
server.registerResource(
|
|
4452
4715
|
"schema-playbook",
|