@diviops/mcp-server 1.5.3 → 1.5.5

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.
@@ -110,8 +110,44 @@ export declare function wrapResponse<T>(producer: () => Promise<T | DiviopsRespo
110
110
  * (e.g. shrink schema, project fields) without unwrapping by hand.
111
111
  */
112
112
  export declare function envelopeMap<T, U>(response: DiviopsResponse<T>, fn: (data: T) => U): DiviopsResponse<U>;
113
+ /**
114
+ * Idempotency vocabulary surfaced to clients via `_meta.idempotent` on
115
+ * every tool response (#597). Mirrors the registration-level `_meta.idempotent`
116
+ * declared at each `registerTool()` callsite so per-call consumers see the
117
+ * field where they naturally look (response envelope), not just at
118
+ * `tools/list` discovery time.
119
+ */
120
+ export type IdempotentVerdict = "true" | "false" | "conditional";
121
+ /**
122
+ * Record a tool's idempotency verdict at registration time so per-call
123
+ * `serializeEnvelope(result, toolName)` can emit it on every response.
124
+ * Throws if the registration is missing `_meta.idempotent` — fail-loud
125
+ * matches `feedback_explicit_reject_over_silent_sanitize`: a tool that
126
+ * skipped the audit would silently lack the field, defeating #597.
127
+ */
128
+ export declare function recordIdempotent(name: string, meta: {
129
+ idempotent?: string;
130
+ } | undefined): void;
131
+ /**
132
+ * Look up a tool's idempotency verdict. Returns undefined if the tool
133
+ * hasn't been recorded — caller decides whether to fail or skip emission.
134
+ */
135
+ export declare function getRecordedIdempotent(name: string): IdempotentVerdict | undefined;
136
+ /**
137
+ * Snapshot of the recorded table — used by tests + the CI gate. Returns
138
+ * a fresh object so callers can't mutate the live registry.
139
+ */
140
+ export declare function snapshotIdempotentTable(): Record<string, IdempotentVerdict>;
113
141
  /**
114
142
  * Serialize a `DiviopsResponse` as the JSON string an MCP tool emits in its
115
143
  * `content[0].text` slot. Single emit point keeps the wire shape consistent.
144
+ *
145
+ * When `toolName` is provided AND the tool has been recorded via
146
+ * `recordIdempotent()`, injects `_meta.idempotent` onto the response
147
+ * envelope before serialization (#597). Per-call emission is a strict
148
+ * superset of the `tools/list` registration-level `_meta` surface — both
149
+ * keep working, per-call consumers get the field where they look. Applied
150
+ * to ok:true AND ok:false envelopes since idempotency is a property of
151
+ * the tool, not the call outcome.
116
152
  */
117
- export declare function serializeEnvelope<T>(response: DiviopsResponse<T>): string;
153
+ export declare function serializeEnvelope<T>(response: DiviopsResponse<T>, toolName?: string): string;
package/dist/envelope.js CHANGED
@@ -162,10 +162,71 @@ export function envelopeMap(response, fn) {
162
162
  return { ok: true, data: fn(response.data) };
163
163
  return response;
164
164
  }
165
+ /**
166
+ * Tool-name → idempotent-verdict registry, populated at server startup by
167
+ * `recordIdempotent()` from each tool's registration `_meta`. The audit
168
+ * doc (`docs/idempotency-audit.md`) remains the single source of truth;
169
+ * this table is just the runtime mirror used to enrich per-call responses.
170
+ */
171
+ const IDEMPOTENT_TABLE = new Map();
172
+ /**
173
+ * Record a tool's idempotency verdict at registration time so per-call
174
+ * `serializeEnvelope(result, toolName)` can emit it on every response.
175
+ * Throws if the registration is missing `_meta.idempotent` — fail-loud
176
+ * matches `feedback_explicit_reject_over_silent_sanitize`: a tool that
177
+ * skipped the audit would silently lack the field, defeating #597.
178
+ */
179
+ export function recordIdempotent(name, meta) {
180
+ const verdict = meta?.idempotent;
181
+ if (verdict !== "true" && verdict !== "false" && verdict !== "conditional") {
182
+ throw new Error(`Tool '${name}' is missing or has invalid _meta.idempotent declaration ` +
183
+ `(got: ${verdict === undefined ? "undefined" : JSON.stringify(verdict)}). ` +
184
+ `Every tool must declare idempotent: "true" | "false" | "conditional" ` +
185
+ `per docs/idempotency-audit.md.`);
186
+ }
187
+ IDEMPOTENT_TABLE.set(name, verdict);
188
+ }
189
+ /**
190
+ * Look up a tool's idempotency verdict. Returns undefined if the tool
191
+ * hasn't been recorded — caller decides whether to fail or skip emission.
192
+ */
193
+ export function getRecordedIdempotent(name) {
194
+ return IDEMPOTENT_TABLE.get(name);
195
+ }
196
+ /**
197
+ * Snapshot of the recorded table — used by tests + the CI gate. Returns
198
+ * a fresh object so callers can't mutate the live registry.
199
+ */
200
+ export function snapshotIdempotentTable() {
201
+ return Object.fromEntries(IDEMPOTENT_TABLE.entries());
202
+ }
165
203
  /**
166
204
  * Serialize a `DiviopsResponse` as the JSON string an MCP tool emits in its
167
205
  * `content[0].text` slot. Single emit point keeps the wire shape consistent.
206
+ *
207
+ * When `toolName` is provided AND the tool has been recorded via
208
+ * `recordIdempotent()`, injects `_meta.idempotent` onto the response
209
+ * envelope before serialization (#597). Per-call emission is a strict
210
+ * superset of the `tools/list` registration-level `_meta` surface — both
211
+ * keep working, per-call consumers get the field where they look. Applied
212
+ * to ok:true AND ok:false envelopes since idempotency is a property of
213
+ * the tool, not the call outcome.
168
214
  */
169
- export function serializeEnvelope(response) {
170
- return JSON.stringify(response);
215
+ export function serializeEnvelope(response, toolName) {
216
+ if (toolName === undefined) {
217
+ return JSON.stringify(response);
218
+ }
219
+ const verdict = IDEMPOTENT_TABLE.get(toolName);
220
+ if (verdict === undefined) {
221
+ return JSON.stringify(response);
222
+ }
223
+ const existingMetaRaw = response._meta;
224
+ const existingMeta = typeof existingMetaRaw === "object" && existingMetaRaw !== null
225
+ ? existingMetaRaw
226
+ : {};
227
+ const enriched = {
228
+ ...response,
229
+ _meta: { ...existingMeta, idempotent: verdict },
230
+ };
231
+ return JSON.stringify(enriched);
171
232
  }
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
13
13
  import { z } from "zod";
14
14
  import { WPClient } from "./wp-client.js";
15
15
  import { MissingCapabilityError } from "./compatibility.js";
16
- import { ErrorCodes, envelopeMap, serializeEnvelope, withCode, wrapResponse, } from "./envelope.js";
16
+ import { ErrorCodes, envelopeMap, recordIdempotent, serializeEnvelope, withCode, wrapResponse, } from "./envelope.js";
17
17
  import { optimizeSchema } from "./schema-optimizer.js";
18
18
  import { createWpCli } from "./wp-cli.js";
19
19
  import { findForeignVarRefs, scanAttrsForForeignVarRefs, isolationErrorResult, } from "./validate-attrs.js";
@@ -122,8 +122,20 @@ function registerPluginTool(name, config, handler) {
122
122
  }
123
123
  return handler(args);
124
124
  });
125
+ recordIdempotent(name, config?._meta);
125
126
  server.registerTool(name, config, wrapped);
126
127
  }
128
+ /**
129
+ * Server-local tools (no plugin dependency) register via this thin shim
130
+ * instead of `server.registerTool` directly. Same recording obligation
131
+ * as `registerPluginTool` — every tool surface needs `_meta.idempotent`
132
+ * captured into the runtime table so `serializeEnvelope(result, name)`
133
+ * can emit it on per-call responses (#597).
134
+ */
135
+ function registerLocalTool(name, config, handler) {
136
+ recordIdempotent(name, config?._meta);
137
+ server.registerTool(name, config, handler);
138
+ }
127
139
  /* eslint-enable @typescript-eslint/no-explicit-any */
128
140
  // ── dry_run convention ──────────────────────────────────────────────
129
141
  //
@@ -172,7 +184,7 @@ registerPluginTool("diviops_page_list", {
172
184
  });
173
185
  return {
174
186
  content: [
175
- { type: "text", text: serializeEnvelope(result) },
187
+ { type: "text", text: serializeEnvelope(result, "diviops_page_list") },
176
188
  ],
177
189
  };
178
190
  });
@@ -187,7 +199,7 @@ registerPluginTool("diviops_page_get", {
187
199
  const result = await wp.requestEnveloped(`/page/get/${page_id}`);
188
200
  return {
189
201
  content: [
190
- { type: "text", text: serializeEnvelope(result) },
202
+ { type: "text", text: serializeEnvelope(result, "diviops_page_get") },
191
203
  ],
192
204
  };
193
205
  });
@@ -209,7 +221,7 @@ registerPluginTool("diviops_page_get_layout", {
209
221
  });
210
222
  return {
211
223
  content: [
212
- { type: "text", text: serializeEnvelope(result) },
224
+ { type: "text", text: serializeEnvelope(result, "diviops_page_get_layout") },
213
225
  ],
214
226
  };
215
227
  });
@@ -221,7 +233,7 @@ registerPluginTool("diviops_schema_list_modules", {
221
233
  const result = await wp.requestEnveloped("/schema/modules");
222
234
  return {
223
235
  content: [
224
- { type: "text", text: serializeEnvelope(result) },
236
+ { type: "text", text: serializeEnvelope(result, "diviops_schema_list_modules") },
225
237
  ],
226
238
  };
227
239
  });
@@ -263,12 +275,12 @@ registerPluginTool("diviops_schema_get_module", {
263
275
  },
264
276
  };
265
277
  return {
266
- content: [{ type: "text", text: serializeEnvelope(failure) }],
278
+ content: [{ type: "text", text: serializeEnvelope(failure, "diviops_schema_get_module") }],
267
279
  };
268
280
  }
269
281
  const result = await wp.requestEnveloped("/schema/module/dump-all");
270
282
  return {
271
- content: [{ type: "text", text: serializeEnvelope(result) }],
283
+ content: [{ type: "text", text: serializeEnvelope(result, "diviops_schema_get_module") }],
272
284
  };
273
285
  }
274
286
  if (!module_name) {
@@ -280,14 +292,14 @@ registerPluginTool("diviops_schema_get_module", {
280
292
  },
281
293
  };
282
294
  return {
283
- content: [{ type: "text", text: serializeEnvelope(failure) }],
295
+ content: [{ type: "text", text: serializeEnvelope(failure, "diviops_schema_get_module") }],
284
296
  };
285
297
  }
286
298
  const result = await wp.requestEnveloped(`/schema/module/${encodeURIComponent(module_name)}`);
287
299
  const projected = envelopeMap(result, (data) => raw ? data : optimizeSchema(data));
288
300
  return {
289
301
  content: [
290
- { type: "text", text: serializeEnvelope(projected) },
302
+ { type: "text", text: serializeEnvelope(projected, "diviops_schema_get_module") },
291
303
  ],
292
304
  };
293
305
  });
@@ -299,7 +311,7 @@ registerPluginTool("diviops_schema_get_settings", {
299
311
  const result = await wp.requestEnveloped("/schema/settings");
300
312
  return {
301
313
  content: [
302
- { type: "text", text: serializeEnvelope(result) },
314
+ { type: "text", text: serializeEnvelope(result, "diviops_schema_get_settings") },
303
315
  ],
304
316
  };
305
317
  });
@@ -311,7 +323,7 @@ registerPluginTool("diviops_global_color_list", {
311
323
  const result = await wp.requestEnveloped("/global-color/list");
312
324
  return {
313
325
  content: [
314
- { type: "text", text: serializeEnvelope(result) },
326
+ { type: "text", text: serializeEnvelope(result, "diviops_global_color_list") },
315
327
  ],
316
328
  };
317
329
  });
@@ -354,7 +366,7 @@ registerPluginTool("diviops_global_color_create", {
354
366
  method: "POST",
355
367
  body,
356
368
  });
357
- return { content: [{ type: "text", text: serializeEnvelope(result) }] };
369
+ return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_global_color_create") }] };
358
370
  });
359
371
  registerPluginTool("diviops_global_color_update", {
360
372
  description: "Update an existing global color by gcid. Only provided fields are updated; omitted fields are preserved. The lastUpdated timestamp is bumped on every write. Use diviops_global_color_list first to find the gcid for a color. NOTE: the underlying upsert is merge-mode — supplying a gcid that doesn't yet exist creates a new color with that gcid (provided it satisfies the gcid charset/length rules) rather than failing as 'not found'. Pre-check via diviops_global_color_list if you need strict-update semantics. CONCURRENCY: same VB-session race caveat as diviops_global_color_create — the write is read-modify-write on a single WP option, so an active VB session's next save can clobber this update. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; malformed gcid charset/length returns code 'invalid_input' with `error.data` documenting the failed field; non-CSS color value returns code 'invalid_input'; attempts to write to a customizer-bound default (gcid-primary-color / gcid-secondary-color / gcid-heading-color / gcid-body-color / gcid-link-color) return code 'variable.customizer_default_immutable' (HTTP 403) with `error.data = { id, managed_by: 'wp_customizer' }` — same code as diviops_variable_delete because the identity is identical (5.4+ unified gcid-* into the variable manager while preserving customizer-binding for the five legacy defaults)." +
@@ -400,10 +412,10 @@ registerPluginTool("diviops_global_color_update", {
400
412
  method: "POST",
401
413
  body,
402
414
  });
403
- return { content: [{ type: "text", text: serializeEnvelope(result) }] };
415
+ return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_global_color_update") }] };
404
416
  });
405
417
  registerPluginTool("diviops_global_color_delete", {
406
- description: "Delete a global color from the registry by gcid. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }. Live-reference soft-block: returns code 'conflict' (HTTP 409) when Divi's `usedInPosts` index lists references`error.data = { id, ref_count, used_in_posts }` carries Divi's pass-through reference list (Divi-maintained on VB save; element shape is whatever Divi emits, NOT the discriminated-union `locations[]` shape diviops_variable_delete builds via parse_blocks). Pass `force: true` to override; orphan refs will render as invalid CSS until pages are re-saved through VB. Always refuses to delete the 5 customizer-bound defaults (gcid-primary-color, gcid-secondary-color, gcid-heading-color, gcid-body-color, gcid-link-color) regardless of force — returns code 'variable.customizer_default_immutable' (HTTP 403) with `error.data = { id, managed_by: 'wp_customizer' }`. Missing gcids return 'not_found' (HTTP 404). Malformed gcid (empty or missing `gcid-` prefix) returns 'invalid_input'. CONCURRENCY: same VB-session race caveat as diviops_global_color_create — an active VB session's next save can re-introduce a color we just deleted if the session held stale data." +
418
+ description: "Delete a global color from the registry by gcid. 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 (mirrors diviops_variable_delete) MCP-authored content is detected reliably, not just VB-saved content. 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 render as invalid CSS until pages are re-authored. Always refuses to delete the 5 customizer-bound defaults (gcid-primary-color, gcid-secondary-color, gcid-heading-color, gcid-body-color, gcid-link-color) regardless of force — returns code 'variable.customizer_default_immutable' (HTTP 403) with `error.data = { id, managed_by: 'wp_customizer' }`. Missing gcids return 'not_found' (HTTP 404). Malformed gcid (empty or missing `gcid-` prefix) returns 'invalid_input'. CONCURRENCY: same VB-session race caveat as diviops_global_color_create — an active VB session's next save can re-introduce a color we just deleted if the session held stale data." +
407
419
  DRY_RUN_DESC_SUFFIX,
408
420
  inputSchema: {
409
421
  gcid: z
@@ -413,7 +425,7 @@ registerPluginTool("diviops_global_color_delete", {
413
425
  .boolean()
414
426
  .optional()
415
427
  .default(false)
416
- .describe("If true, delete even when usedInPosts shows live references. Customizer-bound defaults remain protected regardless."),
428
+ .describe("If true, delete even when live references exist. Customizer-bound defaults remain protected regardless."),
417
429
  dry_run: DRY_RUN_FIELD,
418
430
  },
419
431
  annotations: { idempotentHint: true },
@@ -428,7 +440,7 @@ registerPluginTool("diviops_global_color_delete", {
428
440
  method: "POST",
429
441
  body,
430
442
  });
431
- return { content: [{ type: "text", text: serializeEnvelope(result) }] };
443
+ return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_global_color_delete") }] };
432
444
  });
433
445
  registerPluginTool("diviops_global_font_list", {
434
446
  description: "Get the global font definitions from Divi settings. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }.",
@@ -438,7 +450,7 @@ registerPluginTool("diviops_global_font_list", {
438
450
  const result = await wp.requestEnveloped("/global-font/list");
439
451
  return {
440
452
  content: [
441
- { type: "text", text: serializeEnvelope(result) },
453
+ { type: "text", text: serializeEnvelope(result, "diviops_global_font_list") },
442
454
  ],
443
455
  };
444
456
  });
@@ -465,7 +477,7 @@ registerPluginTool("diviops_meta_find_icon", {
465
477
  const result = await wp.requestEnveloped(`/meta/find-icon?q=${encodeURIComponent(query)}&type=${type ?? "all"}&limit=${limit ?? 10}`);
466
478
  return {
467
479
  content: [
468
- { type: "text", text: serializeEnvelope(result) },
480
+ { type: "text", text: serializeEnvelope(result, "diviops_meta_find_icon") },
469
481
  ],
470
482
  };
471
483
  });
@@ -495,7 +507,7 @@ registerPluginTool("diviops_page_update_content", {
495
507
  });
496
508
  return {
497
509
  content: [
498
- { type: "text", text: serializeEnvelope(result) },
510
+ { type: "text", text: serializeEnvelope(result, "diviops_page_update_content") },
499
511
  ],
500
512
  };
501
513
  });
@@ -513,7 +525,7 @@ registerPluginTool("diviops_render_preview", {
513
525
  });
514
526
  return {
515
527
  content: [
516
- { type: "text", text: serializeEnvelope(result) },
528
+ { type: "text", text: serializeEnvelope(result, "diviops_render_preview") },
517
529
  ],
518
530
  };
519
531
  });
@@ -531,7 +543,7 @@ registerPluginTool("diviops_validate_blocks", {
531
543
  });
532
544
  return {
533
545
  content: [
534
- { type: "text", text: serializeEnvelope(result) },
546
+ { type: "text", text: serializeEnvelope(result, "diviops_validate_blocks") },
535
547
  ],
536
548
  };
537
549
  });
@@ -565,7 +577,7 @@ registerPluginTool("diviops_section_append", {
565
577
  });
566
578
  return {
567
579
  content: [
568
- { type: "text", text: serializeEnvelope(result) },
580
+ { type: "text", text: serializeEnvelope(result, "diviops_section_append") },
569
581
  ],
570
582
  };
571
583
  });
@@ -613,7 +625,7 @@ registerPluginTool("diviops_section_replace", {
613
625
  });
614
626
  return {
615
627
  content: [
616
- { type: "text", text: serializeEnvelope(result) },
628
+ { type: "text", text: serializeEnvelope(result, "diviops_section_replace") },
617
629
  ],
618
630
  };
619
631
  });
@@ -655,7 +667,7 @@ registerPluginTool("diviops_section_remove", {
655
667
  });
656
668
  return {
657
669
  content: [
658
- { type: "text", text: serializeEnvelope(result) },
670
+ { type: "text", text: serializeEnvelope(result, "diviops_section_remove") },
659
671
  ],
660
672
  };
661
673
  });
@@ -691,7 +703,7 @@ registerPluginTool("diviops_section_get", {
691
703
  const result = await wp.requestEnveloped(`/section/get/${page_id}?${qs}`);
692
704
  return {
693
705
  content: [
694
- { type: "text", text: serializeEnvelope(result) },
706
+ { type: "text", text: serializeEnvelope(result, "diviops_section_get") },
695
707
  ],
696
708
  };
697
709
  });
@@ -747,7 +759,7 @@ registerPluginTool("diviops_module_update", {
747
759
  });
748
760
  return {
749
761
  content: [
750
- { type: "text", text: serializeEnvelope(result) },
762
+ { type: "text", text: serializeEnvelope(result, "diviops_module_update") },
751
763
  ],
752
764
  };
753
765
  });
@@ -827,7 +839,7 @@ registerPluginTool("diviops_module_move", {
827
839
  });
828
840
  return {
829
841
  content: [
830
- { type: "text", text: serializeEnvelope(result) },
842
+ { type: "text", text: serializeEnvelope(result, "diviops_module_move") },
831
843
  ],
832
844
  };
833
845
  });
@@ -857,7 +869,7 @@ registerPluginTool("diviops_module_lock", {
857
869
  if (dry_run)
858
870
  body.dry_run = true;
859
871
  const result = await wp.requestEnveloped(`/module/lock/${page_id}`, { method: "POST", body });
860
- return { content: [{ type: "text", text: serializeEnvelope(result) }] };
872
+ return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_module_lock") }] };
861
873
  });
862
874
  registerPluginTool("diviops_module_unlock", {
863
875
  description: "Unlock a module by removing attrs.locked entirely. Matches Divi VB's convention: unlocked = attribute absent (NOT {value: 'off'}) — VB doesn't write a falsy value on unlock, it removes the field. Same targeting pattern as diviops_module_lock. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; missing module returns 'not_found' with `error.data.target_kind = \"module\"`." +
@@ -885,7 +897,7 @@ registerPluginTool("diviops_module_unlock", {
885
897
  if (dry_run)
886
898
  body.dry_run = true;
887
899
  const result = await wp.requestEnveloped(`/module/unlock/${page_id}`, { method: "POST", body });
888
- return { content: [{ type: "text", text: serializeEnvelope(result) }] };
900
+ return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_module_unlock") }] };
889
901
  });
890
902
  registerPluginTool("diviops_module_clone", {
891
903
  description: 'Clone a module by deep-copying its block JSON and inserting it next to the source within the same parent container. Position controls before/after placement (default "after"). Module IDs are reassigned by Divi at render time from the block tree position, so the clone gets fresh IDs automatically. Same targeting pattern as diviops_module_lock. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; missing module returns code "not_found" with error.data.target_kind = "module", malformed parent containers surface code "divi_error" (HTTP 500).' +
@@ -916,7 +928,7 @@ registerPluginTool("diviops_module_clone", {
916
928
  if (dry_run)
917
929
  body.dry_run = true;
918
930
  const result = await wp.requestEnveloped(`/module/clone/${page_id}`, { method: "POST", body });
919
- return { content: [{ type: "text", text: serializeEnvelope(result) }] };
931
+ return { content: [{ type: "text", text: serializeEnvelope(result, "diviops_module_clone") }] };
920
932
  });
921
933
  registerPluginTool("diviops_page_create", {
922
934
  description: "Create a new WordPress page, optionally with Divi block content. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; non-string content or invalid status return code 'invalid_input' with `error.data` documenting the failed field." +
@@ -952,7 +964,7 @@ registerPluginTool("diviops_page_create", {
952
964
  });
953
965
  return {
954
966
  content: [
955
- { type: "text", text: serializeEnvelope(result) },
967
+ { type: "text", text: serializeEnvelope(result, "diviops_page_create") },
956
968
  ],
957
969
  };
958
970
  });
@@ -983,7 +995,7 @@ registerPluginTool("diviops_page_trash", {
983
995
  });
984
996
  return {
985
997
  content: [
986
- { type: "text", text: serializeEnvelope(result) },
998
+ { type: "text", text: serializeEnvelope(result, "diviops_page_trash") },
987
999
  ],
988
1000
  };
989
1001
  });
@@ -1019,7 +1031,7 @@ registerPluginTool("diviops_page_update_status", {
1019
1031
  });
1020
1032
  return {
1021
1033
  content: [
1022
- { type: "text", text: serializeEnvelope(result) },
1034
+ { type: "text", text: serializeEnvelope(result, "diviops_page_update_status") },
1023
1035
  ],
1024
1036
  };
1025
1037
  });
@@ -1032,7 +1044,7 @@ registerPluginTool("diviops_preset_audit", {
1032
1044
  const result = await wp.requestEnveloped("/preset/audit");
1033
1045
  return {
1034
1046
  content: [
1035
- { type: "text", text: serializeEnvelope(result) },
1047
+ { type: "text", text: serializeEnvelope(result, "diviops_preset_audit") },
1036
1048
  ],
1037
1049
  };
1038
1050
  });
@@ -1080,7 +1092,7 @@ registerPluginTool("diviops_preset_cleanup", {
1080
1092
  });
1081
1093
  return {
1082
1094
  content: [
1083
- { type: "text", text: serializeEnvelope(result) },
1095
+ { type: "text", text: serializeEnvelope(result, "diviops_preset_cleanup") },
1084
1096
  ],
1085
1097
  };
1086
1098
  });
@@ -1119,7 +1131,7 @@ registerPluginTool("diviops_preset_update", {
1119
1131
  });
1120
1132
  return {
1121
1133
  content: [
1122
- { type: "text", text: serializeEnvelope(result) },
1134
+ { type: "text", text: serializeEnvelope(result, "diviops_preset_update") },
1123
1135
  ],
1124
1136
  };
1125
1137
  });
@@ -1144,7 +1156,7 @@ registerPluginTool("diviops_preset_delete", {
1144
1156
  });
1145
1157
  return {
1146
1158
  content: [
1147
- { type: "text", text: serializeEnvelope(result) },
1159
+ { type: "text", text: serializeEnvelope(result, "diviops_preset_delete") },
1148
1160
  ],
1149
1161
  };
1150
1162
  });
@@ -1210,7 +1222,7 @@ registerPluginTool("diviops_preset_create", {
1210
1222
  const result = await wp.requestEnveloped("/preset/create", { method: "POST", body });
1211
1223
  return {
1212
1224
  content: [
1213
- { type: "text", text: serializeEnvelope(result) },
1225
+ { type: "text", text: serializeEnvelope(result, "diviops_preset_create") },
1214
1226
  ],
1215
1227
  };
1216
1228
  });
@@ -1261,7 +1273,7 @@ registerPluginTool("diviops_preset_reassign", {
1261
1273
  });
1262
1274
  return {
1263
1275
  content: [
1264
- { type: "text", text: serializeEnvelope(result) },
1276
+ { type: "text", text: serializeEnvelope(result, "diviops_preset_reassign") },
1265
1277
  ],
1266
1278
  };
1267
1279
  });
@@ -1273,7 +1285,7 @@ registerPluginTool("diviops_preset_scan_orphans", {
1273
1285
  const result = await wp.requestEnveloped("/preset/scan-orphans");
1274
1286
  return {
1275
1287
  content: [
1276
- { type: "text", text: serializeEnvelope(result) },
1288
+ { type: "text", text: serializeEnvelope(result, "diviops_preset_scan_orphans") },
1277
1289
  ],
1278
1290
  };
1279
1291
  });
@@ -1319,7 +1331,7 @@ registerPluginTool("diviops_preset_set_default", {
1319
1331
  });
1320
1332
  return {
1321
1333
  content: [
1322
- { type: "text", text: serializeEnvelope(result) },
1334
+ { type: "text", text: serializeEnvelope(result, "diviops_preset_set_default") },
1323
1335
  ],
1324
1336
  };
1325
1337
  });
@@ -1354,7 +1366,7 @@ registerPluginTool("diviops_library_list", {
1354
1366
  const result = await wp.requestEnveloped("/library/items", { params });
1355
1367
  return {
1356
1368
  content: [
1357
- { type: "text", text: serializeEnvelope(result) },
1369
+ { type: "text", text: serializeEnvelope(result, "diviops_library_list") },
1358
1370
  ],
1359
1371
  };
1360
1372
  });
@@ -1369,7 +1381,7 @@ registerPluginTool("diviops_library_get", {
1369
1381
  const result = await wp.requestEnveloped(`/library/item/${item_id}`);
1370
1382
  return {
1371
1383
  content: [
1372
- { type: "text", text: serializeEnvelope(result) },
1384
+ { type: "text", text: serializeEnvelope(result, "diviops_library_get") },
1373
1385
  ],
1374
1386
  };
1375
1387
  });
@@ -1405,7 +1417,7 @@ registerPluginTool("diviops_library_save", {
1405
1417
  });
1406
1418
  return {
1407
1419
  content: [
1408
- { type: "text", text: serializeEnvelope(result) },
1420
+ { type: "text", text: serializeEnvelope(result, "diviops_library_save") },
1409
1421
  ],
1410
1422
  };
1411
1423
  });
@@ -1432,7 +1444,7 @@ registerPluginTool("diviops_tb_template_list", {
1432
1444
  const result = await wp.requestEnveloped("/theme-builder/template/list", { params });
1433
1445
  return {
1434
1446
  content: [
1435
- { type: "text", text: serializeEnvelope(result) },
1447
+ { type: "text", text: serializeEnvelope(result, "diviops_tb_template_list") },
1436
1448
  ],
1437
1449
  };
1438
1450
  });
@@ -1449,7 +1461,7 @@ registerPluginTool("diviops_tb_layout_get", {
1449
1461
  const result = await wp.requestEnveloped(`/theme-builder/layout/get/${layout_id}`);
1450
1462
  return {
1451
1463
  content: [
1452
- { type: "text", text: serializeEnvelope(result) },
1464
+ { type: "text", text: serializeEnvelope(result, "diviops_tb_layout_get") },
1453
1465
  ],
1454
1466
  };
1455
1467
  });
@@ -1473,7 +1485,7 @@ registerPluginTool("diviops_tb_layout_update", {
1473
1485
  });
1474
1486
  return {
1475
1487
  content: [
1476
- { type: "text", text: serializeEnvelope(result) },
1488
+ { type: "text", text: serializeEnvelope(result, "diviops_tb_layout_update") },
1477
1489
  ],
1478
1490
  };
1479
1491
  });
@@ -1509,13 +1521,13 @@ registerPluginTool("diviops_tb_template_create", {
1509
1521
  });
1510
1522
  return {
1511
1523
  content: [
1512
- { type: "text", text: serializeEnvelope(result) },
1524
+ { type: "text", text: serializeEnvelope(result, "diviops_tb_template_create") },
1513
1525
  ],
1514
1526
  };
1515
1527
  });
1516
1528
  // ── Canvas Tools ────────────────────────────────────────────────────
1517
1529
  registerPluginTool("diviops_canvas_create", {
1518
- 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'." +
1530
+ 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." +
1519
1531
  DRY_RUN_DESC_SUFFIX,
1520
1532
  inputSchema: {
1521
1533
  title: z
@@ -1542,7 +1554,7 @@ registerPluginTool("diviops_canvas_create", {
1542
1554
  dry_run: DRY_RUN_FIELD,
1543
1555
  },
1544
1556
  annotations: { idempotentHint: false },
1545
- _meta: { idempotent: "false" },
1557
+ _meta: { idempotent: "conditional" },
1546
1558
  }, async ({ title, parent_page_id, content, canvas_id, append_to_main, z_index, dry_run, }) => {
1547
1559
  const body = {
1548
1560
  title,
@@ -1560,7 +1572,7 @@ registerPluginTool("diviops_canvas_create", {
1560
1572
  const result = await wp.requestEnveloped("/canvas/create", { method: "POST", body });
1561
1573
  return {
1562
1574
  content: [
1563
- { type: "text", text: serializeEnvelope(result) },
1575
+ { type: "text", text: serializeEnvelope(result, "diviops_canvas_create") },
1564
1576
  ],
1565
1577
  };
1566
1578
  });
@@ -1591,7 +1603,7 @@ registerPluginTool("diviops_canvas_list", {
1591
1603
  const result = await wp.requestEnveloped("/canvas/list", { params });
1592
1604
  return {
1593
1605
  content: [
1594
- { type: "text", text: serializeEnvelope(result) },
1606
+ { type: "text", text: serializeEnvelope(result, "diviops_canvas_list") },
1595
1607
  ],
1596
1608
  };
1597
1609
  });
@@ -1608,7 +1620,7 @@ registerPluginTool("diviops_canvas_get", {
1608
1620
  const result = await wp.requestEnveloped(`/canvas/get/${canvas_post_id}`);
1609
1621
  return {
1610
1622
  content: [
1611
- { type: "text", text: serializeEnvelope(result) },
1623
+ { type: "text", text: serializeEnvelope(result, "diviops_canvas_get") },
1612
1624
  ],
1613
1625
  };
1614
1626
  });
@@ -1649,7 +1661,7 @@ registerPluginTool("diviops_canvas_update", {
1649
1661
  });
1650
1662
  return {
1651
1663
  content: [
1652
- { type: "text", text: serializeEnvelope(result) },
1664
+ { type: "text", text: serializeEnvelope(result, "diviops_canvas_update") },
1653
1665
  ],
1654
1666
  };
1655
1667
  });
@@ -1679,7 +1691,7 @@ registerPluginTool("diviops_canvas_duplicate", {
1679
1691
  });
1680
1692
  return {
1681
1693
  content: [
1682
- { type: "text", text: serializeEnvelope(result) },
1694
+ { type: "text", text: serializeEnvelope(result, "diviops_canvas_duplicate") },
1683
1695
  ],
1684
1696
  };
1685
1697
  });
@@ -1702,12 +1714,12 @@ registerPluginTool("diviops_canvas_delete", {
1702
1714
  });
1703
1715
  return {
1704
1716
  content: [
1705
- { type: "text", text: serializeEnvelope(result) },
1717
+ { type: "text", text: serializeEnvelope(result, "diviops_canvas_delete") },
1706
1718
  ],
1707
1719
  };
1708
1720
  });
1709
1721
  // ── WP-CLI ──────────────────────────────────────────────────────────
1710
- server.registerTool("diviops_meta_wp_cli", {
1722
+ registerLocalTool("diviops_meta_wp_cli", {
1711
1723
  description: "Run a WP-CLI command on the WordPress site. Requires WP_PATH env var (LOCAL_SITE_ID auto-detected from Local by Flywheel), or WP_CLI_CMD for containerized wrappers. Commands validated against a safety allowlist. Default tier covers read ops across options/posts/post-types/taxonomies/users/info/core/db, non-destructive writes (post/term create+update, post meta read/write, cache/rewrite/transient flush, `plugin update` from authenticated sources), ACF/SCF schema ops (`acf export/import/field-group list/get` plus SCF 6.8.4+ `scf json {status,sync,import,export}` and the `acf json …` aliases), and WXR export. Extended tier (requires DIVIOPS_WP_CLI_ALLOW env var) adds destructive or bulk-modifying ops: option update, post/post meta/term delete, search-replace, import, plugin activate/deactivate, eval-file. Filesystem-touching commands (`wp export`, `acf export/import`, `scf|acf json export/import`) are additionally constrained: path arguments must resolve under a safe root (defaults to `<WP_PATH>/.diviops-tmp/`, overridable via DIVIOPS_WP_CLI_SAFE_FS_ROOT, disable via DIVIOPS_WP_CLI_UNSAFE_FS=1); `wp export` and `scf json export` require an explicit `--dir=<path>` (or `--stdout`). In WP_CLI_CMD wrapper mode, DIVIOPS_WP_CLI_SAFE_FS_ROOT is required for FS-sensitive commands. Prefer the typed `diviops_scf_*` wrappers for SCF round-trips — they're easier to invoke and accept the same safe-root scoping. Use --format=json for structured output. Full allowlist + tier rationale + filesystem semantics in the MCP server README. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }. Success payload: { stdout: string, stderr: string, exit_code: 0 }. Four failure modes converge on 'meta_wp_cli.command_failed' with error.data = { exit_code: number | null, stdout: string, stderr: string }: (a) numeric exit_code — wp-cli ran and exited non-zero; stdout/stderr are raw streams verbatim. (b) exit_code=null and message starts with 'wp-cli command terminated:' — execFile launched the child but it was killed (timeout or signal); stdout/stderr carry whatever streamed before the kill. (c) exit_code=null and message starts with 'wp-cli could not spawn:' — the OS refused to start the child (ENOENT/EACCES/EPERM); child never ran, stdout/stderr are empty. (d) exit_code=null and message is the rejection reason — pre-execution rejection by the allowlist / FS validator; rejection reason synthesized into error.data.stderr because the child never ran. A missing wp-cli configuration surfaces as 'meta_wp_cli.not_configured'. stdout is always passed through as a string (no server-side JSON parse) — pass --format=json and parse on the caller side when you want structured output.",
1712
1724
  inputSchema: {
1713
1725
  command: z
@@ -1803,7 +1815,7 @@ server.registerTool("diviops_meta_wp_cli", {
1803
1815
  });
1804
1816
  return {
1805
1817
  content: [
1806
- { type: "text", text: serializeEnvelope(response) },
1818
+ { type: "text", text: serializeEnvelope(response, "diviops_meta_wp_cli") },
1807
1819
  ],
1808
1820
  };
1809
1821
  });
@@ -1894,7 +1906,7 @@ function failScfCommand(result, args) {
1894
1906
  command: [...args],
1895
1907
  });
1896
1908
  }
1897
- server.registerTool("diviops_scf_status", {
1909
+ registerLocalTool("diviops_scf_status", {
1898
1910
  description: "Show SCF (Secure Custom Fields) sync status — how many field groups, post types, taxonomies, and options pages have JSON-on-disk newer than the database (or absent from DB). Read-only. Wraps `wp scf json status`. Requires SCF 6.8.4+ and WP_PATH or WP_CLI_CMD. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { stdout: string, stderr: string }. wp-cli failures map to 'scf.command_failed' with `error.data = { exit_code, stdout, stderr, failure_kind, command }` (four failure_kind branches: 'exited'/'killed'/'spawn_failed'/'rejected' — see tools.md). Missing wp-cli configuration surfaces as 'scf.not_configured'.",
1899
1911
  inputSchema: {
1900
1912
  type: z
@@ -1922,11 +1934,11 @@ server.registerTool("diviops_scf_status", {
1922
1934
  });
1923
1935
  return {
1924
1936
  content: [
1925
- { type: "text", text: serializeEnvelope(response) },
1937
+ { type: "text", text: serializeEnvelope(response, "diviops_scf_status") },
1926
1938
  ],
1927
1939
  };
1928
1940
  });
1929
- server.registerTool("diviops_scf_export", {
1941
+ registerLocalTool("diviops_scf_export", {
1930
1942
  description: "Export SCF field groups, post types, taxonomies, and options pages as JSON — to a directory under the safe-root (`<WP_PATH>/.diviops-tmp/` by default, override via DIVIOPS_WP_CLI_SAFE_FS_ROOT) or to stdout. Wraps `wp scf json export`. Either `dir` or `stdout: true` is required. Filters can be combined; without filters, all items are exported. Note: SCF writes a fixed filename `acf-export-YYYY-MM-DD.json` inside `dir` — two exports on the same day silently overwrite. Copy/rename if you're archiving baselines. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { stdout: string, stderr: string }. Pre-wp-cli input rejections (neither/both of `dir`/`stdout`) return code 'invalid_input' with `error.data` documenting the failed fields. wp-cli failures map to 'scf.command_failed' (same shape as scf_status). Missing wp-cli configuration surfaces as 'scf.not_configured'.",
1931
1943
  inputSchema: {
1932
1944
  dir: z
@@ -1980,11 +1992,11 @@ server.registerTool("diviops_scf_export", {
1980
1992
  });
1981
1993
  return {
1982
1994
  content: [
1983
- { type: "text", text: serializeEnvelope(response) },
1995
+ { type: "text", text: serializeEnvelope(response, "diviops_scf_export") },
1984
1996
  ],
1985
1997
  };
1986
1998
  });
1987
- server.registerTool("diviops_scf_import", {
1999
+ registerLocalTool("diviops_scf_import", {
1988
2000
  description: "Import SCF field groups, post types, taxonomies, options pages from a JSON file. Mutates the database. File path must resolve under the safe-root (`<WP_PATH>/.diviops-tmp/` by default, override via DIVIOPS_WP_CLI_SAFE_FS_ROOT). Idempotent — existing items with matching keys are updated. Wraps `wp scf json import <file>`. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { stdout: string, stderr: string }. wp-cli failures (missing/unreadable file, malformed JSON, allowlist or FS-validator rejection) map to 'scf.command_failed' with `error.data = { exit_code, stdout, stderr, failure_kind, command }`. Missing wp-cli configuration surfaces as 'scf.not_configured'.",
1989
2001
  inputSchema: {
1990
2002
  file: z
@@ -2004,11 +2016,11 @@ server.registerTool("diviops_scf_import", {
2004
2016
  });
2005
2017
  return {
2006
2018
  content: [
2007
- { type: "text", text: serializeEnvelope(response) },
2019
+ { type: "text", text: serializeEnvelope(response, "diviops_scf_import") },
2008
2020
  ],
2009
2021
  };
2010
2022
  });
2011
- server.registerTool("diviops_scf_sync", {
2023
+ registerLocalTool("diviops_scf_sync", {
2012
2024
  description: "Apply pending JSON-on-disk SCF changes to the database. Reads JSON files from the theme/plugin acf-json directory and creates/updates DB entries. Defaults to `dry_run: true` for safety — caller must opt in to mutation. Wraps `wp scf json sync`. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { dry_run: boolean, stdout: string, stderr: string }. NOTE: `dry_run` is passed through as wp-cli's `--dry-run` flag — the upstream output shape is wp-cli's plain-text summary, NOT the standard `data.plan = { summary, changes[] }` shape used by plugin-routed `dry_run` tools. The `dry_run` boolean is reflected in the success payload so callers can branch without re-checking input args, but the SCF-on-disk preview is what wp-cli produced. wp-cli failures map to 'scf.command_failed'; missing wp-cli configuration surfaces as 'scf.not_configured'.",
2013
2025
  inputSchema: {
2014
2026
  type: z
@@ -2047,11 +2059,11 @@ server.registerTool("diviops_scf_sync", {
2047
2059
  });
2048
2060
  return {
2049
2061
  content: [
2050
- { type: "text", text: serializeEnvelope(response) },
2062
+ { type: "text", text: serializeEnvelope(response, "diviops_scf_sync") },
2051
2063
  ],
2052
2064
  };
2053
2065
  });
2054
- server.registerTool("diviops_scf_field_group_list", {
2066
+ registerLocalTool("diviops_scf_field_group_list", {
2055
2067
  description: "List all SCF/ACF field groups in the database (post_name = ACF key, post_title, post_status, post_modified). Read-only. Queries the underlying `acf-field-group` post type via `wp post list` — works on both SCF 6.8.4+ (which dropped the legacy `wp acf field-group …` family in favor of the `wp scf json` namespace) and older ACF installs. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is `Array<{ ID, post_name, post_title, post_status, post_modified }>` parsed from wp-cli's JSON output (or an empty array on no results). wp-cli failures map to 'scf.command_failed'; missing wp-cli configuration surfaces as 'scf.not_configured'.",
2056
2068
  annotations: { idempotentHint: true },
2057
2069
  _meta: { idempotent: "true" },
@@ -2082,11 +2094,11 @@ server.registerTool("diviops_scf_field_group_list", {
2082
2094
  });
2083
2095
  return {
2084
2096
  content: [
2085
- { type: "text", text: serializeEnvelope(response) },
2097
+ { type: "text", text: serializeEnvelope(response, "diviops_scf_field_group_list") },
2086
2098
  ],
2087
2099
  };
2088
2100
  });
2089
- server.registerTool("diviops_scf_field_group_get", {
2101
+ registerLocalTool("diviops_scf_field_group_get", {
2090
2102
  description: "Fetch a single SCF/ACF field group from the `acf-field-group` post type — by ACF key (`group_abc123`, looked up via `post_name`) or by numeric WP post ID. Returns the WP post fields (post_name, post_title, post_content with serialized fields blob, post_status, post_modified). For the parsed/structured field tree including nested fields, use `diviops_scf_export --field-groups=<key> --stdout` instead. Read-only. SCF 6.8.4 dropped the legacy `wp acf field-group get` command, so this wrapper queries the post type directly via `wp post`. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is the parsed `wp post get --format=json` object. Unresolvable key (no row in the `acf-field-group` post type and not a numeric ID that wp-cli accepts) returns code 'not_found' with hint pointing to diviops_scf_field_group_list. wp-cli failures map to 'scf.command_failed'; missing wp-cli configuration surfaces as 'scf.not_configured'.",
2091
2103
  inputSchema: {
2092
2104
  key: z
@@ -2157,12 +2169,12 @@ server.registerTool("diviops_scf_field_group_get", {
2157
2169
  });
2158
2170
  return {
2159
2171
  content: [
2160
- { type: "text", text: serializeEnvelope(response) },
2172
+ { type: "text", text: serializeEnvelope(response, "diviops_scf_field_group_get") },
2161
2173
  ],
2162
2174
  };
2163
2175
  });
2164
2176
  // ── Connection ──────────────────────────────────────────────────────
2165
- server.registerTool("diviops_meta_ping", {
2177
+ registerLocalTool("diviops_meta_ping", {
2166
2178
  description: "Test the connection to the WordPress site and verify the Divi MCP plugin is active. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { connected: true, message: \"Connected to Divi <version>\" } and connection failure surfaces as { ok: false, error: { code: 'wp_error', message } } with the underlying transport message preserved.",
2167
2179
  annotations: { idempotentHint: true },
2168
2180
  _meta: { idempotent: "true" },
@@ -2176,11 +2188,11 @@ server.registerTool("diviops_meta_ping", {
2176
2188
  });
2177
2189
  return {
2178
2190
  content: [
2179
- { type: "text", text: serializeEnvelope(response) },
2191
+ { type: "text", text: serializeEnvelope(response, "diviops_meta_ping") },
2180
2192
  ],
2181
2193
  };
2182
2194
  });
2183
- server.registerTool("diviops_meta_info", {
2195
+ registerLocalTool("diviops_meta_info", {
2184
2196
  description: "Returns DiviOps MCP server identity, version, license type, and available capabilities. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }.",
2185
2197
  annotations: { idempotentHint: true },
2186
2198
  _meta: { idempotent: "true" },
@@ -2207,7 +2219,7 @@ server.registerTool("diviops_meta_info", {
2207
2219
  }));
2208
2220
  return {
2209
2221
  content: [
2210
- { type: "text", text: serializeEnvelope(response) },
2222
+ { type: "text", text: serializeEnvelope(response, "diviops_meta_info") },
2211
2223
  ],
2212
2224
  };
2213
2225
  });
@@ -2305,7 +2317,7 @@ function loadTemplates() {
2305
2317
  }
2306
2318
  const templates = loadTemplates();
2307
2319
  // Register a list tool so Claude can discover available templates
2308
- server.registerTool("diviops_template_list", {
2320
+ registerLocalTool("diviops_template_list", {
2309
2321
  description: "List available Divi page section templates. Each template contains verified block markup patterns that can be used as a base for page generation. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is an array of { name, description, customizable, requires_css }.",
2310
2322
  annotations: { idempotentHint: true },
2311
2323
  _meta: { idempotent: "true" },
@@ -2318,11 +2330,11 @@ server.registerTool("diviops_template_list", {
2318
2330
  })));
2319
2331
  return {
2320
2332
  content: [
2321
- { type: "text", text: serializeEnvelope(response) },
2333
+ { type: "text", text: serializeEnvelope(response, "diviops_template_list") },
2322
2334
  ],
2323
2335
  };
2324
2336
  });
2325
- server.registerTool("diviops_template_get", {
2337
+ registerLocalTool("diviops_template_get", {
2326
2338
  description: "Get a specific Divi template with verified block markup, customizable variables, and usage notes. Use this to generate pages based on proven patterns. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }. Missing template names return ok:false with code 'not_found' and error.data.available: string[] listing the registered template names.",
2327
2339
  inputSchema: {
2328
2340
  template_name: z
@@ -2341,7 +2353,7 @@ server.registerTool("diviops_template_get", {
2341
2353
  });
2342
2354
  return {
2343
2355
  content: [
2344
- { type: "text", text: serializeEnvelope(response) },
2356
+ { type: "text", text: serializeEnvelope(response, "diviops_template_get") },
2345
2357
  ],
2346
2358
  };
2347
2359
  });
@@ -2369,7 +2381,7 @@ registerPluginTool("diviops_variable_list", {
2369
2381
  const result = await wp.requestEnveloped("/variable/list", { params });
2370
2382
  return {
2371
2383
  content: [
2372
- { type: "text", text: serializeEnvelope(result) },
2384
+ { type: "text", text: serializeEnvelope(result, "diviops_variable_list") },
2373
2385
  ],
2374
2386
  };
2375
2387
  });
@@ -2443,7 +2455,7 @@ registerPluginTool("diviops_variable_create", {
2443
2455
  });
2444
2456
  return {
2445
2457
  content: [
2446
- { type: "text", text: serializeEnvelope(result) },
2458
+ { type: "text", text: serializeEnvelope(result, "diviops_variable_create") },
2447
2459
  ],
2448
2460
  };
2449
2461
  });
@@ -2613,7 +2625,7 @@ registerPluginTool("diviops_variable_create_fluid_system", {
2613
2625
  });
2614
2626
  return {
2615
2627
  content: [
2616
- { type: "text", text: serializeEnvelope(result) },
2628
+ { type: "text", text: serializeEnvelope(result, "diviops_variable_create_fluid_system") },
2617
2629
  ],
2618
2630
  };
2619
2631
  });
@@ -2643,7 +2655,7 @@ registerPluginTool("diviops_variable_delete", {
2643
2655
  });
2644
2656
  return {
2645
2657
  content: [
2646
- { type: "text", text: serializeEnvelope(result) },
2658
+ { type: "text", text: serializeEnvelope(result, "diviops_variable_delete") },
2647
2659
  ],
2648
2660
  };
2649
2661
  });
@@ -2655,7 +2667,7 @@ registerPluginTool("diviops_variable_scan_orphans", {
2655
2667
  const result = await wp.requestEnveloped("/variable/scan-orphans");
2656
2668
  return {
2657
2669
  content: [
2658
- { type: "text", text: serializeEnvelope(result) },
2670
+ { type: "text", text: serializeEnvelope(result, "diviops_variable_scan_orphans") },
2659
2671
  ],
2660
2672
  };
2661
2673
  });
@@ -2674,7 +2686,7 @@ registerPluginTool("diviops_variable_used_on_page", {
2674
2686
  const result = await wp.requestEnveloped(`/variable/used-on-page/${post_id}`);
2675
2687
  return {
2676
2688
  content: [
2677
- { type: "text", text: serializeEnvelope(result) },
2689
+ { type: "text", text: serializeEnvelope(result, "diviops_variable_used_on_page") },
2678
2690
  ],
2679
2691
  };
2680
2692
  });
@@ -2720,7 +2732,7 @@ registerPluginTool("diviops_meta_flush_cache", {
2720
2732
  });
2721
2733
  return {
2722
2734
  content: [
2723
- { type: "text", text: serializeEnvelope(result) },
2735
+ { type: "text", text: serializeEnvelope(result, "diviops_meta_flush_cache") },
2724
2736
  ],
2725
2737
  };
2726
2738
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diviops/mcp-server",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
4
4
  "description": "MCP server exposing Divi 5 Visual Builder as tools for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",