@diviops/mcp-server 1.4.0 → 1.5.2

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.
@@ -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
+ }