@diviops/mcp-server 1.5.6 → 1.5.8
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 +9 -5
- package/dist/envelope.d.ts +14 -10
- package/dist/index.js +238 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# DiviOps MCP Server
|
|
2
2
|
|
|
3
|
-
**AI
|
|
3
|
+
**An AI harness for WordPress site authoring — Divi-native today, WordPress-wide by design.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The Node.js MCP server inside the DiviOps harness. It gives Claude Code, Claude Desktop, and other MCP clients a typed control layer over WordPress site state, dispatching to the DiviOps Agent plugin for Divi 5 page authoring, SCF and CPT data models, design tokens, presets, library and Theme Builder templates, site audits, and safe WP-CLI passthrough. Pairs with the `divi-5-builder` Claude skill so the agent applies Divi's block format and design rules correctly.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
Claude Code <-> MCP Server (stdio) <-> WordPress REST API <-> DiviOps Agent plugin
|
|
@@ -57,14 +57,14 @@ Claude orchestrates a few tool calls in sequence:
|
|
|
57
57
|
1. `diviops_global_color_list` — discovers your brand palette.
|
|
58
58
|
2. `diviops_template_list` / `diviops_template_get` — pulls a verified hero template that matches the request.
|
|
59
59
|
3. `diviops_page_create` — creates `Spring Launch` as a draft with the hero block markup.
|
|
60
|
-
4. `diviops_validate_blocks` — confirms the markup is well-formed before save.
|
|
61
|
-
5. `diviops_render_preview` — returns the rendered HTML so you can verify before publishing.
|
|
60
|
+
4. `diviops_validate_blocks` — confirms the markup is well-formed before save. Accepts inline `content` or a `page_id` to validate already-saved markup.
|
|
61
|
+
5. `diviops_render_preview` — returns the rendered HTML so you can verify before publishing. Accepts inline `content` or a `page_id` to preview an existing page.
|
|
62
62
|
|
|
63
63
|
The skill enforces the Divi block format, the design system, and the response contract throughout — you stay at the prompt level.
|
|
64
64
|
|
|
65
65
|
## Tools at a glance
|
|
66
66
|
|
|
67
|
-
The server exposes **
|
|
67
|
+
The server exposes **70 tools** across the categories below. Each category links to representative tools; the full table lives in [server-reference.md](../docs/server-reference.md).
|
|
68
68
|
|
|
69
69
|
| Category | Use case | Tool prefixes |
|
|
70
70
|
|----------|----------|---------------|
|
|
@@ -108,6 +108,10 @@ Tools return a standardized envelope. The shape lets clients branch on `ok` and
|
|
|
108
108
|
|
|
109
109
|
Namespaces extend the vocabulary using the `<namespace>.<reason>` convention — e.g. `meta_wp_cli.command_failed`, `scf.not_configured`, `preset.bucket_mismatch`, `variable.customizer_default_immutable`. Namespace-prefixed codes carry structured `error.data` documenting the failure (exit codes, conflicting fields, reference counts, etc.). Per-tool descriptions name the codes each tool emits and the `error.data` shape that accompanies them.
|
|
110
110
|
|
|
111
|
+
### Per-tool `error.data` extensions
|
|
112
|
+
|
|
113
|
+
Some tools attach a structured `error.data` payload alongside the `code`/`message`/`hint` envelope — e.g. `meta_wp_cli` carries `{ exit_code, stdout, stderr }` on `meta_wp_cli.command_failed`, `global_color_delete` carries `{ id, ref_count, locations[], scan_truncated, scanned_posts[] }` on `conflict`, and conflict-class adopters across `canvas_*`/`library_*`/`variable_*` echo the conflicting fields. The shape is per-tool and documented in each tool's description prose, not in this canonical envelope summary. The summary stays terse because (a) most tools never emit `error.data` and advertising it universally would be misleading, and (b) the per-tool shape diverges and `data?: unknown` would be information-free. The runtime mechanism is `withCode`'s 4th `data` argument (server-local) / `envelope_error()`'s `$data` parameter (plugin-routed); both flow through `wrapResponse` to land on `error.data`.
|
|
114
|
+
|
|
111
115
|
### `dry_run` plan shape
|
|
112
116
|
|
|
113
117
|
Every write tool accepts `dry_run: boolean` (default `false`). When `true`, the response carries a uniform plan shape and no state is mutated:
|
package/dist/envelope.d.ts
CHANGED
|
@@ -24,17 +24,21 @@ export type DiviopsErrorBody = {
|
|
|
24
24
|
message: string;
|
|
25
25
|
hint?: string;
|
|
26
26
|
/**
|
|
27
|
-
* Optional structured payload attached to the error envelope.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
27
|
+
* Optional structured payload attached to the error envelope. Carried
|
|
28
|
+
* via `withCode`'s 4th `data` argument (server-local) or the plugin's
|
|
29
|
+
* `envelope_error()` `$data` parameter (plugin-routed) — e.g.
|
|
30
|
+
* `meta_wp_cli` emits `{ exit_code, stdout, stderr }`,
|
|
31
|
+
* `global_color_delete` emits `{ id, ref_count, locations[], ... }` on
|
|
32
|
+
* `conflict`, and conflict-class adopters echo conflicting fields.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* The shape is per-tool and documented in each tool's description
|
|
35
|
+
* prose, not in this canonical type. The type stays `unknown` because
|
|
36
|
+
* (a) most tools never emit `error.data` so advertising it universally
|
|
37
|
+
* would be misleading, and (b) the per-tool shape diverges and a
|
|
38
|
+
* concrete union here would be information-free for consumers.
|
|
39
|
+
*
|
|
40
|
+
* See `diviops-server/README.md` "Per-tool error.data extensions" for
|
|
41
|
+
* the codified convention.
|
|
38
42
|
*/
|
|
39
43
|
data?: unknown;
|
|
40
44
|
};
|
package/dist/index.js
CHANGED
|
@@ -443,7 +443,7 @@ registerPluginTool("diviops_global_color_delete", {
|
|
|
443
443
|
return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_global_color_delete") }] };
|
|
444
444
|
});
|
|
445
445
|
registerPluginTool("diviops_global_font_list", {
|
|
446
|
-
description: "
|
|
446
|
+
description: "List the DiviOps-managed global fonts registered under `et_global_data.global_fonts`. ALWAYS returns the normalized shape `{ count: number, fonts: { <gfid>: <record>, ... } }` — even on empty substrates (count:0, fonts:{}), never bare `false`. Distinct from the variable-manager font tokens (`gvid-*` under `et_global_data.global_variables.fonts`, surfaced via `diviops_variable_list({type:\"fonts\"})`) — `global_font_*` is the DiviOps-controlled font catalog presets bind to via canonical `gfid-` slugs. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }.",
|
|
447
447
|
annotations: { idempotentHint: true },
|
|
448
448
|
_meta: { idempotent: "true" },
|
|
449
449
|
}, async () => {
|
|
@@ -454,6 +454,158 @@ registerPluginTool("diviops_global_font_list", {
|
|
|
454
454
|
],
|
|
455
455
|
};
|
|
456
456
|
});
|
|
457
|
+
registerPluginTool("diviops_global_font_create", {
|
|
458
|
+
description: "Create a new global font in DiviOps's registry under `et_global_data.global_fonts`. Mints a fresh `gfid-<uuid>` if `id` is omitted; otherwise uses the supplied id (must match `gfid-[0-9a-z-]{1,80}`; auto-prefixes `gfid-` if missing). Strict create — collision on existing id returns `conflict` (HTTP 409) with `error.data = { id, existing }`; use diviops_global_font_update to modify an existing record. Stored shape: `{ family, source, weights[], subsets[], label, fallback, status, lastUpdated }`. Required: `family` (CSS family name, e.g. \"Sora\") + `source` (one of `google`/`system`/`custom`). Distinct from `diviops_variable_create({type:\"fonts\"})` which writes `gvid-*` font tokens to the variable manager — `global_font_*` is the DiviOps catalog presets bind via `gfid-` slugs. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; input-shape rejections (malformed id, invalid source enum, non-array weights/subsets, missing required `family`/`source` for a new entry) collapse onto `invalid_input` with structured `error.data`." +
|
|
459
|
+
DRY_RUN_DESC_SUFFIX,
|
|
460
|
+
inputSchema: {
|
|
461
|
+
family: z
|
|
462
|
+
.string()
|
|
463
|
+
.describe('CSS font family name (e.g. "Sora", "Inter", "JetBrains Mono"). Stored as the bare name; consumers wrap in single quotes when emitting CSS.'),
|
|
464
|
+
source: z
|
|
465
|
+
.enum(["google", "system", "custom"])
|
|
466
|
+
.describe('Font source: "google" (Google Fonts), "system" (system/web-safe families), or "custom" (self-hosted/CDN).'),
|
|
467
|
+
id: z
|
|
468
|
+
.string()
|
|
469
|
+
.optional()
|
|
470
|
+
.describe('Optional explicit gfid (e.g. "gfid-oa-sora"). Auto-prefixes `gfid-` if missing; must match `[0-9a-z-]{1,80}` after the prefix. Omit to mint a fresh `gfid-<uuid>`.'),
|
|
471
|
+
weights: z
|
|
472
|
+
.array(z.union([z.number(), z.string()]))
|
|
473
|
+
.optional()
|
|
474
|
+
.describe('Font weights to load. Accepts integers (100,200,...,900) or keyword strings ("normal","bold","lighter","bolder"). Defaults to []. Drives Google Fonts URL composition + loader hints.'),
|
|
475
|
+
subsets: z
|
|
476
|
+
.array(z.string())
|
|
477
|
+
.optional()
|
|
478
|
+
.describe('Character subsets (e.g. ["latin","latin-ext"]). Defaults to []. Permissive — not allowlisted server-side (Google adds new subsets regularly).'),
|
|
479
|
+
label: z
|
|
480
|
+
.string()
|
|
481
|
+
.optional()
|
|
482
|
+
.describe('Human-readable display label. Defaults to the family name.'),
|
|
483
|
+
fallback: z
|
|
484
|
+
.string()
|
|
485
|
+
.optional()
|
|
486
|
+
.describe('CSS fallback chain appended after the family name (e.g. "sans-serif", "Georgia, serif"). Defaults to empty.'),
|
|
487
|
+
status: z
|
|
488
|
+
.enum(["active", "archived"])
|
|
489
|
+
.optional()
|
|
490
|
+
.default("active")
|
|
491
|
+
.describe('Font status — "active" (visible) or "archived" (hidden but preserved).'),
|
|
492
|
+
dry_run: DRY_RUN_FIELD,
|
|
493
|
+
},
|
|
494
|
+
annotations: { idempotentHint: false },
|
|
495
|
+
_meta: { idempotent: "false" },
|
|
496
|
+
}, async ({ family, source, id, weights, subsets, label, fallback, status, dry_run }) => {
|
|
497
|
+
const body = { family, source };
|
|
498
|
+
if (id !== undefined)
|
|
499
|
+
body.id = id;
|
|
500
|
+
if (weights !== undefined)
|
|
501
|
+
body.weights = weights;
|
|
502
|
+
if (subsets !== undefined)
|
|
503
|
+
body.subsets = subsets;
|
|
504
|
+
if (label !== undefined)
|
|
505
|
+
body.label = label;
|
|
506
|
+
if (fallback !== undefined)
|
|
507
|
+
body.fallback = fallback;
|
|
508
|
+
if (status)
|
|
509
|
+
body.status = status;
|
|
510
|
+
if (dry_run)
|
|
511
|
+
body.dry_run = true;
|
|
512
|
+
const result = await wp.requestEnveloped("/global-font/create", {
|
|
513
|
+
method: "POST",
|
|
514
|
+
body,
|
|
515
|
+
});
|
|
516
|
+
return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_global_font_create") }] };
|
|
517
|
+
});
|
|
518
|
+
registerPluginTool("diviops_global_font_update", {
|
|
519
|
+
description: "Update an existing global font by gfid. Strict update — `id` must reference an existing record; unknown gfid returns `not_found` (HTTP 404) with `error.data = { id }` (unlike `diviops_global_color_update`'s merge-mode semantics). Partial: only supplied fields are written, omitted fields preserved; `lastUpdated` bumped on every write. To rename a font's family slug, use diviops_global_font_delete + diviops_global_font_create — `family` itself can be updated in place but the `gfid` identity is immutable via this tool. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; malformed id charset/length returns 'invalid_input'; missing `id` returns 'invalid_input' with `error.data.missing = \"id\"`; invalid source enum / non-array weights / non-array subsets return 'invalid_input' with structured `error.data`." +
|
|
520
|
+
DRY_RUN_DESC_SUFFIX,
|
|
521
|
+
inputSchema: {
|
|
522
|
+
id: z
|
|
523
|
+
.string()
|
|
524
|
+
.describe('Global font ID (e.g. "gfid-oa-sora"). Required. Get from diviops_global_font_list.'),
|
|
525
|
+
family: z
|
|
526
|
+
.string()
|
|
527
|
+
.optional()
|
|
528
|
+
.describe('New CSS family name. Omit to keep existing.'),
|
|
529
|
+
source: z
|
|
530
|
+
.enum(["google", "system", "custom"])
|
|
531
|
+
.optional()
|
|
532
|
+
.describe('New source. Omit to keep existing.'),
|
|
533
|
+
weights: z
|
|
534
|
+
.array(z.union([z.number(), z.string()]))
|
|
535
|
+
.optional()
|
|
536
|
+
.describe('New weights array. Omit to keep existing; pass [] to clear.'),
|
|
537
|
+
subsets: z
|
|
538
|
+
.array(z.string())
|
|
539
|
+
.optional()
|
|
540
|
+
.describe('New subsets array. Omit to keep existing; pass [] to clear.'),
|
|
541
|
+
label: z
|
|
542
|
+
.string()
|
|
543
|
+
.optional()
|
|
544
|
+
.describe('New label. Pass empty string to clear.'),
|
|
545
|
+
fallback: z
|
|
546
|
+
.string()
|
|
547
|
+
.optional()
|
|
548
|
+
.describe('New CSS fallback chain. Pass empty string to clear.'),
|
|
549
|
+
status: z
|
|
550
|
+
.enum(["active", "archived"])
|
|
551
|
+
.optional()
|
|
552
|
+
.describe('New status — "active" or "archived". Omit to keep existing.'),
|
|
553
|
+
dry_run: DRY_RUN_FIELD,
|
|
554
|
+
},
|
|
555
|
+
annotations: { idempotentHint: false },
|
|
556
|
+
_meta: { idempotent: "conditional" },
|
|
557
|
+
}, async ({ id, family, source, weights, subsets, label, fallback, status, dry_run }) => {
|
|
558
|
+
const body = { id };
|
|
559
|
+
if (family !== undefined)
|
|
560
|
+
body.family = family;
|
|
561
|
+
if (source !== undefined)
|
|
562
|
+
body.source = source;
|
|
563
|
+
if (weights !== undefined)
|
|
564
|
+
body.weights = weights;
|
|
565
|
+
if (subsets !== undefined)
|
|
566
|
+
body.subsets = subsets;
|
|
567
|
+
if (label !== undefined)
|
|
568
|
+
body.label = label;
|
|
569
|
+
if (fallback !== undefined)
|
|
570
|
+
body.fallback = fallback;
|
|
571
|
+
if (status)
|
|
572
|
+
body.status = status;
|
|
573
|
+
if (dry_run)
|
|
574
|
+
body.dry_run = true;
|
|
575
|
+
const result = await wp.requestEnveloped("/global-font/update", {
|
|
576
|
+
method: "POST",
|
|
577
|
+
body,
|
|
578
|
+
});
|
|
579
|
+
return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_global_font_update") }] };
|
|
580
|
+
});
|
|
581
|
+
registerPluginTool("diviops_global_font_delete", {
|
|
582
|
+
description: "Delete a global font from the registry by gfid. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }. Live-reference detection uses parse_blocks over post_content across pages / TB layouts / library / canvas + the preset registry (parallel to diviops_variable_delete / diviops_global_color_delete) — MCP-authored content is detected reliably. Returns code 'conflict' (HTTP 409) when references exist with `error.data = { id, ref_count, locations[], scan_truncated, scanned_posts }`. Pass `force: true` to override; orphan refs will fall back to the browser default until pages are re-authored. Missing gfid returns 'not_found' (HTTP 404) with `error.data = { id }`. Malformed gfid (empty or missing `gfid-` prefix) returns 'invalid_input'. Unlike global_color_delete, no customizer-bound `gfid-*` defaults exist to protect — the Divi customizer-bound font defaults live in `heading_font` / `body_font` plain WP options, not this registry." +
|
|
583
|
+
DRY_RUN_DESC_SUFFIX,
|
|
584
|
+
inputSchema: {
|
|
585
|
+
id: z
|
|
586
|
+
.string()
|
|
587
|
+
.describe('Global font ID to delete (must start with "gfid-").'),
|
|
588
|
+
force: z
|
|
589
|
+
.boolean()
|
|
590
|
+
.optional()
|
|
591
|
+
.default(false)
|
|
592
|
+
.describe("If true, delete even when live references exist."),
|
|
593
|
+
dry_run: DRY_RUN_FIELD,
|
|
594
|
+
},
|
|
595
|
+
annotations: { idempotentHint: true },
|
|
596
|
+
_meta: { idempotent: "true" },
|
|
597
|
+
}, async ({ id, force, dry_run }) => {
|
|
598
|
+
const body = { id };
|
|
599
|
+
if (force)
|
|
600
|
+
body.force = true;
|
|
601
|
+
if (dry_run)
|
|
602
|
+
body.dry_run = true;
|
|
603
|
+
const result = await wp.requestEnveloped("/global-font/delete", {
|
|
604
|
+
method: "POST",
|
|
605
|
+
body,
|
|
606
|
+
});
|
|
607
|
+
return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_global_font_delete") }] };
|
|
608
|
+
});
|
|
457
609
|
registerPluginTool("diviops_meta_find_icon", {
|
|
458
610
|
description: "Search for icons by keyword. Returns matching icons with unicode, type (fa/divi), and weight. Use the returned unicode/type/weight in Blurb icon or Icon module attributes. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }.",
|
|
459
611
|
inputSchema: {
|
|
@@ -512,16 +664,38 @@ registerPluginTool("diviops_page_update_content", {
|
|
|
512
664
|
};
|
|
513
665
|
});
|
|
514
666
|
registerPluginTool("diviops_render_preview", {
|
|
515
|
-
description: "Render Divi block markup to HTML.
|
|
667
|
+
description: "Render Divi block markup to HTML. Accepts EITHER inline `content` (string of block markup) OR `page_id` (loads `post_content` from the DB, requires edit_post capability on the page — useful for previewing shipped pages without round-tripping the markup blob). Provide exactly one. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { rendered_html: string }. Errors map to `invalid_input` (neither/both supplied, or invalid page_id), `forbidden` (caller lacks edit_post on the page), `not_found` (page_id does not exist), or `divi_error` (parser/render exception, with truncated message and full detail in `error.data.detail`).",
|
|
516
668
|
inputSchema: {
|
|
517
|
-
content: z
|
|
669
|
+
content: z
|
|
670
|
+
.string()
|
|
671
|
+
.optional()
|
|
672
|
+
.describe("Divi block markup to render to HTML. Provide exactly one of {content, page_id}."),
|
|
673
|
+
page_id: z
|
|
674
|
+
.number()
|
|
675
|
+
.int()
|
|
676
|
+
.optional()
|
|
677
|
+
.describe("WordPress post/page ID to read post_content from the DB. Requires edit_post capability on the page. Provide exactly one of {content, page_id}."),
|
|
518
678
|
},
|
|
519
679
|
annotations: { idempotentHint: true },
|
|
520
680
|
_meta: { idempotent: "true" },
|
|
521
|
-
}, async ({ content }) => {
|
|
681
|
+
}, async ({ content, page_id }) => {
|
|
682
|
+
if (page_id !== undefined) {
|
|
683
|
+
try {
|
|
684
|
+
requireCapability("validate_render_by_page_id");
|
|
685
|
+
}
|
|
686
|
+
catch (e) {
|
|
687
|
+
if (e instanceof MissingCapabilityError) {
|
|
688
|
+
return {
|
|
689
|
+
content: [{ type: "text", text: e.message }],
|
|
690
|
+
isError: true,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
throw e;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
522
696
|
const result = await wp.requestEnveloped("/render", {
|
|
523
697
|
method: "POST",
|
|
524
|
-
body: { content },
|
|
698
|
+
body: { content, page_id },
|
|
525
699
|
});
|
|
526
700
|
return {
|
|
527
701
|
content: [
|
|
@@ -530,16 +704,38 @@ registerPluginTool("diviops_render_preview", {
|
|
|
530
704
|
};
|
|
531
705
|
});
|
|
532
706
|
registerPluginTool("diviops_validate_blocks", {
|
|
533
|
-
description: "Validate Divi block markup before saving. Checks structure (malformed comments, unknown blocks, missing builderVersion), required attributes (layout display on containers), and known pitfalls (button padding path, icon.enable, gradient enabled/positions). Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { valid: bool, total_blocks: number, errors: Finding[], warnings: Finding[] } where each Finding is { block, index, code, message, path? }. Note: shape errors detected in the markup surface as success-branch `data.errors[]` entries (NOT `validation_failed` envelopes) — the findings array is the payload, not an error. The envelope's error branch fires only for tool-level failures (`invalid_input` for
|
|
707
|
+
description: "Validate Divi block markup before saving. Accepts EITHER inline `content` (string of block markup) OR `page_id` (loads `post_content` from the DB, requires edit_post capability on the page — useful for regression checks on shipped pages without round-tripping the markup blob). Provide exactly one. Checks structure (malformed comments, unknown blocks, missing builderVersion), required attributes (layout display on containers), and known pitfalls (button padding path, icon.enable, gradient enabled/positions). Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { valid: bool, total_blocks: number, errors: Finding[], warnings: Finding[] } where each Finding is { block, index, code, message, path? }. Note: shape errors detected in the markup surface as success-branch `data.errors[]` entries (NOT `validation_failed` envelopes) — the findings array is the payload, not an error. The envelope's error branch fires only for tool-level failures (`invalid_input` for neither/both supplied or invalid page_id; `forbidden` for missing edit_post; `not_found` for unknown page_id; `divi_error` for an exception in the walker).",
|
|
534
708
|
inputSchema: {
|
|
535
|
-
content: z
|
|
709
|
+
content: z
|
|
710
|
+
.string()
|
|
711
|
+
.optional()
|
|
712
|
+
.describe("Divi block markup to validate. Provide exactly one of {content, page_id}."),
|
|
713
|
+
page_id: z
|
|
714
|
+
.number()
|
|
715
|
+
.int()
|
|
716
|
+
.optional()
|
|
717
|
+
.describe("WordPress post/page ID to read post_content from the DB. Requires edit_post capability on the page. Provide exactly one of {content, page_id}."),
|
|
536
718
|
},
|
|
537
719
|
annotations: { idempotentHint: true },
|
|
538
720
|
_meta: { idempotent: "true" },
|
|
539
|
-
}, async ({ content }) => {
|
|
721
|
+
}, async ({ content, page_id }) => {
|
|
722
|
+
if (page_id !== undefined) {
|
|
723
|
+
try {
|
|
724
|
+
requireCapability("validate_render_by_page_id");
|
|
725
|
+
}
|
|
726
|
+
catch (e) {
|
|
727
|
+
if (e instanceof MissingCapabilityError) {
|
|
728
|
+
return {
|
|
729
|
+
content: [{ type: "text", text: e.message }],
|
|
730
|
+
isError: true,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
throw e;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
540
736
|
const result = await wp.requestEnveloped("/validate/blocks", {
|
|
541
737
|
method: "POST",
|
|
542
|
-
body: { content },
|
|
738
|
+
body: { content, page_id },
|
|
543
739
|
});
|
|
544
740
|
return {
|
|
545
741
|
content: [
|
|
@@ -1490,13 +1686,13 @@ registerPluginTool("diviops_tb_layout_update", {
|
|
|
1490
1686
|
};
|
|
1491
1687
|
});
|
|
1492
1688
|
registerPluginTool("diviops_tb_template_create", {
|
|
1493
|
-
description: "Create a Theme Builder template with custom header and/or footer. Automatically creates layout posts, sets conditions, and links to Theme Builder. Returns the standardized envelope { ok, data?, error: { code, message, hint? } };
|
|
1689
|
+
description: "Create a Theme Builder template with custom header and/or footer. Automatically creates layout posts, sets conditions, and links to Theme Builder. Pass condition=\"default\" (case-insensitive) or an empty string to register the template as the catch-all Default Website Template — the route writes the `_et_default = '1'` flag with an empty `_et_use_on`, matching the meta shape Divi's TB router gates the default route on; any other condition string lands in `_et_use_on` unchanged. Default Website Template is a singleton scoped to the active Theme Builder master: if the active master's `_et_template` linked list already names an et_template carrying `_et_default = '1'` (regardless of `_et_enabled` status — the router resolves by linked-list position before checking the enable-gate, so a disabled existing default linked ahead of the new one would still shadow it), the route rejects with code `tb_template.default_already_exists` (HTTP 409) and `error.data.existing_default_id` + `error.data.master_post_id`. Templates outside the active master's linked list (orphan defaults, library-cloned-master defaults) cannot shadow the router's pick and DO NOT block creation. Caller resolves a real conflict by trashing the existing default (diviops_tb_template_trash) or pinning this template to a specific condition; the route never silently flips the existing default's flag or proceeds with non-deterministic router state. If the Theme Builder master post is missing (fresh substrate that never opened Divi → Theme Builder in WP Admin), the route auto-bootstraps one with the same shape Divi creates on first admin visit and returns `data.master_post_bootstrapped: true` so callers can audit the side-effect. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; failures during master-post bootstrap or template/layout insert surface the underlying WP_Error code (commonly `db_insert_error`, `db_update_error`, or other slugs from the WordPress vocabulary), not a generic `wp_error` — branch on `error.code` against the WP slug, not against a hard-coded string. The literal `wp_error` slug only surfaces when the upstream WP_Error has an empty code." +
|
|
1494
1690
|
DRY_RUN_DESC_SUFFIX,
|
|
1495
1691
|
inputSchema: {
|
|
1496
1692
|
title: z.string().describe('Template name (e.g. "Landing Pages")'),
|
|
1497
1693
|
condition: z
|
|
1498
1694
|
.string()
|
|
1499
|
-
.describe('Condition string (
|
|
1695
|
+
.describe('Condition string. Pass "default" (case-insensitive) or "" for the catch-all Default Website Template (sets `_et_default = 1`). Otherwise a Divi router-recognized location string such as "singular:post_type:page:all", "singular:post_type:project:all", "archive:taxonomy:category:all", "homepage", or "404" (lands in `_et_use_on`).'),
|
|
1500
1696
|
header_content: z
|
|
1501
1697
|
.string()
|
|
1502
1698
|
.optional()
|
|
@@ -1525,6 +1721,37 @@ registerPluginTool("diviops_tb_template_create", {
|
|
|
1525
1721
|
],
|
|
1526
1722
|
};
|
|
1527
1723
|
});
|
|
1724
|
+
registerPluginTool("diviops_tb_template_trash", {
|
|
1725
|
+
description: "Trash (or permanently delete) a Theme Builder template AND its linked header/body/footer layouts AND scrub the `_et_template` meta refs on the Theme Builder master post. Closes the orphan-meta gap left by `diviops_page_trash` / wp-cli `post delete` on linked layouts: the typed wrapper does the cleanup atomically. Defaults to trash (reversible via WP Admin → Trash). Pass `force=true` to permanently delete (wp_delete_post — irreversible, one-shot: a repeat call after a successful force-delete returns 'not_found' because the template post is gone from the DB). Idempotency applies to the default trash mode only: a repeat call after a successful trash-mode cleanup returns ok:true with `data.already_trashed = true` (mirrors `diviops_page_trash`). If a prior trash-mode call partially succeeded (some layouts already trashed, master meta still carries refs), the next call detects already-trashed targets via pre-state checks, skips the no-op WP destructor calls (which would otherwise return false), and still runs the meta scrub — `data.linked_layouts[].skipped` and `data.template_skipped` flag the targets that were already at the end-state. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; missing template_id returns 'not_found' (HTTP 404), delete-permission failures return 'forbidden' (HTTP 403); per-step trash/delete/meta-scrub failures return the namespaced 'tb_template.command_failed' (HTTP 500) with `error.data.failed_step` ∈ { 'layout_destroy', 'template_destroy', 'meta_scrub' } plus `template_id` and `force`." +
|
|
1726
|
+
DRY_RUN_DESC_SUFFIX,
|
|
1727
|
+
inputSchema: {
|
|
1728
|
+
template_id: z
|
|
1729
|
+
.number()
|
|
1730
|
+
.int()
|
|
1731
|
+
.describe("Theme Builder template post ID (the `et_template` post). Discover via diviops_tb_template_list — NOT the linked layout IDs."),
|
|
1732
|
+
force: z
|
|
1733
|
+
.boolean()
|
|
1734
|
+
.optional()
|
|
1735
|
+
.default(false)
|
|
1736
|
+
.describe("When true, permanently delete (skips trash). Default false moves to trash."),
|
|
1737
|
+
dry_run: DRY_RUN_FIELD,
|
|
1738
|
+
},
|
|
1739
|
+
annotations: { idempotentHint: false },
|
|
1740
|
+
_meta: { idempotent: "conditional" },
|
|
1741
|
+
}, async ({ template_id, force, dry_run }) => {
|
|
1742
|
+
const result = await wp.requestEnveloped(`/theme-builder/template/trash/${template_id}`, {
|
|
1743
|
+
method: "POST",
|
|
1744
|
+
body: {
|
|
1745
|
+
force: force ?? false,
|
|
1746
|
+
dry_run: dry_run ?? false,
|
|
1747
|
+
},
|
|
1748
|
+
});
|
|
1749
|
+
return {
|
|
1750
|
+
content: [
|
|
1751
|
+
{ type: "text", text: serializeEnvelope(result, "diviops_tb_template_trash") },
|
|
1752
|
+
],
|
|
1753
|
+
};
|
|
1754
|
+
});
|
|
1528
1755
|
// ── Canvas Tools ────────────────────────────────────────────────────
|
|
1529
1756
|
registerPluginTool("diviops_canvas_create", {
|
|
1530
1757
|
description: "Create a canvas (off-canvas workspace) linked to a page. Used for popups, off-canvas menus, modals. Content uses standard Divi block markup. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; missing parent_page_id returns ok:false with code 'not_found'; non-string content / malformed canvas_id / append_to_main outside {above, below} returns 'invalid_input'. Returns code 'conflict' (HTTP 409) when a canvas with the same title already exists under the same parent_page_id — error.data = { existing_canvas_id, parent_page_id, title }. Mirrors diviops_preset_create's uniqueness contract." +
|