@diviops/mcp-server 1.4.0 → 1.4.1

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 CHANGED
@@ -296,9 +296,21 @@ Tool returns a preview of the changes it would make; caller reviews the diff, th
296
296
 
297
297
  > Both preview-then-commit tools share the same semantic pattern but use different parameter shapes (`mode` enum vs `dry_run` bool). Both predate this convention and stay as-is for caller compatibility. **New bulk tools should use the enum form** (`mode: "dry-run" | "apply"`) — it's more extensible if future modes are needed (`"interactive"`, `"selective"`, etc.) and keeps the interface consistent as the tool set grows.
298
298
 
299
+ ### Pattern B (variant) — universal `dry_run: boolean` on every write tool
300
+
301
+ Every mutating tool also accepts an optional `dry_run: boolean` (default `false`). When `true`, the response carries a uniform plan shape — `{ ok: true, data: { dry_run: true, plan: { summary, changes[, warnings] } } }` — and no state is mutated. Apply mode (`dry_run` omitted) keeps each tool's pre-existing response shape unchanged, so adding the parameter is non-breaking.
302
+
303
+ This complements Pattern B's `mode: dry-run/apply` convention on bulk operations:
304
+ - `mode: "dry-run"/"apply"` is the explicit two-round-trip contract for bulk tools where the preview *is* the value.
305
+ - `dry_run: true` is the lighter "preview before commit" knob available on every write tool, including single-item ones.
306
+
307
+ A handful of pre-existing tools predate the standard plan shape and keep their original response shapes for now: `preset_cleanup`, `preset_reassign`, `preset_delete`, `canvas_duplicate`, `page_trash`, `page_update_status`, `scf_sync`, `variable_create_fluid_system`.
308
+
309
+ `meta_wp_cli` and `scf_import` do not accept `dry_run` — `meta_wp_cli` is a raw passthrough (use explicit read-only commands instead) and SCF's upstream `wp scf json import` lacks a `--dry-run` flag (use `scf_sync --dry_run` for the SCF-on-disk preview path).
310
+
299
311
  ### Picking a pattern for a new tool
300
312
 
301
- Ask: **single item or many?** If single, Pattern A. If many, Pattern B.
313
+ Ask: **single item or many?** If single, Pattern A. If many, Pattern B (with `mode: "dry-run"/"apply"`). Either way, the universal `dry_run: boolean` knob is also available on every write tool — adding it on a new write tool is the default expectation.
302
314
 
303
315
  Don't introduce a third pattern (`confirmation_token`, session-based preview, etc.) unless a tool has a genuine need that neither A nor B covers — both patterns above are stateless and flexible enough for most cases.
304
316
 
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Standardized response envelope (#489).
3
+ *
4
+ * Every diviops tool — server-local or plugin-routed — returns:
5
+ * success: { ok: true, data: T }
6
+ * error: { ok: false, error: { code: string, message: string, hint?: string } }
7
+ *
8
+ * Adoption is per-namespace (#489 + per-namespace follow-ups). Tools that
9
+ * have not adopted yet still pass legacy raw payloads through to consumers;
10
+ * the helpers here coexist with that shape during the rollout window.
11
+ *
12
+ * HTTP status stays orthogonal to envelope shape on the wire — the plugin
13
+ * emits 200 on ok:true and the real status (400/404/409/412/500) on ok:false.
14
+ * The server-side `wp.request` raises on non-2xx today, so error envelopes
15
+ * arrive as thrown errors carrying the JSON body; `wrapResponse` is the
16
+ * normalizer that maps both branches to the typed `DiviopsResponse<T>`.
17
+ */
18
+ export type DiviopsSuccess<T> = {
19
+ ok: true;
20
+ data: T;
21
+ };
22
+ export type DiviopsErrorBody = {
23
+ code: string;
24
+ message: string;
25
+ hint?: string;
26
+ /**
27
+ * Optional structured payload attached to the error envelope. Used by
28
+ * tools whose failure mode carries machine-readable detail beyond a
29
+ * code/message pair — e.g. `meta_wp_cli` exposes
30
+ * `error.data = { exit_code, stdout, stderr }` so callers can branch
31
+ * on the wp-cli exit code without parsing the message string. Tools
32
+ * that don't need it omit the field entirely.
33
+ *
34
+ * Naming convention: when a namespace-specific error code attaches
35
+ * `data`, list the field shape in the tool's description and in
36
+ * tools.md "Response shape" so consumers know what to expect per
37
+ * code without runtime probing.
38
+ */
39
+ data?: unknown;
40
+ };
41
+ export type DiviopsFailure = {
42
+ ok: false;
43
+ error: DiviopsErrorBody;
44
+ };
45
+ export type DiviopsResponse<T> = DiviopsSuccess<T> | DiviopsFailure;
46
+ /**
47
+ * Standard error code vocabulary. Plugin and server emit only these codes
48
+ * for the matching condition; namespace-specific extensions use the
49
+ * `<namespace>.<reason>` form (e.g. `library.import_conflict`).
50
+ */
51
+ export declare const ErrorCodes: {
52
+ /** Target ID does not resolve. HTTP 404. */
53
+ readonly NOT_FOUND: "not_found";
54
+ /** Schema violation, malformed args. HTTP 400. */
55
+ readonly INVALID_INPUT: "invalid_input";
56
+ /** Underlying WordPress error (wraps WP_Error or REST framework error). HTTP 500. */
57
+ readonly WP_ERROR: "wp_error";
58
+ /** Divi-specific error (block parser, validator, etc.). HTTP 500. */
59
+ readonly DIVI_ERROR: "divi_error";
60
+ /** Plugin version below required for this tool (#486 handshake miss). HTTP 412. */
61
+ readonly CAPABILITY_MISSING: "capability_missing";
62
+ /** validate_blocks-detected shape error in submitted markup. HTTP 400. */
63
+ readonly VALIDATION_FAILED: "validation_failed";
64
+ /** Uniqueness collision (#542 / #543). HTTP 409. */
65
+ readonly CONFLICT: "conflict";
66
+ };
67
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes] | string;
68
+ /**
69
+ * Typed throw used inside server-local handlers to short-circuit with a
70
+ * specific envelope error code. `wrapResponse` catches it and re-emits.
71
+ */
72
+ export declare class DiviopsError extends Error {
73
+ readonly code: string;
74
+ readonly hint?: string;
75
+ readonly data?: unknown;
76
+ constructor(code: string, message: string, hint?: string, data?: unknown);
77
+ }
78
+ /**
79
+ * Throw a `DiviopsError`. Helper for ergonomics — `withCode(...)` reads
80
+ * better at call sites than `throw new DiviopsError(...)` and matches the
81
+ * kickoff's helper-API sketch. Pass `data` when the failure carries a
82
+ * structured payload (e.g. wp-cli exit code + captured streams).
83
+ */
84
+ export declare function withCode(code: string, message: string, hint?: string, data?: unknown): never;
85
+ /**
86
+ * Type guard: is `value` already shaped as a `DiviopsResponse`?
87
+ *
88
+ * Plugin-routed tools return the envelope; server-local tools may return
89
+ * raw values. `wrapResponse` uses this to avoid double-wrapping.
90
+ */
91
+ export declare function isEnveloped(value: unknown): value is DiviopsResponse<unknown>;
92
+ /**
93
+ * Run an async producer and normalize its outcome to a `DiviopsResponse<T>`.
94
+ *
95
+ * Behavior:
96
+ * - Producer returns an already-enveloped value → pass through unchanged
97
+ * (no double-wrap). Plugin-routed tools land here.
98
+ * - Producer returns a raw value → wrap as `{ok: true, data: <value>}`.
99
+ * Server-local tools land here.
100
+ * - Producer throws `DiviopsError` → emit `{ok: false, error: {code, message, hint?}}`.
101
+ * - Producer throws anything else → emit `{ok: false, error: {code: 'wp_error', message: e.message}}`.
102
+ * The fallback covers thrown HTTP errors from `wp.request` whose body
103
+ * is the plugin's envelope JSON; we attempt to parse the message and
104
+ * promote the embedded code/hint when present.
105
+ */
106
+ export declare function wrapResponse<T>(producer: () => Promise<T | DiviopsResponse<T>>): Promise<DiviopsResponse<T>>;
107
+ /**
108
+ * Map the data of a `DiviopsResponse` through `fn` (success branch only).
109
+ * Errors pass through unchanged. Use to post-process a wrapped result
110
+ * (e.g. shrink schema, project fields) without unwrapping by hand.
111
+ */
112
+ export declare function envelopeMap<T, U>(response: DiviopsResponse<T>, fn: (data: T) => U): DiviopsResponse<U>;
113
+ /**
114
+ * Serialize a `DiviopsResponse` as the JSON string an MCP tool emits in its
115
+ * `content[0].text` slot. Single emit point keeps the wire shape consistent.
116
+ */
117
+ export declare function serializeEnvelope<T>(response: DiviopsResponse<T>): string;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Standardized response envelope (#489).
3
+ *
4
+ * Every diviops tool — server-local or plugin-routed — returns:
5
+ * success: { ok: true, data: T }
6
+ * error: { ok: false, error: { code: string, message: string, hint?: string } }
7
+ *
8
+ * Adoption is per-namespace (#489 + per-namespace follow-ups). Tools that
9
+ * have not adopted yet still pass legacy raw payloads through to consumers;
10
+ * the helpers here coexist with that shape during the rollout window.
11
+ *
12
+ * HTTP status stays orthogonal to envelope shape on the wire — the plugin
13
+ * emits 200 on ok:true and the real status (400/404/409/412/500) on ok:false.
14
+ * The server-side `wp.request` raises on non-2xx today, so error envelopes
15
+ * arrive as thrown errors carrying the JSON body; `wrapResponse` is the
16
+ * normalizer that maps both branches to the typed `DiviopsResponse<T>`.
17
+ */
18
+ /**
19
+ * Standard error code vocabulary. Plugin and server emit only these codes
20
+ * for the matching condition; namespace-specific extensions use the
21
+ * `<namespace>.<reason>` form (e.g. `library.import_conflict`).
22
+ */
23
+ export const ErrorCodes = {
24
+ /** Target ID does not resolve. HTTP 404. */
25
+ NOT_FOUND: "not_found",
26
+ /** Schema violation, malformed args. HTTP 400. */
27
+ INVALID_INPUT: "invalid_input",
28
+ /** Underlying WordPress error (wraps WP_Error or REST framework error). HTTP 500. */
29
+ WP_ERROR: "wp_error",
30
+ /** Divi-specific error (block parser, validator, etc.). HTTP 500. */
31
+ DIVI_ERROR: "divi_error",
32
+ /** Plugin version below required for this tool (#486 handshake miss). HTTP 412. */
33
+ CAPABILITY_MISSING: "capability_missing",
34
+ /** validate_blocks-detected shape error in submitted markup. HTTP 400. */
35
+ VALIDATION_FAILED: "validation_failed",
36
+ /** Uniqueness collision (#542 / #543). HTTP 409. */
37
+ CONFLICT: "conflict",
38
+ };
39
+ /**
40
+ * Typed throw used inside server-local handlers to short-circuit with a
41
+ * specific envelope error code. `wrapResponse` catches it and re-emits.
42
+ */
43
+ export class DiviopsError extends Error {
44
+ code;
45
+ hint;
46
+ data;
47
+ constructor(code, message, hint, data) {
48
+ super(message);
49
+ this.name = "DiviopsError";
50
+ this.code = code;
51
+ this.hint = hint;
52
+ this.data = data;
53
+ }
54
+ }
55
+ /**
56
+ * Throw a `DiviopsError`. Helper for ergonomics — `withCode(...)` reads
57
+ * better at call sites than `throw new DiviopsError(...)` and matches the
58
+ * kickoff's helper-API sketch. Pass `data` when the failure carries a
59
+ * structured payload (e.g. wp-cli exit code + captured streams).
60
+ */
61
+ export function withCode(code, message, hint, data) {
62
+ throw new DiviopsError(code, message, hint, data);
63
+ }
64
+ /**
65
+ * Type guard: is `value` already shaped as a `DiviopsResponse`?
66
+ *
67
+ * Plugin-routed tools return the envelope; server-local tools may return
68
+ * raw values. `wrapResponse` uses this to avoid double-wrapping.
69
+ */
70
+ export function isEnveloped(value) {
71
+ if (typeof value !== "object" || value === null)
72
+ return false;
73
+ const v = value;
74
+ if (v.ok === true && "data" in v)
75
+ return true;
76
+ if (v.ok === false && typeof v.error === "object" && v.error !== null) {
77
+ const e = v.error;
78
+ return typeof e.code === "string" && typeof e.message === "string";
79
+ }
80
+ return false;
81
+ }
82
+ /**
83
+ * Run an async producer and normalize its outcome to a `DiviopsResponse<T>`.
84
+ *
85
+ * Behavior:
86
+ * - Producer returns an already-enveloped value → pass through unchanged
87
+ * (no double-wrap). Plugin-routed tools land here.
88
+ * - Producer returns a raw value → wrap as `{ok: true, data: <value>}`.
89
+ * Server-local tools land here.
90
+ * - Producer throws `DiviopsError` → emit `{ok: false, error: {code, message, hint?}}`.
91
+ * - Producer throws anything else → emit `{ok: false, error: {code: 'wp_error', message: e.message}}`.
92
+ * The fallback covers thrown HTTP errors from `wp.request` whose body
93
+ * is the plugin's envelope JSON; we attempt to parse the message and
94
+ * promote the embedded code/hint when present.
95
+ */
96
+ export async function wrapResponse(producer) {
97
+ try {
98
+ const result = await producer();
99
+ if (isEnveloped(result)) {
100
+ return result;
101
+ }
102
+ return { ok: true, data: result };
103
+ }
104
+ catch (e) {
105
+ if (e instanceof DiviopsError) {
106
+ const error = { code: e.code, message: e.message };
107
+ if (e.hint)
108
+ error.hint = e.hint;
109
+ if (e.data !== undefined)
110
+ error.data = e.data;
111
+ return { ok: false, error };
112
+ }
113
+ return { ok: false, error: parseThrownError(e) };
114
+ }
115
+ }
116
+ /**
117
+ * Extract envelope error info from a thrown value.
118
+ *
119
+ * `wp.request` throws Errors of the form
120
+ * `WordPress API error (NNN): <body>`
121
+ * where `<body>` may be the plugin's envelope JSON. We try to recover the
122
+ * embedded code/hint in that case so re-thrown plugin errors don't lose
123
+ * their structured shape on round-trip through `wrapResponse`.
124
+ */
125
+ function parseThrownError(e) {
126
+ const message = e instanceof Error ? e.message : String(e);
127
+ const bodyMatch = message.match(/^WordPress API error \(\d+\):\s*(.*)$/s);
128
+ if (bodyMatch) {
129
+ try {
130
+ const parsed = JSON.parse(bodyMatch[1]);
131
+ if (parsed &&
132
+ typeof parsed === "object" &&
133
+ parsed.ok === false &&
134
+ parsed.error &&
135
+ typeof parsed.error === "object" &&
136
+ typeof parsed.error.code === "string" &&
137
+ typeof parsed.error.message === "string") {
138
+ const out = {
139
+ code: parsed.error.code,
140
+ message: parsed.error.message,
141
+ };
142
+ if (typeof parsed.error.hint === "string")
143
+ out.hint = parsed.error.hint;
144
+ if (parsed.error.data !== undefined)
145
+ out.data = parsed.error.data;
146
+ return out;
147
+ }
148
+ }
149
+ catch {
150
+ // Body wasn't JSON, fall through to generic wp_error.
151
+ }
152
+ }
153
+ return { code: ErrorCodes.WP_ERROR, message };
154
+ }
155
+ /**
156
+ * Map the data of a `DiviopsResponse` through `fn` (success branch only).
157
+ * Errors pass through unchanged. Use to post-process a wrapped result
158
+ * (e.g. shrink schema, project fields) without unwrapping by hand.
159
+ */
160
+ export function envelopeMap(response, fn) {
161
+ if (response.ok)
162
+ return { ok: true, data: fn(response.data) };
163
+ return response;
164
+ }
165
+ /**
166
+ * Serialize a `DiviopsResponse` as the JSON string an MCP tool emits in its
167
+ * `content[0].text` slot. Single emit point keeps the wire shape consistent.
168
+ */
169
+ export function serializeEnvelope(response) {
170
+ return JSON.stringify(response);
171
+ }