@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 +13 -1
- package/dist/envelope.d.ts +117 -0
- package/dist/envelope.js +171 -0
- package/dist/index.js +846 -428
- package/dist/wp-cli.d.ts +8 -0
- package/dist/wp-cli.js +75 -19
- package/dist/wp-client.d.ts +38 -0
- package/dist/wp-client.js +147 -2
- package/package.json +1 -1
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;
|
package/dist/envelope.js
ADDED
|
@@ -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
|
+
}
|