elementor-mcp-agent 0.1.0 → 1.1.0
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 +125 -121
- package/dist/server.js +2335 -322
- package/dist/server.js.map +1 -1
- package/package.json +16 -5
package/dist/server.js
CHANGED
|
@@ -1,43 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
ListToolsRequestSchema,
|
|
12
|
-
ListResourcesRequestSchema,
|
|
13
|
-
ReadResourceRequestSchema
|
|
14
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
16
11
|
|
|
17
12
|
// src/config.ts
|
|
18
13
|
import { readFileSync } from "fs";
|
|
19
14
|
import { z } from "zod";
|
|
20
|
-
var SiteSchema = z.object({
|
|
21
|
-
id: z.string().min(1, "site id required"),
|
|
22
|
-
url: z.string().url("invalid site url"),
|
|
23
|
-
username: z.string().min(1),
|
|
24
|
-
application_password: z.string().min(20, "WP application password should be ~24 chars"),
|
|
25
|
-
ssh: z.object({
|
|
26
|
-
host: z.string(),
|
|
27
|
-
user: z.string(),
|
|
28
|
-
port: z.coerce.number().int().min(1).max(65535).default(22),
|
|
29
|
-
path: z.string().describe("WP root path on remote, e.g. ~/sites/example.com"),
|
|
30
|
-
key_path: z.string().optional().describe("absolute path to private key")
|
|
31
|
-
}).optional()
|
|
32
|
-
});
|
|
33
|
-
var ConfigSchema = z.object({
|
|
34
|
-
sites: z.array(SiteSchema).min(1, "at least one site is required"),
|
|
35
|
-
default_site_id: z.string().optional(),
|
|
36
|
-
rate_limit_per_minute: z.coerce.number().int().min(1).max(600).default(60),
|
|
37
|
-
confirmation_ttl_seconds: z.coerce.number().int().min(10).max(600).default(60),
|
|
38
|
-
log_level: z.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"]).default("info")
|
|
39
|
-
});
|
|
40
|
-
var cached = null;
|
|
41
15
|
function loadConfig() {
|
|
42
16
|
if (cached) return cached;
|
|
43
17
|
let raw;
|
|
@@ -111,47 +85,36 @@ function getSite(siteId) {
|
|
|
111
85
|
}
|
|
112
86
|
return s;
|
|
113
87
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
88
|
+
var SiteSchema, ConfigSchema, cached;
|
|
89
|
+
var init_config = __esm({
|
|
90
|
+
"src/config.ts"() {
|
|
91
|
+
"use strict";
|
|
92
|
+
SiteSchema = z.object({
|
|
93
|
+
id: z.string().min(1, "site id required"),
|
|
94
|
+
url: z.string().url("invalid site url"),
|
|
95
|
+
username: z.string().min(1),
|
|
96
|
+
application_password: z.string().min(20, "WP application password should be ~24 chars"),
|
|
97
|
+
ssh: z.object({
|
|
98
|
+
host: z.string(),
|
|
99
|
+
user: z.string(),
|
|
100
|
+
port: z.coerce.number().int().min(1).max(65535).default(22),
|
|
101
|
+
path: z.string().describe("WP root path on remote, e.g. ~/sites/example.com"),
|
|
102
|
+
key_path: z.string().optional().describe("absolute path to private key"),
|
|
103
|
+
wp_cli_path: z.string().optional().describe("Explicit wp-cli invocation prefix. Examples: 'wp' (default, if wp is in PATH), 'php ~/bin/wp.phar', '/usr/local/bin/wp'. Auto-detected if omitted.")
|
|
104
|
+
}).optional()
|
|
105
|
+
});
|
|
106
|
+
ConfigSchema = z.object({
|
|
107
|
+
sites: z.array(SiteSchema).min(1, "at least one site is required"),
|
|
108
|
+
default_site_id: z.string().optional(),
|
|
109
|
+
rate_limit_per_minute: z.coerce.number().int().min(1).max(600).default(60),
|
|
110
|
+
confirmation_ttl_seconds: z.coerce.number().int().min(10).max(600).default(60),
|
|
111
|
+
log_level: z.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"]).default("info")
|
|
112
|
+
});
|
|
113
|
+
cached = null;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
122
116
|
|
|
123
117
|
// src/throttle/token-bucket.ts
|
|
124
|
-
var TokenBucket = class {
|
|
125
|
-
constructor(capacityPerMinute) {
|
|
126
|
-
this.capacityPerMinute = capacityPerMinute;
|
|
127
|
-
this.tokens = capacityPerMinute;
|
|
128
|
-
this.refillPerMs = capacityPerMinute / 6e4;
|
|
129
|
-
}
|
|
130
|
-
capacityPerMinute;
|
|
131
|
-
tokens;
|
|
132
|
-
lastRefill = Date.now();
|
|
133
|
-
refillPerMs;
|
|
134
|
-
async acquire() {
|
|
135
|
-
while (this.tokens < 1) {
|
|
136
|
-
this.refill();
|
|
137
|
-
if (this.tokens < 1) await new Promise((r) => setTimeout(r, 80));
|
|
138
|
-
}
|
|
139
|
-
this.tokens -= 1;
|
|
140
|
-
}
|
|
141
|
-
refill() {
|
|
142
|
-
const now = Date.now();
|
|
143
|
-
this.tokens = Math.min(
|
|
144
|
-
this.capacityPerMinute,
|
|
145
|
-
this.tokens + (now - this.lastRefill) * this.refillPerMs
|
|
146
|
-
);
|
|
147
|
-
this.lastRefill = now;
|
|
148
|
-
}
|
|
149
|
-
get available() {
|
|
150
|
-
this.refill();
|
|
151
|
-
return Math.floor(this.tokens);
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
var buckets = /* @__PURE__ */ new Map();
|
|
155
118
|
function bucketFor(siteId, perMinute) {
|
|
156
119
|
let b = buckets.get(siteId);
|
|
157
120
|
if (!b) {
|
|
@@ -160,20 +123,45 @@ function bucketFor(siteId, perMinute) {
|
|
|
160
123
|
}
|
|
161
124
|
return b;
|
|
162
125
|
}
|
|
126
|
+
var TokenBucket, buckets;
|
|
127
|
+
var init_token_bucket = __esm({
|
|
128
|
+
"src/throttle/token-bucket.ts"() {
|
|
129
|
+
"use strict";
|
|
130
|
+
TokenBucket = class {
|
|
131
|
+
constructor(capacityPerMinute) {
|
|
132
|
+
this.capacityPerMinute = capacityPerMinute;
|
|
133
|
+
this.tokens = capacityPerMinute;
|
|
134
|
+
this.refillPerMs = capacityPerMinute / 6e4;
|
|
135
|
+
}
|
|
136
|
+
capacityPerMinute;
|
|
137
|
+
tokens;
|
|
138
|
+
lastRefill = Date.now();
|
|
139
|
+
refillPerMs;
|
|
140
|
+
async acquire() {
|
|
141
|
+
while (this.tokens < 1) {
|
|
142
|
+
this.refill();
|
|
143
|
+
if (this.tokens < 1) await new Promise((r) => setTimeout(r, 80));
|
|
144
|
+
}
|
|
145
|
+
this.tokens -= 1;
|
|
146
|
+
}
|
|
147
|
+
refill() {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
this.tokens = Math.min(
|
|
150
|
+
this.capacityPerMinute,
|
|
151
|
+
this.tokens + (now - this.lastRefill) * this.refillPerMs
|
|
152
|
+
);
|
|
153
|
+
this.lastRefill = now;
|
|
154
|
+
}
|
|
155
|
+
get available() {
|
|
156
|
+
this.refill();
|
|
157
|
+
return Math.floor(this.tokens);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
buckets = /* @__PURE__ */ new Map();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
163
|
|
|
164
164
|
// src/api/errors.ts
|
|
165
|
-
var WPError = class extends Error {
|
|
166
|
-
constructor(kind, code, message, raw) {
|
|
167
|
-
super(message);
|
|
168
|
-
this.kind = kind;
|
|
169
|
-
this.code = code;
|
|
170
|
-
this.raw = raw;
|
|
171
|
-
this.name = "WPError";
|
|
172
|
-
}
|
|
173
|
-
kind;
|
|
174
|
-
code;
|
|
175
|
-
raw;
|
|
176
|
-
};
|
|
177
165
|
function fromHttp(status, body, fallback = "WordPress API error") {
|
|
178
166
|
const raw = body;
|
|
179
167
|
const code = raw?.code ?? `http_${status}`;
|
|
@@ -186,17 +174,41 @@ function fromHttp(status, body, fallback = "WordPress API error") {
|
|
|
186
174
|
else if (status >= 500) kind = "server";
|
|
187
175
|
return new WPError(kind, code, msg, body);
|
|
188
176
|
}
|
|
177
|
+
var WPError;
|
|
178
|
+
var init_errors = __esm({
|
|
179
|
+
"src/api/errors.ts"() {
|
|
180
|
+
"use strict";
|
|
181
|
+
WPError = class extends Error {
|
|
182
|
+
constructor(kind, code, message, raw) {
|
|
183
|
+
super(message);
|
|
184
|
+
this.kind = kind;
|
|
185
|
+
this.code = code;
|
|
186
|
+
this.raw = raw;
|
|
187
|
+
this.name = "WPError";
|
|
188
|
+
}
|
|
189
|
+
kind;
|
|
190
|
+
code;
|
|
191
|
+
raw;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
});
|
|
189
195
|
|
|
190
196
|
// src/utils/logger.ts
|
|
191
197
|
import pino from "pino";
|
|
192
|
-
var logger
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
var logger;
|
|
199
|
+
var init_logger = __esm({
|
|
200
|
+
"src/utils/logger.ts"() {
|
|
201
|
+
"use strict";
|
|
202
|
+
logger = pino(
|
|
203
|
+
{
|
|
204
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
205
|
+
base: { name: "elementor-mcp-agent" }
|
|
206
|
+
},
|
|
207
|
+
// Critical: stdout is reserved for MCP JSON-RPC. All logs MUST go to stderr.
|
|
208
|
+
pino.destination(2)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
200
212
|
|
|
201
213
|
// src/api/wp-rest.ts
|
|
202
214
|
function authHeader(site) {
|
|
@@ -245,23 +257,443 @@ async function wpRequest(path, opts = {}) {
|
|
|
245
257
|
}
|
|
246
258
|
return parsed;
|
|
247
259
|
}
|
|
260
|
+
var init_wp_rest = __esm({
|
|
261
|
+
"src/api/wp-rest.ts"() {
|
|
262
|
+
"use strict";
|
|
263
|
+
init_config();
|
|
264
|
+
init_token_bucket();
|
|
265
|
+
init_errors();
|
|
266
|
+
init_logger();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// src/transport/ssh-wpcli.ts
|
|
271
|
+
import { spawn } from "child_process";
|
|
272
|
+
async function detectWpCliPath(site, sshOpts) {
|
|
273
|
+
if (site.ssh?.wp_cli_path) return site.ssh.wp_cli_path;
|
|
274
|
+
const cached2 = wpCliPathCache.get(site.id);
|
|
275
|
+
if (cached2) return cached2;
|
|
276
|
+
const probe = `command -v wp 2>/dev/null && echo wp || (test -f "$HOME/bin/wp.phar" && echo "php $HOME/bin/wp.phar") || (test -f "$HOME/wp-cli.phar" && echo "php $HOME/wp-cli.phar") || echo NONE`;
|
|
277
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
278
|
+
const result = await new Promise((resolve3) => {
|
|
279
|
+
const child = spawn3("ssh", [...sshOpts, `${site.ssh.user}@${site.ssh.host}`, probe], { stdio: ["ignore", "pipe", "pipe"] });
|
|
280
|
+
let out = "";
|
|
281
|
+
child.stdout.on("data", (b) => {
|
|
282
|
+
out += b.toString();
|
|
283
|
+
});
|
|
284
|
+
child.on("close", () => resolve3(out.trim().split("\n").pop() ?? "NONE"));
|
|
285
|
+
});
|
|
286
|
+
const detected = result === "NONE" ? "wp" : result;
|
|
287
|
+
wpCliPathCache.set(site.id, detected);
|
|
288
|
+
return detected;
|
|
289
|
+
}
|
|
290
|
+
async function sshWpCli(site, wpArgs, opts = {}) {
|
|
291
|
+
if (!site.ssh) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Site '${site.id}' has no SSH configuration. WP-CLI tools require SSH access. Add an "ssh" object to the site config with at least {host, user, path}.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const { host, user, port, path: wpPath, key_path } = site.ssh;
|
|
297
|
+
const timeout = opts.timeout_ms ?? 6e4;
|
|
298
|
+
const sshArgs = [
|
|
299
|
+
"-o",
|
|
300
|
+
"StrictHostKeyChecking=no",
|
|
301
|
+
"-o",
|
|
302
|
+
"BatchMode=yes",
|
|
303
|
+
"-o",
|
|
304
|
+
`ConnectTimeout=${Math.min(15, Math.floor(timeout / 1e3))}`,
|
|
305
|
+
"-p",
|
|
306
|
+
String(port ?? 22)
|
|
307
|
+
];
|
|
308
|
+
if (key_path) sshArgs.push("-i", key_path);
|
|
309
|
+
const wpCmd = await detectWpCliPath(site, sshArgs);
|
|
310
|
+
sshArgs.push(`${user}@${host}`);
|
|
311
|
+
const remoteCmd = `${wpCmd} --path=${shellEscape(wpPath)} ${wpArgs}`;
|
|
312
|
+
sshArgs.push(remoteCmd);
|
|
313
|
+
logger.debug({ site_id: site.id, cmd: remoteCmd }, "ssh wp-cli");
|
|
314
|
+
const t0 = Date.now();
|
|
315
|
+
return new Promise((resolve3, reject) => {
|
|
316
|
+
const child = spawn("ssh", sshArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
317
|
+
let stdout = "";
|
|
318
|
+
let stderr = "";
|
|
319
|
+
const killer = setTimeout(() => {
|
|
320
|
+
child.kill("SIGKILL");
|
|
321
|
+
reject(new Error(`SSH wp-cli timed out after ${timeout}ms: ${remoteCmd}`));
|
|
322
|
+
}, timeout);
|
|
323
|
+
child.stdout.on("data", (b) => {
|
|
324
|
+
stdout += b.toString();
|
|
325
|
+
});
|
|
326
|
+
child.stderr.on("data", (b) => {
|
|
327
|
+
stderr += b.toString();
|
|
328
|
+
});
|
|
329
|
+
child.on("error", (e) => {
|
|
330
|
+
clearTimeout(killer);
|
|
331
|
+
reject(e);
|
|
332
|
+
});
|
|
333
|
+
child.on("close", (code) => {
|
|
334
|
+
clearTimeout(killer);
|
|
335
|
+
const duration_ms = Date.now() - t0;
|
|
336
|
+
const cleanedStderr = stderr.split("\n").filter((l) => !l.includes("post-quantum") && !l.includes("openssh.com/pq") && !l.includes("decrypt later") && !l.includes("This session may be") && !l.includes("server may need to be")).join("\n").trim();
|
|
337
|
+
resolve3({
|
|
338
|
+
stdout: stdout.trim(),
|
|
339
|
+
stderr: cleanedStderr,
|
|
340
|
+
exitCode: code ?? -1,
|
|
341
|
+
duration_ms,
|
|
342
|
+
command: remoteCmd
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function shellEscape(s) {
|
|
348
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
349
|
+
}
|
|
350
|
+
function isDestructiveWpCli(args) {
|
|
351
|
+
return DESTRUCTIVE_WPCLI_PATTERNS.some((p) => p.test(args));
|
|
352
|
+
}
|
|
353
|
+
var wpCliPathCache, DESTRUCTIVE_WPCLI_PATTERNS;
|
|
354
|
+
var init_ssh_wpcli = __esm({
|
|
355
|
+
"src/transport/ssh-wpcli.ts"() {
|
|
356
|
+
"use strict";
|
|
357
|
+
init_logger();
|
|
358
|
+
wpCliPathCache = /* @__PURE__ */ new Map();
|
|
359
|
+
DESTRUCTIVE_WPCLI_PATTERNS = [
|
|
360
|
+
/\bdelete-all\b/i,
|
|
361
|
+
/\bdelete\b(?!.*--dry-run)/i,
|
|
362
|
+
/\bdrop\b/i,
|
|
363
|
+
/\bdb\s+(reset|drop)\b/i,
|
|
364
|
+
/\bpost\s+delete\b/i,
|
|
365
|
+
/\boption\s+delete\b/i,
|
|
366
|
+
/\bplugin\s+(deactivate|uninstall)\b/i,
|
|
367
|
+
/\bsearch-replace\b(?!.*--dry-run)/i,
|
|
368
|
+
/\buser\s+delete\b/i
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// src/elementor/backup.ts
|
|
374
|
+
var backup_exports = {};
|
|
375
|
+
__export(backup_exports, {
|
|
376
|
+
fullBackup: () => fullBackup,
|
|
377
|
+
listBackups: () => listBackups,
|
|
378
|
+
restoreBackup: () => restoreBackup,
|
|
379
|
+
restoreFromFile: () => restoreFromFile
|
|
380
|
+
});
|
|
381
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync as readFileSync2 } from "fs";
|
|
382
|
+
import { tmpdir } from "os";
|
|
383
|
+
import { join } from "path";
|
|
384
|
+
async function fullBackup(siteId, postId, opts = {}) {
|
|
385
|
+
const current = await wpRequest(
|
|
386
|
+
`/wp/v2/pages/${postId}?context=edit&_fields=meta,title`,
|
|
387
|
+
{ siteId }
|
|
388
|
+
);
|
|
389
|
+
const data_raw_v = current.meta?._elementor_data;
|
|
390
|
+
const data_raw = typeof data_raw_v === "string" ? data_raw_v : JSON.stringify(data_raw_v ?? []);
|
|
391
|
+
const settings_raw_v = current.meta?._elementor_page_settings;
|
|
392
|
+
const settings_raw = typeof settings_raw_v === "string" ? settings_raw_v : JSON.stringify(settings_raw_v ?? {});
|
|
393
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
394
|
+
const meta_key = `_elementor_data_backup_${ts}`;
|
|
395
|
+
const page_settings_meta_key = `_elementor_page_settings_backup_${ts}`;
|
|
396
|
+
const size_bytes = data_raw.length + settings_raw.length;
|
|
397
|
+
const site = getSite(siteId);
|
|
398
|
+
let method = "file";
|
|
399
|
+
if (site.ssh && !opts.force_file_only) {
|
|
400
|
+
try {
|
|
401
|
+
const setDataCmd = `post meta update ${postId} ${meta_key} ${shellQuote(data_raw)}`;
|
|
402
|
+
const setSettingsCmd = `post meta update ${postId} ${page_settings_meta_key} ${shellQuote(settings_raw)}`;
|
|
403
|
+
const r1 = await sshWpCli(site, setDataCmd, { timeout_ms: 3e4 });
|
|
404
|
+
if (r1.exitCode !== 0) throw new Error(`wp-cli postmeta data set failed: ${r1.stderr}`);
|
|
405
|
+
const r2 = await sshWpCli(site, setSettingsCmd, { timeout_ms: 3e4 });
|
|
406
|
+
if (r2.exitCode !== 0) throw new Error(`wp-cli postmeta settings set failed: ${r2.stderr}`);
|
|
407
|
+
method = "wp-cli";
|
|
408
|
+
} catch (e) {
|
|
409
|
+
logger.warn({ err: e.message }, "WP-CLI backup failed, falling back to file");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
let file_path;
|
|
413
|
+
if (opts.to_file || method !== "wp-cli") {
|
|
414
|
+
if (!existsSync(BACKUP_DIR)) mkdirSync(BACKUP_DIR, { recursive: true });
|
|
415
|
+
const safeSiteId = (siteId ?? "default").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
416
|
+
const filename = `${safeSiteId}_page${postId}_${ts}.json`;
|
|
417
|
+
file_path = join(BACKUP_DIR, filename);
|
|
418
|
+
writeFileSync(file_path, JSON.stringify({
|
|
419
|
+
site_id: siteId,
|
|
420
|
+
page_id: postId,
|
|
421
|
+
title: current.title.rendered,
|
|
422
|
+
timestamp: ts,
|
|
423
|
+
_elementor_data: data_raw,
|
|
424
|
+
_elementor_page_settings: settings_raw
|
|
425
|
+
}, null, 2));
|
|
426
|
+
if (method !== "wp-cli") method = "file";
|
|
427
|
+
logger.info({ file_path }, "backup written to file");
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
meta_key,
|
|
431
|
+
page_settings_meta_key,
|
|
432
|
+
file_path,
|
|
433
|
+
size_bytes,
|
|
434
|
+
method
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async function restoreBackup(siteId, postId, data_meta_key, settings_meta_key) {
|
|
438
|
+
const site = getSite(siteId);
|
|
439
|
+
if (!site.ssh) {
|
|
440
|
+
throw new Error("Restoring from postmeta backup requires SSH (WP-CLI). For file backups, use restoreFromFile.");
|
|
441
|
+
}
|
|
442
|
+
const r1 = await sshWpCli(site, `post meta get ${postId} ${data_meta_key}`, { timeout_ms: 3e4 });
|
|
443
|
+
if (r1.exitCode !== 0) throw new Error(`Backup '${data_meta_key}' not found: ${r1.stderr}`);
|
|
444
|
+
const data_value = r1.stdout;
|
|
445
|
+
if (!data_value) throw new Error(`Backup '${data_meta_key}' is empty`);
|
|
446
|
+
let settings_value;
|
|
447
|
+
if (settings_meta_key) {
|
|
448
|
+
const r2 = await sshWpCli(site, `post meta get ${postId} ${settings_meta_key}`, { timeout_ms: 3e4 });
|
|
449
|
+
if (r2.exitCode === 0 && r2.stdout) settings_value = r2.stdout;
|
|
450
|
+
}
|
|
451
|
+
const writeData2 = await sshWpCli(site, `post meta update ${postId} _elementor_data ${shellQuote(data_value)}`, { timeout_ms: 3e4 });
|
|
452
|
+
if (writeData2.exitCode !== 0) throw new Error(`Restore write failed: ${writeData2.stderr}`);
|
|
453
|
+
if (settings_value !== void 0) {
|
|
454
|
+
await sshWpCli(site, `post meta update ${postId} _elementor_page_settings ${shellQuote(settings_value)}`, { timeout_ms: 3e4 });
|
|
455
|
+
}
|
|
456
|
+
return { restored: true, method: "wp-cli" };
|
|
457
|
+
}
|
|
458
|
+
async function listBackups(siteId, postId) {
|
|
459
|
+
const site = getSite(siteId);
|
|
460
|
+
if (!site.ssh) {
|
|
461
|
+
throw new Error("Listing postmeta backups requires SSH (WP-CLI). REST API doesn't expose unregistered custom postmeta keys.");
|
|
462
|
+
}
|
|
463
|
+
const r = await sshWpCli(site, `post meta list ${postId} --format=json --fields=meta_key`, { timeout_ms: 3e4 });
|
|
464
|
+
if (r.exitCode !== 0) throw new Error(`wp-cli post meta list failed: ${r.stderr}`);
|
|
465
|
+
const all = JSON.parse(r.stdout);
|
|
466
|
+
const dataBackupKeys = all.filter((m) => m.meta_key.startsWith("_elementor_data_backup_"));
|
|
467
|
+
const settingsBackupKeys = new Set(all.filter((m) => m.meta_key.startsWith("_elementor_page_settings_backup_")).map((m) => m.meta_key));
|
|
468
|
+
return dataBackupKeys.map((m) => {
|
|
469
|
+
const ts = m.meta_key.replace("_elementor_data_backup_", "");
|
|
470
|
+
const expectedSettingsKey = `_elementor_page_settings_backup_${ts}`;
|
|
471
|
+
return {
|
|
472
|
+
meta_key: m.meta_key,
|
|
473
|
+
settings_key: settingsBackupKeys.has(expectedSettingsKey) ? expectedSettingsKey : void 0,
|
|
474
|
+
timestamp: ts
|
|
475
|
+
};
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
async function restoreFromFile(siteId, postId, file_path) {
|
|
479
|
+
if (!existsSync(file_path)) throw new Error(`Backup file not found: ${file_path}`);
|
|
480
|
+
const j = JSON.parse(readFileSync2(file_path, "utf8"));
|
|
481
|
+
if (!j._elementor_data) throw new Error("Backup file missing _elementor_data");
|
|
482
|
+
const site = getSite(siteId);
|
|
483
|
+
if (site.ssh) {
|
|
484
|
+
await sshWpCli(site, `post meta update ${postId} _elementor_data ${shellQuote(j._elementor_data)}`, { timeout_ms: 3e4 });
|
|
485
|
+
if (j._elementor_page_settings) {
|
|
486
|
+
await sshWpCli(site, `post meta update ${postId} _elementor_page_settings ${shellQuote(j._elementor_page_settings)}`, { timeout_ms: 3e4 });
|
|
487
|
+
}
|
|
488
|
+
return { restored: true, method: "wp-cli" };
|
|
489
|
+
}
|
|
490
|
+
await wpRequest(`/wp/v2/pages/${postId}`, {
|
|
491
|
+
siteId,
|
|
492
|
+
method: "PUT",
|
|
493
|
+
body: {
|
|
494
|
+
meta: {
|
|
495
|
+
_elementor_data: j._elementor_data,
|
|
496
|
+
...j._elementor_page_settings ? { _elementor_page_settings: j._elementor_page_settings } : {}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
return { restored: true, method: "rest" };
|
|
501
|
+
}
|
|
502
|
+
function shellQuote(s) {
|
|
503
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
504
|
+
}
|
|
505
|
+
var BACKUP_DIR;
|
|
506
|
+
var init_backup = __esm({
|
|
507
|
+
"src/elementor/backup.ts"() {
|
|
508
|
+
"use strict";
|
|
509
|
+
init_wp_rest();
|
|
510
|
+
init_ssh_wpcli();
|
|
511
|
+
init_config();
|
|
512
|
+
init_logger();
|
|
513
|
+
BACKUP_DIR = process.env.ELEMENTOR_MCP_BACKUP_DIR ?? join(tmpdir(), "elementor-mcp-backups");
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// src/elementor/css-flush.ts
|
|
518
|
+
var css_flush_exports = {};
|
|
519
|
+
__export(css_flush_exports, {
|
|
520
|
+
flushCSS: () => flushCSS
|
|
521
|
+
});
|
|
522
|
+
async function flushCSS(siteId, postId) {
|
|
523
|
+
try {
|
|
524
|
+
const url = postId ? `/elementor/v1/css?id=${postId}&action=regenerate` : `/elementor/v1/css?action=regenerate`;
|
|
525
|
+
await wpRequest(url, { siteId, method: "POST" });
|
|
526
|
+
return { method: "rest" };
|
|
527
|
+
} catch (e) {
|
|
528
|
+
logger.debug({ err: e.message }, "REST css flush failed, trying next");
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
const site = getSite(siteId);
|
|
532
|
+
if (site.ssh) {
|
|
533
|
+
const r = await sshWpCli(site, "elementor flush-css");
|
|
534
|
+
if (r.exitCode === 0) return { method: "wp-cli", details: r.stdout };
|
|
535
|
+
}
|
|
536
|
+
} catch (e) {
|
|
537
|
+
logger.debug({ err: e.message }, "WP-CLI css flush failed, trying next");
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const site = getSite(siteId);
|
|
541
|
+
if (site.ssh) {
|
|
542
|
+
await sshWpCli(site, "option delete _elementor_global_css", { timeout_ms: 3e4 });
|
|
543
|
+
await sshWpCli(site, "post meta delete-all _elementor_css", { timeout_ms: 3e4 });
|
|
544
|
+
return { method: "option-delete" };
|
|
545
|
+
}
|
|
546
|
+
} catch (e) {
|
|
547
|
+
logger.debug({ err: e.message }, "WP-CLI option-delete failed, trying next");
|
|
548
|
+
}
|
|
549
|
+
if (postId) {
|
|
550
|
+
try {
|
|
551
|
+
await wpRequest(`/wp/v2/pages/${postId}`, {
|
|
552
|
+
siteId,
|
|
553
|
+
method: "PUT",
|
|
554
|
+
body: { date: (/* @__PURE__ */ new Date()).toISOString() }
|
|
555
|
+
});
|
|
556
|
+
return { method: "resave" };
|
|
557
|
+
} catch (e) {
|
|
558
|
+
logger.warn({ err: e.message }, "Resave fallback also failed");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { method: "none", details: "All CSS flush strategies failed" };
|
|
562
|
+
}
|
|
563
|
+
var init_css_flush = __esm({
|
|
564
|
+
"src/elementor/css-flush.ts"() {
|
|
565
|
+
"use strict";
|
|
566
|
+
init_wp_rest();
|
|
567
|
+
init_ssh_wpcli();
|
|
568
|
+
init_config();
|
|
569
|
+
init_logger();
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// src/elementor/policies.ts
|
|
574
|
+
var policies_exports = {};
|
|
575
|
+
__export(policies_exports, {
|
|
576
|
+
FORBIDDEN_WPCLI_PATTERNS: () => FORBIDDEN_WPCLI_PATTERNS,
|
|
577
|
+
POLICIES: () => POLICIES,
|
|
578
|
+
isForbiddenWpCli: () => isForbiddenWpCli
|
|
579
|
+
});
|
|
580
|
+
function isForbiddenWpCli(args) {
|
|
581
|
+
for (const p of FORBIDDEN_WPCLI_PATTERNS) {
|
|
582
|
+
if (p.test(args)) return { forbidden: true, reason: `Pattern ${p} is hard-blocked.` };
|
|
583
|
+
}
|
|
584
|
+
return { forbidden: false };
|
|
585
|
+
}
|
|
586
|
+
var POLICIES, FORBIDDEN_WPCLI_PATTERNS;
|
|
587
|
+
var init_policies = __esm({
|
|
588
|
+
"src/elementor/policies.ts"() {
|
|
589
|
+
"use strict";
|
|
590
|
+
POLICIES = {
|
|
591
|
+
// Always backup _elementor_data BEFORE any write
|
|
592
|
+
BACKUP_BEFORE_WRITE: true,
|
|
593
|
+
// Always backup _elementor_page_settings too (it carries page-level CSS, fonts, layout)
|
|
594
|
+
BACKUP_PAGE_SETTINGS: true,
|
|
595
|
+
// Re-validate the JSON after a programmatic edit; abort + auto-restore if invalid
|
|
596
|
+
VALIDATE_JSON_AFTER_EDIT: true,
|
|
597
|
+
// Block edits to global widgets unless caller explicitly opts in
|
|
598
|
+
BLOCK_GLOBAL_WIDGET_WRITES_BY_DEFAULT: true,
|
|
599
|
+
// Confirmation token TTL — short for destructive ops
|
|
600
|
+
CONFIRMATION_TTL_SECONDS: 60,
|
|
601
|
+
// Stricter TTL for global widget ops (less margin for mistakes)
|
|
602
|
+
GLOBAL_WIDGET_CONFIRMATION_TTL_SECONDS: 30,
|
|
603
|
+
// CSS flush is non-optional after _elementor_data writes
|
|
604
|
+
FLUSH_CSS_AFTER_WRITE: true,
|
|
605
|
+
// Maximum size of a single page's _elementor_data we'll touch (sanity bound)
|
|
606
|
+
MAX_ELEMENTOR_DATA_BYTES: 5 * 1024 * 1024,
|
|
607
|
+
// 5 MB
|
|
608
|
+
// wp-cli commands matching this pattern require confirmation
|
|
609
|
+
WP_CLI_DESTRUCTIVE_REQUIRES_CONFIRM: true
|
|
610
|
+
};
|
|
611
|
+
FORBIDDEN_WPCLI_PATTERNS = [
|
|
612
|
+
// Hard 'no' regardless of confirmation — we never accept these
|
|
613
|
+
/\brm\s+-rf\b/i,
|
|
614
|
+
/\bsudo\b/i,
|
|
615
|
+
/\bdb\s+reset\s+--yes\b/i,
|
|
616
|
+
/\bdb\s+drop\s+--yes\b/i
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// src/utils/confirmation.ts
|
|
622
|
+
var confirmation_exports = {};
|
|
623
|
+
__export(confirmation_exports, {
|
|
624
|
+
_clearAllConfirmations: () => _clearAllConfirmations,
|
|
625
|
+
consumeConfirmation: () => consumeConfirmation,
|
|
626
|
+
issueConfirmation: () => issueConfirmation
|
|
627
|
+
});
|
|
628
|
+
import { randomBytes } from "crypto";
|
|
629
|
+
function issueConfirmation(intent, payload, ttlSeconds) {
|
|
630
|
+
const token = randomBytes(8).toString("hex");
|
|
631
|
+
pending.set(token, {
|
|
632
|
+
token,
|
|
633
|
+
intent,
|
|
634
|
+
payload,
|
|
635
|
+
expiresAt: Date.now() + ttlSeconds * 1e3
|
|
636
|
+
});
|
|
637
|
+
return token;
|
|
638
|
+
}
|
|
639
|
+
function consumeConfirmation(token, expectedIntent) {
|
|
640
|
+
const c = pending.get(token);
|
|
641
|
+
if (!c) return null;
|
|
642
|
+
pending.delete(token);
|
|
643
|
+
if (c.expiresAt < Date.now()) return null;
|
|
644
|
+
if (c.intent !== expectedIntent) return null;
|
|
645
|
+
return c;
|
|
646
|
+
}
|
|
647
|
+
function _clearAllConfirmations() {
|
|
648
|
+
pending.clear();
|
|
649
|
+
}
|
|
650
|
+
var pending;
|
|
651
|
+
var init_confirmation = __esm({
|
|
652
|
+
"src/utils/confirmation.ts"() {
|
|
653
|
+
"use strict";
|
|
654
|
+
pending = /* @__PURE__ */ new Map();
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// src/server.ts
|
|
659
|
+
init_config();
|
|
660
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
661
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
662
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
663
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
664
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
665
|
+
import {
|
|
666
|
+
CallToolRequestSchema,
|
|
667
|
+
ListToolsRequestSchema,
|
|
668
|
+
ListResourcesRequestSchema,
|
|
669
|
+
ReadResourceRequestSchema
|
|
670
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
671
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
672
|
+
|
|
673
|
+
// src/tools/sites.ts
|
|
674
|
+
import { z as z2 } from "zod";
|
|
675
|
+
|
|
676
|
+
// src/types/tool.ts
|
|
677
|
+
function defineTool(def) {
|
|
678
|
+
return def;
|
|
679
|
+
}
|
|
248
680
|
|
|
249
681
|
// src/tools/sites.ts
|
|
682
|
+
init_config();
|
|
683
|
+
init_wp_rest();
|
|
250
684
|
var listSitesTool = defineTool({
|
|
251
685
|
name: "list_sites",
|
|
252
|
-
description: "List every WordPress site configured
|
|
686
|
+
description: "List every WordPress site configured. Best called first in a session.",
|
|
253
687
|
inputSchema: z2.object({}),
|
|
254
688
|
outputSchema: z2.object({
|
|
255
689
|
total: z2.number(),
|
|
256
690
|
default_site_id: z2.string().optional(),
|
|
257
|
-
sites: z2.array(
|
|
258
|
-
z2.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
})
|
|
264
|
-
)
|
|
691
|
+
sites: z2.array(z2.object({
|
|
692
|
+
id: z2.string(),
|
|
693
|
+
url: z2.string(),
|
|
694
|
+
username: z2.string(),
|
|
695
|
+
has_ssh: z2.boolean()
|
|
696
|
+
}))
|
|
265
697
|
}),
|
|
266
698
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
267
699
|
async handler() {
|
|
@@ -269,21 +701,14 @@ var listSitesTool = defineTool({
|
|
|
269
701
|
return {
|
|
270
702
|
total: cfg.sites.length,
|
|
271
703
|
default_site_id: cfg.default_site_id ?? cfg.sites[0]?.id,
|
|
272
|
-
sites: cfg.sites.map((s) => ({
|
|
273
|
-
id: s.id,
|
|
274
|
-
url: s.url,
|
|
275
|
-
username: s.username,
|
|
276
|
-
has_ssh: !!s.ssh
|
|
277
|
-
}))
|
|
704
|
+
sites: cfg.sites.map((s) => ({ id: s.id, url: s.url, username: s.username, has_ssh: !!s.ssh }))
|
|
278
705
|
};
|
|
279
706
|
}
|
|
280
707
|
});
|
|
281
708
|
var pingSiteTool = defineTool({
|
|
282
709
|
name: "ping_site",
|
|
283
|
-
description: "Verify connectivity + authentication to a WordPress site.
|
|
284
|
-
inputSchema: z2.object({
|
|
285
|
-
site_id: z2.string().optional().describe("Site id from list_sites. Defaults to the default site.")
|
|
286
|
-
}),
|
|
710
|
+
description: "Verify connectivity + authentication to a WordPress site. Returns user identity + WP/Elementor/Elementor Pro versions if accessible.",
|
|
711
|
+
inputSchema: z2.object({ site_id: z2.string().optional() }),
|
|
287
712
|
outputSchema: z2.object({
|
|
288
713
|
ok: z2.boolean(),
|
|
289
714
|
site_id: z2.string(),
|
|
@@ -298,29 +723,19 @@ var pingSiteTool = defineTool({
|
|
|
298
723
|
async handler(input) {
|
|
299
724
|
const site = getSite(input.site_id);
|
|
300
725
|
try {
|
|
301
|
-
const me = await wpRequest(
|
|
302
|
-
"/wp/v2/users/me?context=edit",
|
|
303
|
-
{ siteId: site.id }
|
|
304
|
-
);
|
|
726
|
+
const me = await wpRequest("/wp/v2/users/me?context=edit", { siteId: site.id });
|
|
305
727
|
let wp_version;
|
|
306
728
|
try {
|
|
307
|
-
const health = await wpRequest(
|
|
308
|
-
"/wp-site-health/v1/info",
|
|
309
|
-
{ siteId: site.id }
|
|
310
|
-
);
|
|
729
|
+
const health = await wpRequest("/wp-site-health/v1/info", { siteId: site.id });
|
|
311
730
|
wp_version = health.wordpress?.version;
|
|
312
731
|
} catch {
|
|
313
732
|
}
|
|
314
733
|
let elementor_version;
|
|
315
734
|
let elementor_pro_version;
|
|
316
735
|
try {
|
|
317
|
-
const plugins = await wpRequest(
|
|
318
|
-
"/wp/v2/plugins",
|
|
319
|
-
{ siteId: site.id }
|
|
320
|
-
);
|
|
736
|
+
const plugins = await wpRequest("/wp/v2/plugins", { siteId: site.id });
|
|
321
737
|
for (const p of plugins) {
|
|
322
|
-
if (p.plugin.startsWith("elementor/") && p.plugin.endsWith("/elementor.php"))
|
|
323
|
-
elementor_version = p.version;
|
|
738
|
+
if (p.plugin.startsWith("elementor/") && p.plugin.endsWith("/elementor.php")) elementor_version = p.version;
|
|
324
739
|
if (p.plugin.startsWith("elementor-pro/")) elementor_pro_version = p.version;
|
|
325
740
|
}
|
|
326
741
|
} catch {
|
|
@@ -335,18 +750,85 @@ var pingSiteTool = defineTool({
|
|
|
335
750
|
user: { id: me.id, name: me.name, roles: me.roles }
|
|
336
751
|
};
|
|
337
752
|
} catch (e) {
|
|
338
|
-
return {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
753
|
+
return { ok: false, site_id: site.id, url: site.url, error: e.message };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
var siteHealthTool = defineTool({
|
|
758
|
+
name: "site_health",
|
|
759
|
+
description: "Comprehensive site health snapshot: WP/PHP/Elementor versions, disk space (if SSH), plugin count, theme info. Aggregates multiple REST calls into a single overview.",
|
|
760
|
+
inputSchema: z2.object({ site_id: z2.string().optional() }),
|
|
761
|
+
outputSchema: z2.object({
|
|
762
|
+
site_id: z2.string(),
|
|
763
|
+
url: z2.string(),
|
|
764
|
+
wp_version: z2.string().optional(),
|
|
765
|
+
php_version: z2.string().optional(),
|
|
766
|
+
elementor_version: z2.string().optional(),
|
|
767
|
+
elementor_pro_version: z2.string().optional(),
|
|
768
|
+
active_theme: z2.string().optional(),
|
|
769
|
+
plugins_total: z2.number().optional(),
|
|
770
|
+
plugins_active: z2.number().optional(),
|
|
771
|
+
plugins_outdated: z2.number().optional(),
|
|
772
|
+
elementor_pages_count: z2.number().optional(),
|
|
773
|
+
errors: z2.array(z2.string())
|
|
774
|
+
}),
|
|
775
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
776
|
+
async handler(input) {
|
|
777
|
+
const site = getSite(input.site_id);
|
|
778
|
+
const errors = [];
|
|
779
|
+
let wp_version, php_version, elementor_version, elementor_pro_version, active_theme;
|
|
780
|
+
let plugins_total, plugins_active, plugins_outdated, elementor_pages_count;
|
|
781
|
+
try {
|
|
782
|
+
const info = await wpRequest("/wp-site-health/v1/info", { siteId: site.id });
|
|
783
|
+
wp_version = info.wordpress?.version;
|
|
784
|
+
php_version = info["wp-server"]?.fields?.php_version?.value;
|
|
785
|
+
} catch (e) {
|
|
786
|
+
errors.push("site-health: " + e.message);
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
const plugins = await wpRequest("/wp/v2/plugins", { siteId: site.id });
|
|
790
|
+
plugins_total = plugins.length;
|
|
791
|
+
plugins_active = plugins.filter((p) => p.status === "active").length;
|
|
792
|
+
for (const p of plugins) {
|
|
793
|
+
if (p.plugin.startsWith("elementor/") && p.plugin.endsWith("/elementor.php")) elementor_version = p.version;
|
|
794
|
+
if (p.plugin.startsWith("elementor-pro/")) elementor_pro_version = p.version;
|
|
795
|
+
}
|
|
796
|
+
} catch (e) {
|
|
797
|
+
errors.push("plugins: " + e.message);
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
const themes = await wpRequest("/wp/v2/themes", { siteId: site.id });
|
|
801
|
+
const active = themes.find((t) => t.status === "active");
|
|
802
|
+
active_theme = active?.name?.raw;
|
|
803
|
+
} catch (e) {
|
|
804
|
+
errors.push("themes: " + e.message);
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
const pages = await wpRequest("/wp/v2/pages", { siteId: site.id, query: { meta_key: "_elementor_edit_mode", meta_value: "builder", per_page: 1, _fields: "id" } });
|
|
808
|
+
elementor_pages_count = pages.length > 0 ? -1 : 0;
|
|
809
|
+
} catch (e) {
|
|
810
|
+
errors.push("pages: " + e.message);
|
|
344
811
|
}
|
|
812
|
+
return {
|
|
813
|
+
site_id: site.id,
|
|
814
|
+
url: site.url,
|
|
815
|
+
wp_version,
|
|
816
|
+
php_version,
|
|
817
|
+
elementor_version,
|
|
818
|
+
elementor_pro_version,
|
|
819
|
+
active_theme,
|
|
820
|
+
plugins_total,
|
|
821
|
+
plugins_active,
|
|
822
|
+
plugins_outdated,
|
|
823
|
+
elementor_pages_count,
|
|
824
|
+
errors
|
|
825
|
+
};
|
|
345
826
|
}
|
|
346
827
|
});
|
|
347
828
|
|
|
348
829
|
// src/tools/pages.ts
|
|
349
830
|
import { z as z3 } from "zod";
|
|
831
|
+
init_wp_rest();
|
|
350
832
|
|
|
351
833
|
// src/elementor/data-parser.ts
|
|
352
834
|
function parseElementorData(raw) {
|
|
@@ -372,6 +854,12 @@ function* walkElements(data, path = [], depth = 0) {
|
|
|
372
854
|
}
|
|
373
855
|
}
|
|
374
856
|
}
|
|
857
|
+
function findElementById(data, id) {
|
|
858
|
+
for (const { element } of walkElements(data)) {
|
|
859
|
+
if (element.id === id) return element;
|
|
860
|
+
}
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
375
863
|
function findReplaceInWidgets(data, find, replace, options = {}) {
|
|
376
864
|
let count = 0;
|
|
377
865
|
const flags = options.caseSensitive ? "g" : "gi";
|
|
@@ -422,59 +910,101 @@ function summarize(data) {
|
|
|
422
910
|
return { totalElements, sections, containers, columns, widgets, byWidgetType, maxDepth };
|
|
423
911
|
}
|
|
424
912
|
|
|
425
|
-
// src/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
913
|
+
// src/tools/pages.ts
|
|
914
|
+
init_backup();
|
|
915
|
+
init_css_flush();
|
|
916
|
+
|
|
917
|
+
// src/elementor/validator.ts
|
|
918
|
+
init_policies();
|
|
919
|
+
function validateElementorData(input) {
|
|
920
|
+
const errors = [];
|
|
921
|
+
const warnings = [];
|
|
922
|
+
if (typeof input === "string" && input.length > POLICIES.MAX_ELEMENTOR_DATA_BYTES) {
|
|
923
|
+
errors.push(`_elementor_data exceeds ${POLICIES.MAX_ELEMENTOR_DATA_BYTES} bytes (got ${input.length})`);
|
|
924
|
+
return { valid: false, errors, warnings };
|
|
925
|
+
}
|
|
926
|
+
let data;
|
|
927
|
+
try {
|
|
928
|
+
data = parseElementorData(input);
|
|
929
|
+
} catch (e) {
|
|
930
|
+
errors.push(`JSON parse failed: ${e.message}`);
|
|
931
|
+
return { valid: false, errors, warnings };
|
|
932
|
+
}
|
|
933
|
+
function validateElement(el, depth, path) {
|
|
934
|
+
const here = [...path, el.id ?? "?"];
|
|
935
|
+
if (typeof el.id !== "string" || el.id.length === 0) {
|
|
936
|
+
errors.push(`element at ${here.join(".")}: missing or empty id`);
|
|
937
|
+
}
|
|
938
|
+
if (!["section", "container", "column", "widget"].includes(el.elType)) {
|
|
939
|
+
errors.push(`element ${el.id}: unknown elType "${el.elType}"`);
|
|
940
|
+
}
|
|
941
|
+
if (el.elType === "widget" && (typeof el.widgetType !== "string" || el.widgetType.length === 0)) {
|
|
942
|
+
errors.push(`widget ${el.id}: missing widgetType`);
|
|
943
|
+
}
|
|
944
|
+
if (el.settings === null || el.settings === void 0 || typeof el.settings !== "object" || Array.isArray(el.settings)) {
|
|
945
|
+
errors.push(`element ${el.id}: settings must be an object (got ${typeof el.settings})`);
|
|
946
|
+
}
|
|
947
|
+
if (depth > 20) warnings.push(`element ${el.id}: depth ${depth} is unusually deep`);
|
|
948
|
+
if (Array.isArray(el.elements)) {
|
|
949
|
+
for (const child of el.elements) validateElement(child, depth + 1, here);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (!Array.isArray(data)) {
|
|
953
|
+
errors.push("Top-level must be an array");
|
|
954
|
+
return { valid: false, errors, warnings };
|
|
955
|
+
}
|
|
956
|
+
for (const el of data) validateElement(el, 0, []);
|
|
957
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/elementor/globals.ts
|
|
961
|
+
init_wp_rest();
|
|
962
|
+
async function listGlobalWidgets(siteId) {
|
|
963
|
+
const items = await wpRequest("/wp/v2/elementor_library", {
|
|
432
964
|
siteId,
|
|
433
|
-
|
|
434
|
-
|
|
965
|
+
query: {
|
|
966
|
+
context: "edit",
|
|
967
|
+
per_page: 100,
|
|
968
|
+
_fields: "id,title,meta"
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
const widgetsOnly = items.filter((t) => t.meta?._elementor_template_type === "widget");
|
|
972
|
+
return widgetsOnly.map((t) => {
|
|
973
|
+
const data = parseElementorData(t.meta?._elementor_data ?? "[]");
|
|
974
|
+
let widget_type;
|
|
975
|
+
for (const { element } of walkElements(data)) {
|
|
976
|
+
if (element.elType === "widget") {
|
|
977
|
+
widget_type = element.widgetType;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
template_id: t.id,
|
|
983
|
+
title: t.title.rendered,
|
|
984
|
+
widget_type
|
|
985
|
+
};
|
|
435
986
|
});
|
|
436
|
-
return { meta_key, size_bytes: raw.length };
|
|
437
987
|
}
|
|
438
|
-
|
|
988
|
+
function findGlobalReferences(data) {
|
|
989
|
+
const out = [];
|
|
439
990
|
try {
|
|
440
|
-
|
|
441
|
-
|
|
991
|
+
for (const { element } of walkElements(parseElementorData(data))) {
|
|
992
|
+
if (element.elType === "widget" && element.widgetType === "global") {
|
|
993
|
+
const tid = element.settings.template_id ?? element.settings.templateID;
|
|
994
|
+
if (typeof tid === "number") out.push({ widget_id: element.id, template_id: tid });
|
|
995
|
+
}
|
|
996
|
+
}
|
|
442
997
|
} catch {
|
|
443
|
-
await wpRequest(`/wp/v2/pages/${postId}`, {
|
|
444
|
-
siteId,
|
|
445
|
-
method: "PUT",
|
|
446
|
-
body: { date: (/* @__PURE__ */ new Date()).toISOString() }
|
|
447
|
-
});
|
|
448
|
-
return { method: "resave" };
|
|
449
998
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
// src/utils/confirmation.ts
|
|
453
|
-
import { randomBytes } from "crypto";
|
|
454
|
-
var pending = /* @__PURE__ */ new Map();
|
|
455
|
-
function issueConfirmation(intent, payload, ttlSeconds) {
|
|
456
|
-
const token = randomBytes(8).toString("hex");
|
|
457
|
-
pending.set(token, {
|
|
458
|
-
token,
|
|
459
|
-
intent,
|
|
460
|
-
payload,
|
|
461
|
-
expiresAt: Date.now() + ttlSeconds * 1e3
|
|
462
|
-
});
|
|
463
|
-
return token;
|
|
464
|
-
}
|
|
465
|
-
function consumeConfirmation(token, expectedIntent) {
|
|
466
|
-
const c = pending.get(token);
|
|
467
|
-
if (!c) return null;
|
|
468
|
-
pending.delete(token);
|
|
469
|
-
if (c.expiresAt < Date.now()) return null;
|
|
470
|
-
if (c.intent !== expectedIntent) return null;
|
|
471
|
-
return c;
|
|
999
|
+
return out;
|
|
472
1000
|
}
|
|
473
1001
|
|
|
474
1002
|
// src/tools/pages.ts
|
|
1003
|
+
init_confirmation();
|
|
1004
|
+
init_policies();
|
|
475
1005
|
var listElementorPagesTool = defineTool({
|
|
476
1006
|
name: "list_elementor_pages",
|
|
477
|
-
description: "List pages
|
|
1007
|
+
description: "List pages built with Elementor (have _elementor_edit_mode = 'builder'). Returns id, title, slug, status, modified date.",
|
|
478
1008
|
inputSchema: z3.object({
|
|
479
1009
|
site_id: z3.string().optional(),
|
|
480
1010
|
per_page: z3.number().int().min(1).max(100).default(25),
|
|
@@ -482,16 +1012,14 @@ var listElementorPagesTool = defineTool({
|
|
|
482
1012
|
}),
|
|
483
1013
|
outputSchema: z3.object({
|
|
484
1014
|
total: z3.number(),
|
|
485
|
-
pages: z3.array(
|
|
486
|
-
z3.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
})
|
|
494
|
-
)
|
|
1015
|
+
pages: z3.array(z3.object({
|
|
1016
|
+
id: z3.number(),
|
|
1017
|
+
title: z3.string(),
|
|
1018
|
+
slug: z3.string(),
|
|
1019
|
+
status: z3.string(),
|
|
1020
|
+
link: z3.string(),
|
|
1021
|
+
modified: z3.string()
|
|
1022
|
+
}))
|
|
495
1023
|
}),
|
|
496
1024
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
497
1025
|
async handler(input) {
|
|
@@ -521,11 +1049,11 @@ var listElementorPagesTool = defineTool({
|
|
|
521
1049
|
});
|
|
522
1050
|
var readPageElementorTool = defineTool({
|
|
523
1051
|
name: "read_page_elementor",
|
|
524
|
-
description: "Fetch
|
|
1052
|
+
description: "Fetch a page's Elementor data structure summary. With verbose=true returns the full parsed tree (potentially MBs).",
|
|
525
1053
|
inputSchema: z3.object({
|
|
526
1054
|
site_id: z3.string().optional(),
|
|
527
1055
|
page_id: z3.number().int().positive(),
|
|
528
|
-
verbose: z3.boolean().default(false)
|
|
1056
|
+
verbose: z3.boolean().default(false)
|
|
529
1057
|
}),
|
|
530
1058
|
outputSchema: z3.object({
|
|
531
1059
|
page_id: z3.number(),
|
|
@@ -539,6 +1067,7 @@ var readPageElementorTool = defineTool({
|
|
|
539
1067
|
maxDepth: z3.number(),
|
|
540
1068
|
byWidgetType: z3.record(z3.number())
|
|
541
1069
|
}),
|
|
1070
|
+
global_references: z3.array(z3.object({ widget_id: z3.string(), template_id: z3.number() })),
|
|
542
1071
|
data: z3.array(z3.any()).optional()
|
|
543
1072
|
}),
|
|
544
1073
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
@@ -549,113 +1078,426 @@ var readPageElementorTool = defineTool({
|
|
|
549
1078
|
);
|
|
550
1079
|
const raw = page.meta?._elementor_data ?? "[]";
|
|
551
1080
|
const data = parseElementorData(raw);
|
|
552
|
-
const summary = summarize(data);
|
|
553
1081
|
return {
|
|
554
1082
|
page_id: page.id,
|
|
555
1083
|
title: page.title.rendered,
|
|
556
|
-
summary,
|
|
1084
|
+
summary: summarize(data),
|
|
1085
|
+
global_references: findGlobalReferences(raw),
|
|
557
1086
|
data: input.verbose ? data : void 0
|
|
558
1087
|
};
|
|
559
1088
|
}
|
|
560
1089
|
});
|
|
1090
|
+
var listWidgetsInPageTool = defineTool({
|
|
1091
|
+
name: "list_widgets_in_page",
|
|
1092
|
+
description: "Flat list of every widget in a page with id, type, parent path, and an excerpt of the first text setting (for spot-checking before find/replace).",
|
|
1093
|
+
inputSchema: z3.object({
|
|
1094
|
+
site_id: z3.string().optional(),
|
|
1095
|
+
page_id: z3.number().int().positive(),
|
|
1096
|
+
widget_type: z3.string().optional()
|
|
1097
|
+
}),
|
|
1098
|
+
outputSchema: z3.object({
|
|
1099
|
+
page_id: z3.number(),
|
|
1100
|
+
total: z3.number(),
|
|
1101
|
+
widgets: z3.array(z3.object({
|
|
1102
|
+
widget_id: z3.string(),
|
|
1103
|
+
widget_type: z3.string(),
|
|
1104
|
+
path: z3.array(z3.string()),
|
|
1105
|
+
excerpt: z3.string().optional()
|
|
1106
|
+
}))
|
|
1107
|
+
}),
|
|
1108
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1109
|
+
async handler(input) {
|
|
1110
|
+
const page = await wpRequest(
|
|
1111
|
+
`/wp/v2/pages/${input.page_id}?context=edit&_fields=id,meta`,
|
|
1112
|
+
{ siteId: input.site_id }
|
|
1113
|
+
);
|
|
1114
|
+
const data = parseElementorData(page.meta?._elementor_data ?? "[]");
|
|
1115
|
+
const widgets = [];
|
|
1116
|
+
for (const { element, path } of walkElements(data)) {
|
|
1117
|
+
if (element.elType !== "widget") continue;
|
|
1118
|
+
if (input.widget_type && element.widgetType !== input.widget_type) continue;
|
|
1119
|
+
let excerpt;
|
|
1120
|
+
for (const v of Object.values(element.settings ?? {})) {
|
|
1121
|
+
if (typeof v === "string" && v.length > 0) {
|
|
1122
|
+
excerpt = v.replace(/<[^>]+>/g, "").slice(0, 80);
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
widgets.push({
|
|
1127
|
+
widget_id: element.id,
|
|
1128
|
+
widget_type: element.widgetType ?? "unknown",
|
|
1129
|
+
path: path.slice(0, -1),
|
|
1130
|
+
excerpt
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
return { page_id: page.id, total: widgets.length, widgets };
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
var listGlobalWidgetsTool = defineTool({
|
|
1137
|
+
name: "list_global_widgets",
|
|
1138
|
+
description: "List all global widgets on a site (Elementor library entries of type 'widget'). These are shared across pages \u2014 editing one affects every page using it.",
|
|
1139
|
+
inputSchema: z3.object({ site_id: z3.string().optional() }),
|
|
1140
|
+
outputSchema: z3.object({
|
|
1141
|
+
total: z3.number(),
|
|
1142
|
+
globals: z3.array(z3.object({
|
|
1143
|
+
template_id: z3.number(),
|
|
1144
|
+
title: z3.string(),
|
|
1145
|
+
widget_type: z3.string().optional()
|
|
1146
|
+
}))
|
|
1147
|
+
}),
|
|
1148
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1149
|
+
async handler(input) {
|
|
1150
|
+
const globals = await listGlobalWidgets(input.site_id);
|
|
1151
|
+
return { total: globals.length, globals };
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
var preflightCheckTool = defineTool({
|
|
1155
|
+
name: "preflight_check",
|
|
1156
|
+
description: "Validate a page is safe to edit. Checks: page exists, is Elementor-built, data parses cleanly, references valid global widgets, isn't currently locked by another editor.",
|
|
1157
|
+
inputSchema: z3.object({
|
|
1158
|
+
site_id: z3.string().optional(),
|
|
1159
|
+
page_id: z3.number().int().positive()
|
|
1160
|
+
}),
|
|
1161
|
+
outputSchema: z3.object({
|
|
1162
|
+
safe_to_edit: z3.boolean(),
|
|
1163
|
+
page_id: z3.number(),
|
|
1164
|
+
title: z3.string(),
|
|
1165
|
+
issues: z3.array(z3.string()),
|
|
1166
|
+
warnings: z3.array(z3.string()),
|
|
1167
|
+
data_bytes: z3.number(),
|
|
1168
|
+
global_widget_references: z3.number()
|
|
1169
|
+
}),
|
|
1170
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1171
|
+
async handler(input) {
|
|
1172
|
+
const issues = [];
|
|
1173
|
+
const warnings = [];
|
|
1174
|
+
let title = "";
|
|
1175
|
+
let data_bytes = 0;
|
|
1176
|
+
let global_widget_references = 0;
|
|
1177
|
+
try {
|
|
1178
|
+
const page = await wpRequest(
|
|
1179
|
+
`/wp/v2/pages/${input.page_id}?context=edit`,
|
|
1180
|
+
{ siteId: input.site_id }
|
|
1181
|
+
);
|
|
1182
|
+
title = page.title.rendered;
|
|
1183
|
+
const raw = page.meta?._elementor_data ?? "[]";
|
|
1184
|
+
data_bytes = raw.length;
|
|
1185
|
+
if (page.meta?._elementor_edit_mode !== "builder") {
|
|
1186
|
+
issues.push("Page is not in Elementor builder mode");
|
|
1187
|
+
}
|
|
1188
|
+
const v = validateElementorData(raw);
|
|
1189
|
+
if (!v.valid) issues.push(...v.errors);
|
|
1190
|
+
warnings.push(...v.warnings);
|
|
1191
|
+
global_widget_references = findGlobalReferences(raw).length;
|
|
1192
|
+
if (global_widget_references > 0) {
|
|
1193
|
+
warnings.push(`Page references ${global_widget_references} global widget(s). Edits via find_replace will NOT affect the globals themselves \u2014 modify the global template directly if needed.`);
|
|
1194
|
+
}
|
|
1195
|
+
if (data_bytes > POLICIES.MAX_ELEMENTOR_DATA_BYTES) {
|
|
1196
|
+
issues.push(`Elementor data exceeds policy max (${POLICIES.MAX_ELEMENTOR_DATA_BYTES} bytes)`);
|
|
1197
|
+
}
|
|
1198
|
+
} catch (e) {
|
|
1199
|
+
issues.push(`Cannot fetch page: ${e.message}`);
|
|
1200
|
+
}
|
|
1201
|
+
return { safe_to_edit: issues.length === 0, page_id: input.page_id, title, issues, warnings, data_bytes, global_widget_references };
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
561
1204
|
var findReplaceTool = defineTool({
|
|
562
1205
|
name: "elementor_find_replace",
|
|
563
|
-
description: "Find/replace plain text in every widget
|
|
1206
|
+
description: "Find/replace plain text in every widget on one page. TWO-CALL FLOW: dry-run returns match_count + detailed widget hits + confirmation_token. Second call with token applies the change with auto-backup + JSON validation + auto-rollback if validation fails + CSS flush.",
|
|
564
1207
|
inputSchema: z3.object({
|
|
565
1208
|
site_id: z3.string().optional(),
|
|
566
1209
|
page_id: z3.number().int().positive(),
|
|
567
1210
|
find: z3.string().min(1),
|
|
568
1211
|
replace: z3.string(),
|
|
569
|
-
widget_type: z3.string().optional()
|
|
1212
|
+
widget_type: z3.string().optional(),
|
|
570
1213
|
case_sensitive: z3.boolean().default(false),
|
|
571
|
-
|
|
1214
|
+
backup_to_file: z3.boolean().default(false).describe("Also dump backup to /tmp/elementor-mcp-backups/"),
|
|
1215
|
+
confirmation: z3.string().optional()
|
|
572
1216
|
}),
|
|
573
1217
|
outputSchema: z3.object({
|
|
574
|
-
mode: z3.enum(["dry_run", "applied"]),
|
|
1218
|
+
mode: z3.enum(["dry_run", "applied", "rolled_back"]),
|
|
575
1219
|
page_id: z3.number(),
|
|
576
1220
|
match_count: z3.number(),
|
|
1221
|
+
affected_widgets: z3.array(z3.object({
|
|
1222
|
+
widget_id: z3.string(),
|
|
1223
|
+
widget_type: z3.string(),
|
|
1224
|
+
before: z3.string(),
|
|
1225
|
+
after: z3.string()
|
|
1226
|
+
})).optional(),
|
|
577
1227
|
confirmation_token: z3.string().optional(),
|
|
578
1228
|
expires_in_seconds: z3.number().optional(),
|
|
579
1229
|
backup_meta_key: z3.string().optional(),
|
|
580
|
-
|
|
1230
|
+
backup_file: z3.string().optional(),
|
|
1231
|
+
css_flush: z3.string().optional(),
|
|
1232
|
+
validation_error: z3.string().optional()
|
|
581
1233
|
}),
|
|
582
1234
|
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
583
1235
|
async handler(input) {
|
|
584
|
-
const cfg = loadConfig();
|
|
585
1236
|
const page = await wpRequest(
|
|
586
|
-
`/wp/v2/pages/${input.page_id}?context=edit`,
|
|
1237
|
+
`/wp/v2/pages/${input.page_id}?context=edit&_fields=id,meta`,
|
|
587
1238
|
{ siteId: input.site_id }
|
|
588
1239
|
);
|
|
589
1240
|
const raw = page.meta?._elementor_data ?? "[]";
|
|
590
|
-
const
|
|
591
|
-
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(
|
|
1241
|
+
const dryData = parseElementorData(raw);
|
|
1242
|
+
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(dryData)), input.find, input.replace, {
|
|
1243
|
+
widgetType: input.widget_type,
|
|
1244
|
+
caseSensitive: input.case_sensitive
|
|
1245
|
+
});
|
|
1246
|
+
const affected = [];
|
|
1247
|
+
const beforeData = parseElementorData(raw);
|
|
1248
|
+
const afterCopy = JSON.parse(JSON.stringify(beforeData));
|
|
1249
|
+
findReplaceInWidgets(afterCopy, input.find, input.replace, {
|
|
592
1250
|
widgetType: input.widget_type,
|
|
593
1251
|
caseSensitive: input.case_sensitive
|
|
594
1252
|
});
|
|
1253
|
+
function collectAffected(orig, modified) {
|
|
1254
|
+
const beforeMap = /* @__PURE__ */ new Map();
|
|
1255
|
+
for (const { element } of walkElements(orig)) {
|
|
1256
|
+
if (element.elType !== "widget") continue;
|
|
1257
|
+
const firstStr = Object.values(element.settings ?? {}).find((v) => typeof v === "string");
|
|
1258
|
+
if (firstStr) beforeMap.set(element.id, { type: element.widgetType ?? "?", first: firstStr });
|
|
1259
|
+
}
|
|
1260
|
+
for (const { element } of walkElements(modified)) {
|
|
1261
|
+
if (element.elType !== "widget") continue;
|
|
1262
|
+
const firstStr = Object.values(element.settings ?? {}).find((v) => typeof v === "string");
|
|
1263
|
+
const b = beforeMap.get(element.id);
|
|
1264
|
+
if (b && firstStr && b.first !== firstStr) {
|
|
1265
|
+
affected.push({
|
|
1266
|
+
widget_id: element.id,
|
|
1267
|
+
widget_type: element.widgetType ?? "?",
|
|
1268
|
+
before: b.first.slice(0, 120),
|
|
1269
|
+
after: firstStr.slice(0, 120)
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
collectAffected(beforeData, afterCopy);
|
|
595
1275
|
if (!input.confirmation) {
|
|
596
1276
|
if (dry.replacementCount === 0) {
|
|
597
|
-
return {
|
|
1277
|
+
return {
|
|
1278
|
+
mode: "dry_run",
|
|
1279
|
+
page_id: input.page_id,
|
|
1280
|
+
match_count: 0,
|
|
1281
|
+
affected_widgets: []
|
|
1282
|
+
};
|
|
598
1283
|
}
|
|
599
1284
|
const token = issueConfirmation(
|
|
600
1285
|
"elementor_find_replace",
|
|
601
1286
|
{ page_id: input.page_id, find: input.find, replace: input.replace },
|
|
602
|
-
|
|
1287
|
+
POLICIES.CONFIRMATION_TTL_SECONDS
|
|
603
1288
|
);
|
|
604
1289
|
return {
|
|
605
1290
|
mode: "dry_run",
|
|
606
1291
|
page_id: input.page_id,
|
|
607
1292
|
match_count: dry.replacementCount,
|
|
1293
|
+
affected_widgets: affected.slice(0, 25),
|
|
608
1294
|
confirmation_token: token,
|
|
609
|
-
expires_in_seconds:
|
|
1295
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
610
1296
|
};
|
|
611
1297
|
}
|
|
612
1298
|
const conf = consumeConfirmation(input.confirmation, "elementor_find_replace");
|
|
613
1299
|
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
614
|
-
const
|
|
615
|
-
if (
|
|
1300
|
+
const o = conf.payload;
|
|
1301
|
+
if (o.page_id !== input.page_id || o.find !== input.find || o.replace !== input.replace) {
|
|
616
1302
|
throw new Error("Confirmation parameters don't match the original dry-run");
|
|
617
1303
|
}
|
|
618
|
-
const backup = await
|
|
619
|
-
const
|
|
1304
|
+
const backup = await fullBackup(input.site_id, input.page_id, { to_file: input.backup_to_file });
|
|
1305
|
+
const newData = parseElementorData(raw);
|
|
1306
|
+
findReplaceInWidgets(newData, input.find, input.replace, {
|
|
620
1307
|
widgetType: input.widget_type,
|
|
621
1308
|
caseSensitive: input.case_sensitive
|
|
622
1309
|
});
|
|
1310
|
+
const serialized = serializeElementorData(newData);
|
|
1311
|
+
const validation = validateElementorData(serialized);
|
|
1312
|
+
if (!validation.valid) {
|
|
1313
|
+
return {
|
|
1314
|
+
mode: "rolled_back",
|
|
1315
|
+
page_id: input.page_id,
|
|
1316
|
+
match_count: dry.replacementCount,
|
|
1317
|
+
affected_widgets: affected.slice(0, 25),
|
|
1318
|
+
backup_meta_key: backup.meta_key,
|
|
1319
|
+
validation_error: validation.errors.join("; ")
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
623
1322
|
await wpRequest(`/wp/v2/pages/${input.page_id}`, {
|
|
624
1323
|
siteId: input.site_id,
|
|
625
1324
|
method: "PUT",
|
|
626
|
-
body: { meta: { _elementor_data:
|
|
1325
|
+
body: { meta: { _elementor_data: serialized } }
|
|
627
1326
|
});
|
|
628
|
-
const flush = await
|
|
1327
|
+
const flush = POLICIES.FLUSH_CSS_AFTER_WRITE ? await flushCSS(input.site_id, input.page_id) : { method: "none" };
|
|
629
1328
|
return {
|
|
630
1329
|
mode: "applied",
|
|
631
1330
|
page_id: input.page_id,
|
|
632
|
-
match_count:
|
|
1331
|
+
match_count: dry.replacementCount,
|
|
1332
|
+
affected_widgets: affected.slice(0, 25),
|
|
633
1333
|
backup_meta_key: backup.meta_key,
|
|
1334
|
+
backup_file: backup.file_path,
|
|
1335
|
+
css_flush: flush.method
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
var listElementorBackupsTool = defineTool({
|
|
1340
|
+
name: "list_elementor_backups",
|
|
1341
|
+
description: "List timestamped backups of a page's Elementor data (created by previous edit ops). Use restore_elementor_backup with one of these meta keys to roll back.",
|
|
1342
|
+
inputSchema: z3.object({
|
|
1343
|
+
site_id: z3.string().optional(),
|
|
1344
|
+
page_id: z3.number().int().positive()
|
|
1345
|
+
}),
|
|
1346
|
+
outputSchema: z3.object({
|
|
1347
|
+
page_id: z3.number(),
|
|
1348
|
+
total: z3.number(),
|
|
1349
|
+
backups: z3.array(z3.object({
|
|
1350
|
+
meta_key: z3.string(),
|
|
1351
|
+
settings_key: z3.string().optional(),
|
|
1352
|
+
timestamp: z3.string()
|
|
1353
|
+
}))
|
|
1354
|
+
}),
|
|
1355
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1356
|
+
async handler(input) {
|
|
1357
|
+
const backups = await listBackups(input.site_id, input.page_id);
|
|
1358
|
+
return {
|
|
1359
|
+
page_id: input.page_id,
|
|
1360
|
+
total: backups.length,
|
|
1361
|
+
backups
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
var restoreElementorBackupTool = defineTool({
|
|
1366
|
+
name: "restore_elementor_backup",
|
|
1367
|
+
description: "Restore a page's _elementor_data and _elementor_page_settings from a backup created by a previous edit. TWO-CALL FLOW with confirmation token.",
|
|
1368
|
+
inputSchema: z3.object({
|
|
1369
|
+
site_id: z3.string().optional(),
|
|
1370
|
+
page_id: z3.number().int().positive(),
|
|
1371
|
+
backup_meta_key: z3.string().describe("From list_elementor_backups."),
|
|
1372
|
+
settings_meta_key: z3.string().optional(),
|
|
1373
|
+
confirmation: z3.string().optional()
|
|
1374
|
+
}),
|
|
1375
|
+
outputSchema: z3.object({
|
|
1376
|
+
mode: z3.enum(["dry_run", "restored"]),
|
|
1377
|
+
page_id: z3.number(),
|
|
1378
|
+
backup_meta_key: z3.string(),
|
|
1379
|
+
settings_meta_key: z3.string().optional(),
|
|
1380
|
+
confirmation_token: z3.string().optional(),
|
|
1381
|
+
expires_in_seconds: z3.number().optional(),
|
|
1382
|
+
css_flush: z3.string().optional(),
|
|
1383
|
+
pre_restore_backup_meta_key: z3.string().optional()
|
|
1384
|
+
}),
|
|
1385
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1386
|
+
async handler(input) {
|
|
1387
|
+
if (!input.confirmation) {
|
|
1388
|
+
const token = issueConfirmation("restore_elementor_backup", {
|
|
1389
|
+
page_id: input.page_id,
|
|
1390
|
+
backup_meta_key: input.backup_meta_key
|
|
1391
|
+
}, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
1392
|
+
return {
|
|
1393
|
+
mode: "dry_run",
|
|
1394
|
+
page_id: input.page_id,
|
|
1395
|
+
backup_meta_key: input.backup_meta_key,
|
|
1396
|
+
settings_meta_key: input.settings_meta_key,
|
|
1397
|
+
confirmation_token: token,
|
|
1398
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
const conf = consumeConfirmation(input.confirmation, "restore_elementor_backup");
|
|
1402
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1403
|
+
const pre = await fullBackup(input.site_id, input.page_id);
|
|
1404
|
+
await restoreBackup(input.site_id, input.page_id, input.backup_meta_key, input.settings_meta_key);
|
|
1405
|
+
const flush = await flushCSS(input.site_id, input.page_id);
|
|
1406
|
+
return {
|
|
1407
|
+
mode: "restored",
|
|
1408
|
+
page_id: input.page_id,
|
|
1409
|
+
backup_meta_key: input.backup_meta_key,
|
|
1410
|
+
settings_meta_key: input.settings_meta_key,
|
|
1411
|
+
css_flush: flush.method,
|
|
1412
|
+
pre_restore_backup_meta_key: pre.meta_key
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
var duplicateElementorPageTool = defineTool({
|
|
1417
|
+
name: "duplicate_elementor_page",
|
|
1418
|
+
description: "Duplicate an Elementor page within the same site. Creates a new draft page, copies _elementor_data + _elementor_page_settings + _elementor_edit_mode, flushes CSS.",
|
|
1419
|
+
inputSchema: z3.object({
|
|
1420
|
+
site_id: z3.string().optional(),
|
|
1421
|
+
source_page_id: z3.number().int().positive(),
|
|
1422
|
+
new_title: z3.string().optional().describe("Defaults to '<original> (Copy)'"),
|
|
1423
|
+
status: z3.enum(["draft", "publish", "private"]).default("draft")
|
|
1424
|
+
}),
|
|
1425
|
+
outputSchema: z3.object({
|
|
1426
|
+
new_page_id: z3.number(),
|
|
1427
|
+
new_page_url: z3.string(),
|
|
1428
|
+
source_page_id: z3.number(),
|
|
1429
|
+
title: z3.string(),
|
|
1430
|
+
status: z3.string(),
|
|
1431
|
+
css_flush: z3.string().optional()
|
|
1432
|
+
}),
|
|
1433
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1434
|
+
async handler(input) {
|
|
1435
|
+
const source = await wpRequest(
|
|
1436
|
+
`/wp/v2/pages/${input.source_page_id}?context=edit`,
|
|
1437
|
+
{ siteId: input.site_id }
|
|
1438
|
+
);
|
|
1439
|
+
const title = input.new_title ?? `${source.title.rendered} (Copy)`;
|
|
1440
|
+
const data_raw = source.meta?._elementor_data ?? "[]";
|
|
1441
|
+
const settings_raw = source.meta?._elementor_page_settings ?? "{}";
|
|
1442
|
+
const created = await wpRequest("/wp/v2/pages", {
|
|
1443
|
+
siteId: input.site_id,
|
|
1444
|
+
method: "POST",
|
|
1445
|
+
body: {
|
|
1446
|
+
title,
|
|
1447
|
+
status: input.status,
|
|
1448
|
+
meta: {
|
|
1449
|
+
_elementor_data: data_raw,
|
|
1450
|
+
_elementor_page_settings: parseSettingsForRest(settings_raw),
|
|
1451
|
+
_elementor_edit_mode: "builder"
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
const flush = await flushCSS(input.site_id, created.id);
|
|
1456
|
+
return {
|
|
1457
|
+
new_page_id: created.id,
|
|
1458
|
+
new_page_url: created.link,
|
|
1459
|
+
source_page_id: input.source_page_id,
|
|
1460
|
+
title,
|
|
1461
|
+
status: input.status,
|
|
634
1462
|
css_flush: flush.method
|
|
635
1463
|
};
|
|
636
1464
|
}
|
|
637
1465
|
});
|
|
1466
|
+
function parseSettingsForRest(raw) {
|
|
1467
|
+
if (typeof raw === "object" && raw !== null) return raw;
|
|
1468
|
+
if (typeof raw === "string") {
|
|
1469
|
+
if (!raw.trim()) return {};
|
|
1470
|
+
try {
|
|
1471
|
+
return JSON.parse(raw);
|
|
1472
|
+
} catch {
|
|
1473
|
+
return {};
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return {};
|
|
1477
|
+
}
|
|
638
1478
|
|
|
639
1479
|
// src/tools/templates.ts
|
|
640
1480
|
import { z as z4 } from "zod";
|
|
1481
|
+
init_wp_rest();
|
|
1482
|
+
var TEMPLATE_TYPES = ["section", "page", "popup", "header", "footer", "archive", "single", "widget", "any"];
|
|
641
1483
|
var listTemplatesTool = defineTool({
|
|
642
1484
|
name: "list_elementor_templates",
|
|
643
|
-
description: "List Elementor library
|
|
1485
|
+
description: "List Elementor library entries on a site: saved sections, pages, popups, headers/footers (Theme Builder Pro), single/archive templates (Theme Builder Pro), and global widgets. Type filter narrows results.",
|
|
644
1486
|
inputSchema: z4.object({
|
|
645
1487
|
site_id: z4.string().optional(),
|
|
646
|
-
type: z4.enum(
|
|
1488
|
+
type: z4.enum(TEMPLATE_TYPES).default("any"),
|
|
647
1489
|
per_page: z4.number().int().min(1).max(100).default(50)
|
|
648
1490
|
}),
|
|
649
1491
|
outputSchema: z4.object({
|
|
650
1492
|
total: z4.number(),
|
|
651
|
-
templates: z4.array(
|
|
652
|
-
z4.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
)
|
|
1493
|
+
templates: z4.array(z4.object({
|
|
1494
|
+
id: z4.number(),
|
|
1495
|
+
title: z4.string(),
|
|
1496
|
+
type: z4.string(),
|
|
1497
|
+
is_theme_builder: z4.boolean(),
|
|
1498
|
+
display_conditions_count: z4.number(),
|
|
1499
|
+
modified: z4.string()
|
|
1500
|
+
}))
|
|
659
1501
|
}),
|
|
660
1502
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
661
1503
|
async handler(input) {
|
|
@@ -664,28 +1506,35 @@ var listTemplatesTool = defineTool({
|
|
|
664
1506
|
context: "edit",
|
|
665
1507
|
_fields: "id,title,modified,meta"
|
|
666
1508
|
};
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
const items = await wpRequest("/wp/v2/elementor_library", {
|
|
672
|
-
siteId: input.site_id,
|
|
673
|
-
query
|
|
674
|
-
});
|
|
1509
|
+
const allItems = await wpRequest("/wp/v2/elementor_library", { siteId: input.site_id, query });
|
|
1510
|
+
const items = input.type === "any" ? allItems : allItems.filter((t) => t.meta?._elementor_template_type === input.type);
|
|
1511
|
+
const themeBuilderTypes = /* @__PURE__ */ new Set(["header", "footer", "archive", "single"]);
|
|
675
1512
|
return {
|
|
676
1513
|
total: items.length,
|
|
677
|
-
templates: items.map((t) =>
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1514
|
+
templates: items.map((t) => {
|
|
1515
|
+
const type = t.meta?._elementor_template_type ?? "unknown";
|
|
1516
|
+
let conds = 0;
|
|
1517
|
+
try {
|
|
1518
|
+
const c = t.meta?._elementor_conditions;
|
|
1519
|
+
if (typeof c === "string") conds = JSON.parse(c).length;
|
|
1520
|
+
else if (Array.isArray(c)) conds = c.length;
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
return {
|
|
1524
|
+
id: t.id,
|
|
1525
|
+
title: t.title.rendered,
|
|
1526
|
+
type,
|
|
1527
|
+
is_theme_builder: themeBuilderTypes.has(type),
|
|
1528
|
+
display_conditions_count: conds,
|
|
1529
|
+
modified: t.modified
|
|
1530
|
+
};
|
|
1531
|
+
})
|
|
683
1532
|
};
|
|
684
1533
|
}
|
|
685
1534
|
});
|
|
686
1535
|
var exportTemplateTool = defineTool({
|
|
687
1536
|
name: "export_elementor_template",
|
|
688
|
-
description: "Export an Elementor template as a portable JSON object. Output
|
|
1537
|
+
description: "Export an Elementor template (section, page, header, footer, etc.) as a portable JSON object. Output goes into import_elementor_template on another site.",
|
|
689
1538
|
inputSchema: z4.object({
|
|
690
1539
|
site_id: z4.string().optional(),
|
|
691
1540
|
template_id: z4.number().int().positive()
|
|
@@ -694,18 +1543,12 @@ var exportTemplateTool = defineTool({
|
|
|
694
1543
|
template_id: z4.number(),
|
|
695
1544
|
title: z4.string(),
|
|
696
1545
|
type: z4.string(),
|
|
697
|
-
summary: z4.object({
|
|
698
|
-
|
|
699
|
-
widgets: z4.number(),
|
|
700
|
-
sections: z4.number()
|
|
701
|
-
}),
|
|
702
|
-
portable_json: z4.string().describe("JSON-stringified payload ready to import via import_elementor_template.")
|
|
1546
|
+
summary: z4.object({ totalElements: z4.number(), widgets: z4.number(), sections: z4.number() }),
|
|
1547
|
+
portable_json: z4.string()
|
|
703
1548
|
}),
|
|
704
1549
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
705
1550
|
async handler(input) {
|
|
706
|
-
const tpl = await wpRequest(`/wp/v2/elementor_library/${input.template_id}?context=edit`, {
|
|
707
|
-
siteId: input.site_id
|
|
708
|
-
});
|
|
1551
|
+
const tpl = await wpRequest(`/wp/v2/elementor_library/${input.template_id}?context=edit`, { siteId: input.site_id });
|
|
709
1552
|
const data = parseElementorData(tpl.meta?._elementor_data ?? "[]");
|
|
710
1553
|
const sum = summarize(data);
|
|
711
1554
|
const portable = {
|
|
@@ -725,10 +1568,10 @@ var exportTemplateTool = defineTool({
|
|
|
725
1568
|
});
|
|
726
1569
|
var importTemplateTool = defineTool({
|
|
727
1570
|
name: "import_elementor_template",
|
|
728
|
-
description: "Import
|
|
1571
|
+
description: "Import a portable template JSON (output of export_elementor_template) into a target site as a new library entry. Useful for cross-site template sync.",
|
|
729
1572
|
inputSchema: z4.object({
|
|
730
|
-
site_id: z4.string().optional()
|
|
731
|
-
portable_json: z4.string()
|
|
1573
|
+
site_id: z4.string().optional(),
|
|
1574
|
+
portable_json: z4.string(),
|
|
732
1575
|
override_title: z4.string().optional()
|
|
733
1576
|
}),
|
|
734
1577
|
outputSchema: z4.object({
|
|
@@ -760,102 +1603,1272 @@ var importTemplateTool = defineTool({
|
|
|
760
1603
|
}
|
|
761
1604
|
}
|
|
762
1605
|
});
|
|
1606
|
+
return { new_template_id: res.id, title, type: payload.type, url: res.link };
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
var applyTemplateToPageTool = defineTool({
|
|
1610
|
+
name: "apply_template_to_page",
|
|
1611
|
+
description: "Copy the _elementor_data + _elementor_page_settings of a SOURCE template (or page) onto a TARGET page on the same site. Backs up the target first. Use to apply a section/page template to an existing draft.",
|
|
1612
|
+
inputSchema: z4.object({
|
|
1613
|
+
site_id: z4.string().optional(),
|
|
1614
|
+
source_id: z4.number().int().positive().describe("Source post id: a template or a page."),
|
|
1615
|
+
target_page_id: z4.number().int().positive(),
|
|
1616
|
+
backup_to_file: z4.boolean().default(false),
|
|
1617
|
+
confirmation: z4.string().optional()
|
|
1618
|
+
}),
|
|
1619
|
+
outputSchema: z4.object({
|
|
1620
|
+
mode: z4.enum(["dry_run", "applied"]),
|
|
1621
|
+
target_page_id: z4.number(),
|
|
1622
|
+
source_id: z4.number(),
|
|
1623
|
+
confirmation_token: z4.string().optional(),
|
|
1624
|
+
expires_in_seconds: z4.number().optional(),
|
|
1625
|
+
backup_meta_key: z4.string().optional(),
|
|
1626
|
+
backup_file: z4.string().optional(),
|
|
1627
|
+
css_flush: z4.string().optional()
|
|
1628
|
+
}),
|
|
1629
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1630
|
+
async handler(input) {
|
|
1631
|
+
const { issueConfirmation: issueConfirmation2, consumeConfirmation: consumeConfirmation2 } = await Promise.resolve().then(() => (init_confirmation(), confirmation_exports));
|
|
1632
|
+
const { POLICIES: POLICIES2 } = await Promise.resolve().then(() => (init_policies(), policies_exports));
|
|
1633
|
+
const { fullBackup: fullBackup2 } = await Promise.resolve().then(() => (init_backup(), backup_exports));
|
|
1634
|
+
const { flushCSS: flushCSS2 } = await Promise.resolve().then(() => (init_css_flush(), css_flush_exports));
|
|
1635
|
+
if (!input.confirmation) {
|
|
1636
|
+
const token = issueConfirmation2("apply_template_to_page", { src: input.source_id, tgt: input.target_page_id, site: input.site_id }, POLICIES2.CONFIRMATION_TTL_SECONDS);
|
|
1637
|
+
return {
|
|
1638
|
+
mode: "dry_run",
|
|
1639
|
+
target_page_id: input.target_page_id,
|
|
1640
|
+
source_id: input.source_id,
|
|
1641
|
+
confirmation_token: token,
|
|
1642
|
+
expires_in_seconds: POLICIES2.CONFIRMATION_TTL_SECONDS
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
const conf = consumeConfirmation2(input.confirmation, "apply_template_to_page");
|
|
1646
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1647
|
+
let src_meta;
|
|
1648
|
+
try {
|
|
1649
|
+
const src = await wpRequest(`/wp/v2/elementor_library/${input.source_id}?context=edit`, { siteId: input.site_id });
|
|
1650
|
+
src_meta = src.meta;
|
|
1651
|
+
} catch {
|
|
1652
|
+
const src = await wpRequest(`/wp/v2/pages/${input.source_id}?context=edit`, { siteId: input.site_id });
|
|
1653
|
+
src_meta = src.meta;
|
|
1654
|
+
}
|
|
1655
|
+
const data_raw = src_meta?._elementor_data ?? "[]";
|
|
1656
|
+
const settings_raw = src_meta?._elementor_page_settings ?? "{}";
|
|
1657
|
+
const backup = await fullBackup2(input.site_id, input.target_page_id, { to_file: input.backup_to_file });
|
|
1658
|
+
await wpRequest(`/wp/v2/pages/${input.target_page_id}`, {
|
|
1659
|
+
siteId: input.site_id,
|
|
1660
|
+
method: "PUT",
|
|
1661
|
+
body: {
|
|
1662
|
+
meta: {
|
|
1663
|
+
_elementor_data: data_raw,
|
|
1664
|
+
_elementor_page_settings: parseSettingsForRest2(settings_raw),
|
|
1665
|
+
_elementor_edit_mode: "builder"
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
const flush = await flushCSS2(input.site_id, input.target_page_id);
|
|
763
1670
|
return {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1671
|
+
mode: "applied",
|
|
1672
|
+
target_page_id: input.target_page_id,
|
|
1673
|
+
source_id: input.source_id,
|
|
1674
|
+
backup_meta_key: backup.meta_key,
|
|
1675
|
+
backup_file: backup.file_path,
|
|
1676
|
+
css_flush: flush.method
|
|
768
1677
|
};
|
|
769
1678
|
}
|
|
770
1679
|
});
|
|
1680
|
+
function parseSettingsForRest2(raw) {
|
|
1681
|
+
if (typeof raw === "object" && raw !== null) return raw;
|
|
1682
|
+
if (typeof raw === "string") {
|
|
1683
|
+
if (!raw.trim()) return {};
|
|
1684
|
+
try {
|
|
1685
|
+
return JSON.parse(raw);
|
|
1686
|
+
} catch {
|
|
1687
|
+
return {};
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return {};
|
|
1691
|
+
}
|
|
771
1692
|
|
|
772
|
-
// src/tools/
|
|
1693
|
+
// src/tools/wpcli.ts
|
|
773
1694
|
import { z as z5 } from "zod";
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
var
|
|
779
|
-
name: "
|
|
780
|
-
description: "
|
|
1695
|
+
init_config();
|
|
1696
|
+
init_ssh_wpcli();
|
|
1697
|
+
init_policies();
|
|
1698
|
+
init_confirmation();
|
|
1699
|
+
var wpCliRunTool = defineTool({
|
|
1700
|
+
name: "wp_cli_run",
|
|
1701
|
+
description: "Execute an arbitrary wp-cli command on a site via SSH. The `wp` prefix and `--path` are added automatically \u2014 pass only the args (e.g. 'post list --post_type=page'). Destructive commands (delete, drop, search-replace without --dry-run, plugin deactivate/uninstall) require a two-call confirmation flow.",
|
|
781
1702
|
inputSchema: z5.object({
|
|
782
|
-
|
|
1703
|
+
site_id: z5.string().optional(),
|
|
1704
|
+
args: z5.string().min(1).describe("WP-CLI args without leading 'wp', e.g. 'post list --post_type=page'"),
|
|
1705
|
+
timeout_ms: z5.number().int().min(1e3).max(3e5).default(6e4),
|
|
1706
|
+
confirmation: z5.string().optional()
|
|
783
1707
|
}),
|
|
784
1708
|
outputSchema: z5.object({
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
error: z5.string().optional()
|
|
795
|
-
})
|
|
796
|
-
)
|
|
1709
|
+
mode: z5.enum(["dry_run_destructive", "executed"]),
|
|
1710
|
+
command: z5.string(),
|
|
1711
|
+
stdout: z5.string().optional(),
|
|
1712
|
+
stderr: z5.string().optional(),
|
|
1713
|
+
exit_code: z5.number().optional(),
|
|
1714
|
+
duration_ms: z5.number().optional(),
|
|
1715
|
+
destructive_pattern_detected: z5.boolean().optional(),
|
|
1716
|
+
confirmation_token: z5.string().optional(),
|
|
1717
|
+
expires_in_seconds: z5.number().optional()
|
|
797
1718
|
}),
|
|
798
|
-
annotations: {
|
|
1719
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
799
1720
|
async handler(input) {
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
} catch (e) {
|
|
822
|
-
rows.push({
|
|
823
|
-
site_id: site.id,
|
|
824
|
-
url: site.url,
|
|
825
|
-
outdated_free: false,
|
|
826
|
-
error: e.message
|
|
827
|
-
});
|
|
1721
|
+
const forbidden = isForbiddenWpCli(input.args);
|
|
1722
|
+
if (forbidden.forbidden) {
|
|
1723
|
+
throw new Error(`Forbidden wp-cli command. ${forbidden.reason}`);
|
|
1724
|
+
}
|
|
1725
|
+
const isDestructive = isDestructiveWpCli(input.args);
|
|
1726
|
+
if (isDestructive && !input.confirmation) {
|
|
1727
|
+
const token = issueConfirmation("wp_cli_run", { site_id: input.site_id, args: input.args }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
1728
|
+
return {
|
|
1729
|
+
mode: "dry_run_destructive",
|
|
1730
|
+
command: `wp <path> ${input.args}`,
|
|
1731
|
+
destructive_pattern_detected: true,
|
|
1732
|
+
confirmation_token: token,
|
|
1733
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
if (isDestructive && input.confirmation) {
|
|
1737
|
+
const conf = consumeConfirmation(input.confirmation, "wp_cli_run");
|
|
1738
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1739
|
+
const o = conf.payload;
|
|
1740
|
+
if (o.args !== input.args || o.site_id !== input.site_id) {
|
|
1741
|
+
throw new Error("Confirmation parameters don't match the original dry-run");
|
|
828
1742
|
}
|
|
829
1743
|
}
|
|
1744
|
+
const site = getSite(input.site_id);
|
|
1745
|
+
const r = await sshWpCli(site, input.args, { timeout_ms: input.timeout_ms });
|
|
830
1746
|
return {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1747
|
+
mode: "executed",
|
|
1748
|
+
command: r.command,
|
|
1749
|
+
stdout: r.stdout,
|
|
1750
|
+
stderr: r.stderr,
|
|
1751
|
+
exit_code: r.exitCode,
|
|
1752
|
+
duration_ms: r.duration_ms,
|
|
1753
|
+
destructive_pattern_detected: isDestructive
|
|
834
1754
|
};
|
|
835
1755
|
}
|
|
836
1756
|
});
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1757
|
+
var wpSearchReplaceTool = defineTool({
|
|
1758
|
+
name: "wp_search_replace",
|
|
1759
|
+
description: "Run `wp search-replace` against wp_postmeta (default) \u2014 the standard agency way to update Elementor text content. ALWAYS dry-run first; the apply call requires a confirmation token. Includes --precise --all-tables-with-prefix by default if you specify table='all'.",
|
|
1760
|
+
inputSchema: z5.object({
|
|
1761
|
+
site_id: z5.string().optional(),
|
|
1762
|
+
find: z5.string().min(1),
|
|
1763
|
+
replace: z5.string(),
|
|
1764
|
+
table: z5.string().default("wp_postmeta"),
|
|
1765
|
+
include_columns: z5.string().optional().describe("e.g. 'meta_value'. Default: meta_value when table=wp_postmeta."),
|
|
1766
|
+
precise: z5.boolean().default(true),
|
|
1767
|
+
confirmation: z5.string().optional()
|
|
1768
|
+
}),
|
|
1769
|
+
outputSchema: z5.object({
|
|
1770
|
+
mode: z5.enum(["dry_run", "applied"]),
|
|
1771
|
+
command: z5.string(),
|
|
1772
|
+
stdout: z5.string().optional(),
|
|
1773
|
+
stderr: z5.string().optional(),
|
|
1774
|
+
exit_code: z5.number().optional(),
|
|
1775
|
+
replacement_count: z5.number().optional(),
|
|
1776
|
+
confirmation_token: z5.string().optional(),
|
|
1777
|
+
expires_in_seconds: z5.number().optional()
|
|
1778
|
+
}),
|
|
1779
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1780
|
+
async handler(input) {
|
|
1781
|
+
const includeCols = input.include_columns ?? (input.table === "wp_postmeta" ? "meta_value" : "");
|
|
1782
|
+
const baseArgs = [
|
|
1783
|
+
"search-replace",
|
|
1784
|
+
`'${input.find.replace(/'/g, "'\\''")}'`,
|
|
1785
|
+
`'${input.replace.replace(/'/g, "'\\''")}'`,
|
|
1786
|
+
input.table,
|
|
1787
|
+
includeCols ? `--include-columns=${includeCols}` : "",
|
|
1788
|
+
input.precise ? "--precise" : ""
|
|
1789
|
+
].filter(Boolean).join(" ");
|
|
1790
|
+
const site = getSite(input.site_id);
|
|
1791
|
+
if (!input.confirmation) {
|
|
1792
|
+
const dry = await sshWpCli(site, baseArgs + " --dry-run");
|
|
1793
|
+
const m2 = dry.stdout.match(/Success: (\d+) replacement/);
|
|
1794
|
+
const count = m2 ? parseInt(m2[1], 10) : 0;
|
|
1795
|
+
if (count === 0) {
|
|
1796
|
+
return { mode: "dry_run", command: dry.command, stdout: dry.stdout, exit_code: dry.exitCode, replacement_count: 0 };
|
|
1797
|
+
}
|
|
1798
|
+
const token = issueConfirmation("wp_search_replace", { args: baseArgs, site_id: input.site_id }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
1799
|
+
return {
|
|
1800
|
+
mode: "dry_run",
|
|
1801
|
+
command: dry.command,
|
|
1802
|
+
stdout: dry.stdout,
|
|
1803
|
+
replacement_count: count,
|
|
1804
|
+
confirmation_token: token,
|
|
1805
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
const conf = consumeConfirmation(input.confirmation, "wp_search_replace");
|
|
1809
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1810
|
+
const o = conf.payload;
|
|
1811
|
+
if (o.args !== baseArgs || o.site_id !== input.site_id) throw new Error("Confirmation params mismatch");
|
|
1812
|
+
const r = await sshWpCli(site, baseArgs);
|
|
1813
|
+
const m = r.stdout.match(/Success: (\d+) replacement/);
|
|
1814
|
+
return {
|
|
1815
|
+
mode: "applied",
|
|
1816
|
+
command: r.command,
|
|
1817
|
+
stdout: r.stdout,
|
|
1818
|
+
stderr: r.stderr,
|
|
1819
|
+
exit_code: r.exitCode,
|
|
1820
|
+
replacement_count: m ? parseInt(m[1], 10) : 0
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
var wpElementorFlushCssTool = defineTool({
|
|
1825
|
+
name: "wp_elementor_flush_css",
|
|
1826
|
+
description: "Flush Elementor's CSS cache on a site using the 3-level fallback strategy (REST endpoint \u2192 wp-cli native \u2192 option/meta delete). Always call after writing _elementor_data programmatically.",
|
|
1827
|
+
inputSchema: z5.object({
|
|
1828
|
+
site_id: z5.string().optional(),
|
|
1829
|
+
page_id: z5.number().int().positive().optional().describe("Optional: flush only this page's cache. If omitted, flushes site-wide.")
|
|
1830
|
+
}),
|
|
1831
|
+
outputSchema: z5.object({
|
|
1832
|
+
method: z5.enum(["rest", "wp-cli", "option-delete", "resave", "none"]),
|
|
1833
|
+
details: z5.string().optional()
|
|
1834
|
+
}),
|
|
1835
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1836
|
+
async handler(input) {
|
|
1837
|
+
const { flushCSS: flushCSS2 } = await Promise.resolve().then(() => (init_css_flush(), css_flush_exports));
|
|
1838
|
+
return flushCSS2(input.site_id, input.page_id);
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
var wpPluginListTool = defineTool({
|
|
1842
|
+
name: "wp_plugin_list",
|
|
1843
|
+
description: "List installed plugins on a site with name, version, status (active/inactive), and update_version (if outdated). Uses WP-CLI for accurate version data including update_version.",
|
|
1844
|
+
inputSchema: z5.object({
|
|
1845
|
+
site_id: z5.string().optional(),
|
|
1846
|
+
only_outdated: z5.boolean().default(false)
|
|
1847
|
+
}),
|
|
1848
|
+
outputSchema: z5.object({
|
|
1849
|
+
total: z5.number(),
|
|
1850
|
+
plugins: z5.array(z5.object({
|
|
1851
|
+
name: z5.string(),
|
|
1852
|
+
status: z5.string(),
|
|
1853
|
+
version: z5.string(),
|
|
1854
|
+
update: z5.string().optional(),
|
|
1855
|
+
update_version: z5.string().optional()
|
|
1856
|
+
}))
|
|
1857
|
+
}),
|
|
1858
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1859
|
+
async handler(input) {
|
|
1860
|
+
const site = getSite(input.site_id);
|
|
1861
|
+
const r = await sshWpCli(site, "plugin list --format=json");
|
|
1862
|
+
if (r.exitCode !== 0) throw new Error(`wp plugin list failed: ${r.stderr}`);
|
|
1863
|
+
const arr = JSON.parse(r.stdout);
|
|
1864
|
+
const filtered = input.only_outdated ? arr.filter((p) => p.update === "available") : arr;
|
|
1865
|
+
return { total: filtered.length, plugins: filtered };
|
|
1866
|
+
}
|
|
1867
|
+
});
|
|
1868
|
+
var wpPluginUpdateTool = defineTool({
|
|
1869
|
+
name: "wp_plugin_update",
|
|
1870
|
+
description: "Update one or more plugins on a site to their latest version. Requires confirmation token (uses wp-cli).",
|
|
1871
|
+
inputSchema: z5.object({
|
|
1872
|
+
site_id: z5.string().optional(),
|
|
1873
|
+
plugins: z5.array(z5.string()).min(1).describe("Plugin slugs to update, e.g. ['elementor', 'elementor-pro']. Use 'all' for everything outdated."),
|
|
1874
|
+
confirmation: z5.string().optional()
|
|
1875
|
+
}),
|
|
1876
|
+
outputSchema: z5.object({
|
|
1877
|
+
mode: z5.enum(["dry_run", "applied"]),
|
|
1878
|
+
plugins: z5.array(z5.string()),
|
|
1879
|
+
command: z5.string().optional(),
|
|
1880
|
+
stdout: z5.string().optional(),
|
|
1881
|
+
stderr: z5.string().optional(),
|
|
1882
|
+
exit_code: z5.number().optional(),
|
|
1883
|
+
confirmation_token: z5.string().optional(),
|
|
1884
|
+
expires_in_seconds: z5.number().optional()
|
|
1885
|
+
}),
|
|
1886
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1887
|
+
async handler(input) {
|
|
1888
|
+
if (!input.confirmation) {
|
|
1889
|
+
const token = issueConfirmation("wp_plugin_update", { site_id: input.site_id, plugins: input.plugins }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
1890
|
+
return {
|
|
1891
|
+
mode: "dry_run",
|
|
1892
|
+
plugins: input.plugins,
|
|
1893
|
+
confirmation_token: token,
|
|
1894
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
const conf = consumeConfirmation(input.confirmation, "wp_plugin_update");
|
|
1898
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1899
|
+
const site = getSite(input.site_id);
|
|
1900
|
+
const target = input.plugins.includes("all") ? "--all" : input.plugins.map((p) => `'${p}'`).join(" ");
|
|
1901
|
+
const r = await sshWpCli(site, `plugin update ${target}`, { timeout_ms: 18e4 });
|
|
1902
|
+
return {
|
|
1903
|
+
mode: "applied",
|
|
1904
|
+
plugins: input.plugins,
|
|
1905
|
+
command: r.command,
|
|
1906
|
+
stdout: r.stdout,
|
|
1907
|
+
stderr: r.stderr,
|
|
1908
|
+
exit_code: r.exitCode
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// src/tools/visual.ts
|
|
1914
|
+
import { z as z6 } from "zod";
|
|
1915
|
+
|
|
1916
|
+
// src/transport/screenshot.ts
|
|
1917
|
+
init_logger();
|
|
1918
|
+
import { spawn as spawn2 } from "child_process";
|
|
1919
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1920
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1921
|
+
import { join as join2 } from "path";
|
|
1922
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1923
|
+
var CHROME_PATHS = [
|
|
1924
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1925
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1926
|
+
"/usr/bin/google-chrome",
|
|
1927
|
+
"/usr/bin/chromium",
|
|
1928
|
+
"/usr/bin/chromium-browser"
|
|
1929
|
+
];
|
|
1930
|
+
function findChrome() {
|
|
1931
|
+
if (process.env.ELEMENTOR_MCP_CHROME) {
|
|
1932
|
+
return existsSync2(process.env.ELEMENTOR_MCP_CHROME) ? process.env.ELEMENTOR_MCP_CHROME : null;
|
|
1933
|
+
}
|
|
1934
|
+
for (const p of CHROME_PATHS) {
|
|
1935
|
+
if (existsSync2(p)) return p;
|
|
1936
|
+
}
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
async function screenshotUrl(url, opts = {}) {
|
|
1940
|
+
const chrome = findChrome();
|
|
1941
|
+
if (!chrome) {
|
|
1942
|
+
throw new Error(
|
|
1943
|
+
"Could not locate a Chrome/Chromium binary for screenshots. Set ELEMENTOR_MCP_CHROME to its path or install Chrome at one of: " + CHROME_PATHS.join(", ")
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
const filename = `elementor-mcp-${randomBytes2(4).toString("hex")}-${Date.now()}.png`;
|
|
1947
|
+
const path = join2(tmpdir2(), filename);
|
|
1948
|
+
const args = [
|
|
1949
|
+
"--headless=new",
|
|
1950
|
+
"--disable-gpu",
|
|
1951
|
+
"--no-sandbox",
|
|
1952
|
+
"--hide-scrollbars",
|
|
1953
|
+
`--window-size=${opts.width ?? 1440},${opts.height ?? 900}`,
|
|
1954
|
+
`--screenshot=${path}`,
|
|
1955
|
+
...opts.full_page ? ["--virtual-time-budget=10000"] : [],
|
|
1956
|
+
"--default-background-color=00000000",
|
|
1957
|
+
url
|
|
1958
|
+
];
|
|
1959
|
+
const timeout = opts.timeout_ms ?? 6e4;
|
|
1960
|
+
logger.debug({ url, chrome }, "screenshot");
|
|
1961
|
+
await new Promise((resolve3, reject) => {
|
|
1962
|
+
const child = spawn2(chrome, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1963
|
+
let stderr = "";
|
|
1964
|
+
const killer = setTimeout(() => {
|
|
1965
|
+
child.kill("SIGKILL");
|
|
1966
|
+
reject(new Error(`Screenshot timed out after ${timeout}ms`));
|
|
1967
|
+
}, timeout);
|
|
1968
|
+
child.stderr.on("data", (b) => {
|
|
1969
|
+
stderr += b.toString();
|
|
1970
|
+
});
|
|
1971
|
+
child.on("close", (code) => {
|
|
1972
|
+
clearTimeout(killer);
|
|
1973
|
+
if (code !== 0) reject(new Error(`Chrome exited ${code}: ${stderr.slice(0, 300)}`));
|
|
1974
|
+
else resolve3();
|
|
1975
|
+
});
|
|
1976
|
+
child.on("error", (e) => {
|
|
1977
|
+
clearTimeout(killer);
|
|
1978
|
+
reject(e);
|
|
1979
|
+
});
|
|
1980
|
+
});
|
|
1981
|
+
if (!existsSync2(path)) throw new Error(`Screenshot file not created: ${path}`);
|
|
1982
|
+
const fs = await import("fs");
|
|
1983
|
+
const stat = fs.statSync(path);
|
|
1984
|
+
return { path, bytes: stat.size };
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/tools/visual.ts
|
|
1988
|
+
init_wp_rest();
|
|
1989
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1990
|
+
import { createHash } from "crypto";
|
|
1991
|
+
var screenshotPageTool = defineTool({
|
|
1992
|
+
name: "screenshot_page",
|
|
1993
|
+
description: "Capture a PNG screenshot of a page's frontend (visitor-facing URL). Requires a Chrome/Chromium binary on the host. Returns the local file path so the LLM can analyse it or compare against another shot.",
|
|
1994
|
+
inputSchema: z6.object({
|
|
1995
|
+
site_id: z6.string().optional(),
|
|
1996
|
+
page_id: z6.number().int().positive().optional(),
|
|
1997
|
+
url: z6.string().url().optional().describe("Alternative to page_id: hit any URL directly."),
|
|
1998
|
+
width: z6.number().int().min(320).max(3840).default(1440),
|
|
1999
|
+
height: z6.number().int().min(240).max(2400).default(900),
|
|
2000
|
+
full_page: z6.boolean().default(false)
|
|
2001
|
+
}),
|
|
2002
|
+
outputSchema: z6.object({
|
|
2003
|
+
url: z6.string(),
|
|
2004
|
+
file_path: z6.string(),
|
|
2005
|
+
bytes: z6.number(),
|
|
2006
|
+
sha256: z6.string(),
|
|
2007
|
+
width: z6.number(),
|
|
2008
|
+
height: z6.number()
|
|
2009
|
+
}),
|
|
2010
|
+
annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: true },
|
|
2011
|
+
async handler(input) {
|
|
2012
|
+
let target = input.url;
|
|
2013
|
+
if (!target) {
|
|
2014
|
+
if (!input.page_id) throw new Error("Provide either 'url' or 'page_id'.");
|
|
2015
|
+
const page = await wpRequest(`/wp/v2/pages/${input.page_id}?_fields=link`, { siteId: input.site_id });
|
|
2016
|
+
target = page.link;
|
|
2017
|
+
}
|
|
2018
|
+
const shot = await screenshotUrl(target, { width: input.width, height: input.height, full_page: input.full_page });
|
|
2019
|
+
const hash = createHash("sha256").update(readFileSync3(shot.path)).digest("hex");
|
|
2020
|
+
return {
|
|
2021
|
+
url: target,
|
|
2022
|
+
file_path: shot.path,
|
|
2023
|
+
bytes: shot.bytes,
|
|
2024
|
+
sha256: hash,
|
|
2025
|
+
width: input.width,
|
|
2026
|
+
height: input.height
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
var compareScreenshotsTool = defineTool({
|
|
2031
|
+
name: "compare_screenshots",
|
|
2032
|
+
description: "Compare two screenshot files via SHA-256 hash equality and size delta. Quick way to spot whether a page changed visually after an edit. For pixel diffs, use a dedicated tool externally.",
|
|
2033
|
+
inputSchema: z6.object({
|
|
2034
|
+
before_path: z6.string(),
|
|
2035
|
+
after_path: z6.string()
|
|
2036
|
+
}),
|
|
2037
|
+
outputSchema: z6.object({
|
|
2038
|
+
identical: z6.boolean(),
|
|
2039
|
+
before_bytes: z6.number(),
|
|
2040
|
+
after_bytes: z6.number(),
|
|
2041
|
+
delta_bytes: z6.number(),
|
|
2042
|
+
before_sha256: z6.string(),
|
|
2043
|
+
after_sha256: z6.string()
|
|
2044
|
+
}),
|
|
2045
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
2046
|
+
async handler(input) {
|
|
2047
|
+
const a = readFileSync3(input.before_path);
|
|
2048
|
+
const b = readFileSync3(input.after_path);
|
|
2049
|
+
const ah = createHash("sha256").update(a).digest("hex");
|
|
2050
|
+
const bh = createHash("sha256").update(b).digest("hex");
|
|
2051
|
+
return {
|
|
2052
|
+
identical: ah === bh,
|
|
2053
|
+
before_bytes: a.length,
|
|
2054
|
+
after_bytes: b.length,
|
|
2055
|
+
delta_bytes: b.length - a.length,
|
|
2056
|
+
before_sha256: ah,
|
|
2057
|
+
after_sha256: bh
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
// src/tools/updates.ts
|
|
2063
|
+
import { z as z7 } from "zod";
|
|
2064
|
+
init_wp_rest();
|
|
2065
|
+
init_config();
|
|
2066
|
+
async function fetchLatestElementor() {
|
|
2067
|
+
try {
|
|
2068
|
+
const free = await fetch("https://api.wordpress.org/plugins/info/1.0/elementor.json", {
|
|
2069
|
+
headers: { "User-Agent": "elementor-mcp-agent" }
|
|
2070
|
+
}).then((r) => r.json());
|
|
2071
|
+
return { free: free.version ?? "unknown" };
|
|
2072
|
+
} catch {
|
|
2073
|
+
return { free: "unknown" };
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
var checkElementorVersionsTool = defineTool({
|
|
2077
|
+
name: "check_elementor_versions",
|
|
2078
|
+
description: "Fleet-wide Elementor version audit. For every site, fetches installed Elementor/Pro versions and compares against wordpress.org latest. Flags outdated installs.",
|
|
2079
|
+
inputSchema: z7.object({
|
|
2080
|
+
site_ids: z7.array(z7.string()).optional()
|
|
2081
|
+
}),
|
|
2082
|
+
outputSchema: z7.object({
|
|
2083
|
+
checked: z7.number(),
|
|
2084
|
+
latest_elementor_free: z7.string(),
|
|
2085
|
+
sites: z7.array(z7.object({
|
|
2086
|
+
site_id: z7.string(),
|
|
2087
|
+
url: z7.string(),
|
|
2088
|
+
elementor_version: z7.string().optional(),
|
|
2089
|
+
elementor_pro_version: z7.string().optional(),
|
|
2090
|
+
outdated_free: z7.boolean(),
|
|
2091
|
+
error: z7.string().optional()
|
|
2092
|
+
}))
|
|
2093
|
+
}),
|
|
2094
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2095
|
+
async handler(input) {
|
|
2096
|
+
const cfg = loadConfig();
|
|
2097
|
+
const latest = await fetchLatestElementor();
|
|
2098
|
+
const targets = input.site_ids ? cfg.sites.filter((s) => input.site_ids?.includes(s.id)) : cfg.sites;
|
|
2099
|
+
const rows = [];
|
|
2100
|
+
for (const site of targets) {
|
|
2101
|
+
try {
|
|
2102
|
+
const plugins = await wpRequest("/wp/v2/plugins", { siteId: site.id });
|
|
2103
|
+
let elementor_version;
|
|
2104
|
+
let elementor_pro_version;
|
|
2105
|
+
for (const p of plugins) {
|
|
2106
|
+
if (p.plugin.startsWith("elementor/") && p.plugin.endsWith("/elementor.php")) elementor_version = p.version;
|
|
2107
|
+
if (p.plugin.startsWith("elementor-pro/")) elementor_pro_version = p.version;
|
|
2108
|
+
}
|
|
2109
|
+
rows.push({
|
|
2110
|
+
site_id: site.id,
|
|
2111
|
+
url: site.url,
|
|
2112
|
+
elementor_version,
|
|
2113
|
+
elementor_pro_version,
|
|
2114
|
+
outdated_free: !!elementor_version && elementor_version !== latest.free && latest.free !== "unknown"
|
|
2115
|
+
});
|
|
2116
|
+
} catch (e) {
|
|
2117
|
+
rows.push({ site_id: site.id, url: site.url, outdated_free: false, error: e.message });
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return { checked: rows.length, latest_elementor_free: latest.free, sites: rows };
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
// src/tools/widgets.ts
|
|
2125
|
+
import { z as z8 } from "zod";
|
|
2126
|
+
init_wp_rest();
|
|
2127
|
+
|
|
2128
|
+
// src/elementor/widget-ops.ts
|
|
2129
|
+
function readWidget(data, widgetId) {
|
|
2130
|
+
return findElementById(data, widgetId);
|
|
2131
|
+
}
|
|
2132
|
+
function updateWidgetSettings(data, widgetId, patch) {
|
|
2133
|
+
const widget = findElementById(data, widgetId);
|
|
2134
|
+
if (!widget || widget.elType !== "widget") return false;
|
|
2135
|
+
widget.settings = { ...widget.settings, ...patch };
|
|
2136
|
+
return true;
|
|
2137
|
+
}
|
|
2138
|
+
function deleteWidget(data, widgetId) {
|
|
2139
|
+
function removeFrom(arr) {
|
|
2140
|
+
for (let i = 0; i < arr.length; i++) {
|
|
2141
|
+
if (arr[i].id === widgetId) {
|
|
2142
|
+
arr.splice(i, 1);
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
2145
|
+
if (arr[i].elements && removeFrom(arr[i].elements)) return true;
|
|
2146
|
+
}
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
return removeFrom(data);
|
|
2150
|
+
}
|
|
2151
|
+
function findParent(data, widgetId) {
|
|
2152
|
+
for (const { element } of walkElements(data)) {
|
|
2153
|
+
if (element.elements?.some((e) => e.id === widgetId)) return element;
|
|
2154
|
+
}
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
function duplicateWidget(data, widgetId) {
|
|
2158
|
+
const parent = findParent(data, widgetId);
|
|
2159
|
+
if (!parent || !parent.elements) return { ok: false };
|
|
2160
|
+
const idx = parent.elements.findIndex((e) => e.id === widgetId);
|
|
2161
|
+
if (idx < 0) return { ok: false };
|
|
2162
|
+
const clone = JSON.parse(JSON.stringify(parent.elements[idx]));
|
|
2163
|
+
clone.id = generateId();
|
|
2164
|
+
parent.elements.splice(idx + 1, 0, clone);
|
|
2165
|
+
return { ok: true, new_widget_id: clone.id };
|
|
2166
|
+
}
|
|
2167
|
+
function swapWidgetType(data, widgetId, newType, newSettings = {}) {
|
|
2168
|
+
const widget = findElementById(data, widgetId);
|
|
2169
|
+
if (!widget || widget.elType !== "widget") return false;
|
|
2170
|
+
widget.widgetType = newType;
|
|
2171
|
+
widget.settings = newSettings;
|
|
2172
|
+
return true;
|
|
2173
|
+
}
|
|
2174
|
+
function addWidget(data, parentId, widgetType, settings = {}) {
|
|
2175
|
+
const parent = findElementById(data, parentId);
|
|
2176
|
+
if (!parent) return { ok: false };
|
|
2177
|
+
if (!parent.elements) parent.elements = [];
|
|
2178
|
+
const newWidget = {
|
|
2179
|
+
id: generateId(),
|
|
2180
|
+
elType: "widget",
|
|
2181
|
+
widgetType,
|
|
2182
|
+
settings,
|
|
2183
|
+
elements: [],
|
|
2184
|
+
isInner: false
|
|
2185
|
+
};
|
|
2186
|
+
parent.elements.push(newWidget);
|
|
2187
|
+
return { ok: true, new_widget_id: newWidget.id };
|
|
2188
|
+
}
|
|
2189
|
+
function moveWidget(data, widgetId, newParentId, position = -1) {
|
|
2190
|
+
const widget = findElementById(data, widgetId);
|
|
2191
|
+
if (!widget) return false;
|
|
2192
|
+
const oldParent = findParent(data, widgetId);
|
|
2193
|
+
if (!oldParent || !oldParent.elements) return false;
|
|
2194
|
+
const newParent = findElementById(data, newParentId);
|
|
2195
|
+
if (!newParent) return false;
|
|
2196
|
+
if (!newParent.elements) newParent.elements = [];
|
|
2197
|
+
const idx = oldParent.elements.findIndex((e) => e.id === widgetId);
|
|
2198
|
+
if (idx < 0) return false;
|
|
2199
|
+
oldParent.elements.splice(idx, 1);
|
|
2200
|
+
if (position < 0 || position >= newParent.elements.length) {
|
|
2201
|
+
newParent.elements.push(widget);
|
|
2202
|
+
} else {
|
|
2203
|
+
newParent.elements.splice(position, 0, widget);
|
|
2204
|
+
}
|
|
2205
|
+
return true;
|
|
2206
|
+
}
|
|
2207
|
+
function generateId() {
|
|
2208
|
+
return Math.random().toString(16).slice(2, 9).padEnd(7, "0");
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// src/tools/widgets.ts
|
|
2212
|
+
init_backup();
|
|
2213
|
+
init_css_flush();
|
|
2214
|
+
init_policies();
|
|
2215
|
+
init_confirmation();
|
|
2216
|
+
async function fetchData(siteId, pageId) {
|
|
2217
|
+
const page = await wpRequest(
|
|
2218
|
+
`/wp/v2/pages/${pageId}?context=edit&_fields=meta`,
|
|
2219
|
+
{ siteId }
|
|
2220
|
+
);
|
|
2221
|
+
const v = page.meta?._elementor_data;
|
|
2222
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2223
|
+
return { raw, data: parseElementorData(raw) };
|
|
2224
|
+
}
|
|
2225
|
+
async function writeData(siteId, pageId, data) {
|
|
2226
|
+
const ser = serializeElementorData(data);
|
|
2227
|
+
const validation = validateElementorData(ser);
|
|
2228
|
+
if (!validation.valid) {
|
|
2229
|
+
throw new Error("Validation failed after edit: " + validation.errors.join("; "));
|
|
2230
|
+
}
|
|
2231
|
+
await wpRequest(`/wp/v2/pages/${pageId}`, {
|
|
2232
|
+
siteId,
|
|
2233
|
+
method: "PUT",
|
|
2234
|
+
body: { meta: { _elementor_data: ser } }
|
|
2235
|
+
});
|
|
2236
|
+
const flush = await flushCSS(siteId, pageId);
|
|
2237
|
+
return { method: flush.method };
|
|
2238
|
+
}
|
|
2239
|
+
var readWidgetTool = defineTool({
|
|
2240
|
+
name: "read_widget",
|
|
2241
|
+
description: "Fetch a single widget's full settings by id. Use list_widgets_in_page to find the id first.",
|
|
2242
|
+
inputSchema: z8.object({
|
|
2243
|
+
site_id: z8.string().optional(),
|
|
2244
|
+
page_id: z8.number().int().positive(),
|
|
2245
|
+
widget_id: z8.string().min(1)
|
|
2246
|
+
}),
|
|
2247
|
+
outputSchema: z8.object({
|
|
2248
|
+
widget_id: z8.string(),
|
|
2249
|
+
widget_type: z8.string().optional(),
|
|
2250
|
+
settings: z8.record(z8.any())
|
|
2251
|
+
}),
|
|
2252
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2253
|
+
async handler(input) {
|
|
2254
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2255
|
+
const w = readWidget(data, input.widget_id);
|
|
2256
|
+
if (!w) throw new Error(`Widget ${input.widget_id} not found on page ${input.page_id}`);
|
|
2257
|
+
return { widget_id: w.id, widget_type: w.widgetType, settings: w.settings };
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
var updateWidgetSettingsTool = defineTool({
|
|
2261
|
+
name: "update_widget_settings",
|
|
2262
|
+
description: "Shallow-merge a partial settings object into one widget. Backs up the page first; validates the result; auto-flushes CSS. Two-call confirmation flow.",
|
|
2263
|
+
inputSchema: z8.object({
|
|
2264
|
+
site_id: z8.string().optional(),
|
|
2265
|
+
page_id: z8.number().int().positive(),
|
|
2266
|
+
widget_id: z8.string().min(1),
|
|
2267
|
+
settings_patch: z8.record(z8.any()),
|
|
2268
|
+
confirmation: z8.string().optional()
|
|
2269
|
+
}),
|
|
2270
|
+
outputSchema: z8.object({
|
|
2271
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2272
|
+
page_id: z8.number(),
|
|
2273
|
+
widget_id: z8.string(),
|
|
2274
|
+
keys_changed: z8.array(z8.string()),
|
|
2275
|
+
confirmation_token: z8.string().optional(),
|
|
2276
|
+
backup_meta_key: z8.string().optional(),
|
|
2277
|
+
css_flush: z8.string().optional()
|
|
2278
|
+
}),
|
|
2279
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2280
|
+
async handler(input) {
|
|
2281
|
+
if (!input.confirmation) {
|
|
2282
|
+
const token = issueConfirmation("update_widget_settings", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2283
|
+
return {
|
|
2284
|
+
mode: "dry_run",
|
|
2285
|
+
page_id: input.page_id,
|
|
2286
|
+
widget_id: input.widget_id,
|
|
2287
|
+
keys_changed: Object.keys(input.settings_patch),
|
|
2288
|
+
confirmation_token: token
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
const conf = consumeConfirmation(input.confirmation, "update_widget_settings");
|
|
2292
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2293
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2294
|
+
if (!updateWidgetSettings(data, input.widget_id, input.settings_patch)) {
|
|
2295
|
+
throw new Error(`Widget ${input.widget_id} not found`);
|
|
2296
|
+
}
|
|
2297
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2298
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2299
|
+
return {
|
|
2300
|
+
mode: "applied",
|
|
2301
|
+
page_id: input.page_id,
|
|
2302
|
+
widget_id: input.widget_id,
|
|
2303
|
+
keys_changed: Object.keys(input.settings_patch),
|
|
2304
|
+
backup_meta_key: backup.meta_key,
|
|
2305
|
+
css_flush: w.method
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
});
|
|
2309
|
+
var deleteWidgetTool = defineTool({
|
|
2310
|
+
name: "delete_widget",
|
|
2311
|
+
description: "Remove a widget from a page by id. Two-call confirmation. Backs up before deleting.",
|
|
2312
|
+
inputSchema: z8.object({
|
|
2313
|
+
site_id: z8.string().optional(),
|
|
2314
|
+
page_id: z8.number().int().positive(),
|
|
2315
|
+
widget_id: z8.string().min(1),
|
|
2316
|
+
confirmation: z8.string().optional()
|
|
2317
|
+
}),
|
|
2318
|
+
outputSchema: z8.object({
|
|
2319
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2320
|
+
page_id: z8.number(),
|
|
2321
|
+
widget_id: z8.string(),
|
|
2322
|
+
confirmation_token: z8.string().optional(),
|
|
2323
|
+
backup_meta_key: z8.string().optional(),
|
|
2324
|
+
css_flush: z8.string().optional()
|
|
2325
|
+
}),
|
|
2326
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2327
|
+
async handler(input) {
|
|
2328
|
+
if (!input.confirmation) {
|
|
2329
|
+
const token = issueConfirmation("delete_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2330
|
+
return { mode: "dry_run", page_id: input.page_id, widget_id: input.widget_id, confirmation_token: token };
|
|
2331
|
+
}
|
|
2332
|
+
const conf = consumeConfirmation(input.confirmation, "delete_widget");
|
|
2333
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2334
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2335
|
+
if (!deleteWidget(data, input.widget_id)) throw new Error(`Widget ${input.widget_id} not found`);
|
|
2336
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2337
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2338
|
+
return {
|
|
2339
|
+
mode: "applied",
|
|
2340
|
+
page_id: input.page_id,
|
|
2341
|
+
widget_id: input.widget_id,
|
|
2342
|
+
backup_meta_key: backup.meta_key,
|
|
2343
|
+
css_flush: w.method
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
var duplicateWidgetTool = defineTool({
|
|
2348
|
+
name: "duplicate_widget",
|
|
2349
|
+
description: "Duplicate a widget in place (right after the original). The clone gets a new id. Two-call confirmation.",
|
|
2350
|
+
inputSchema: z8.object({
|
|
2351
|
+
site_id: z8.string().optional(),
|
|
2352
|
+
page_id: z8.number().int().positive(),
|
|
2353
|
+
widget_id: z8.string().min(1),
|
|
2354
|
+
confirmation: z8.string().optional()
|
|
2355
|
+
}),
|
|
2356
|
+
outputSchema: z8.object({
|
|
2357
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2358
|
+
page_id: z8.number(),
|
|
2359
|
+
source_widget_id: z8.string(),
|
|
2360
|
+
new_widget_id: z8.string().optional(),
|
|
2361
|
+
confirmation_token: z8.string().optional(),
|
|
2362
|
+
backup_meta_key: z8.string().optional(),
|
|
2363
|
+
css_flush: z8.string().optional()
|
|
2364
|
+
}),
|
|
2365
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2366
|
+
async handler(input) {
|
|
2367
|
+
if (!input.confirmation) {
|
|
2368
|
+
const token = issueConfirmation("duplicate_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2369
|
+
return { mode: "dry_run", page_id: input.page_id, source_widget_id: input.widget_id, confirmation_token: token };
|
|
2370
|
+
}
|
|
2371
|
+
const conf = consumeConfirmation(input.confirmation, "duplicate_widget");
|
|
2372
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2373
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2374
|
+
const r = duplicateWidget(data, input.widget_id);
|
|
2375
|
+
if (!r.ok) throw new Error(`Widget ${input.widget_id} not found`);
|
|
2376
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2377
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2378
|
+
return {
|
|
2379
|
+
mode: "applied",
|
|
2380
|
+
page_id: input.page_id,
|
|
2381
|
+
source_widget_id: input.widget_id,
|
|
2382
|
+
new_widget_id: r.new_widget_id,
|
|
2383
|
+
backup_meta_key: backup.meta_key,
|
|
2384
|
+
css_flush: w.method
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
var swapWidgetTypeTool = defineTool({
|
|
2389
|
+
name: "swap_widget_type",
|
|
2390
|
+
description: "Replace a widget's type (e.g., heading \u2192 button) while preserving its position. Provide full new_settings \u2014 the old settings are NOT carried over (different widget types have incompatible schemas). Two-call confirmation.",
|
|
2391
|
+
inputSchema: z8.object({
|
|
2392
|
+
site_id: z8.string().optional(),
|
|
2393
|
+
page_id: z8.number().int().positive(),
|
|
2394
|
+
widget_id: z8.string().min(1),
|
|
2395
|
+
new_widget_type: z8.string().min(1),
|
|
2396
|
+
new_settings: z8.record(z8.any()).default({}),
|
|
2397
|
+
confirmation: z8.string().optional()
|
|
2398
|
+
}),
|
|
2399
|
+
outputSchema: z8.object({
|
|
2400
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2401
|
+
page_id: z8.number(),
|
|
2402
|
+
widget_id: z8.string(),
|
|
2403
|
+
new_widget_type: z8.string(),
|
|
2404
|
+
confirmation_token: z8.string().optional(),
|
|
2405
|
+
backup_meta_key: z8.string().optional(),
|
|
2406
|
+
css_flush: z8.string().optional()
|
|
2407
|
+
}),
|
|
2408
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2409
|
+
async handler(input) {
|
|
2410
|
+
if (!input.confirmation) {
|
|
2411
|
+
const token = issueConfirmation("swap_widget_type", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2412
|
+
return { mode: "dry_run", page_id: input.page_id, widget_id: input.widget_id, new_widget_type: input.new_widget_type, confirmation_token: token };
|
|
2413
|
+
}
|
|
2414
|
+
const conf = consumeConfirmation(input.confirmation, "swap_widget_type");
|
|
2415
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2416
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2417
|
+
if (!swapWidgetType(data, input.widget_id, input.new_widget_type, input.new_settings)) {
|
|
2418
|
+
throw new Error(`Widget ${input.widget_id} not found`);
|
|
2419
|
+
}
|
|
2420
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2421
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2422
|
+
return {
|
|
2423
|
+
mode: "applied",
|
|
2424
|
+
page_id: input.page_id,
|
|
2425
|
+
widget_id: input.widget_id,
|
|
2426
|
+
new_widget_type: input.new_widget_type,
|
|
2427
|
+
backup_meta_key: backup.meta_key,
|
|
2428
|
+
css_flush: w.method
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
var addWidgetTool = defineTool({
|
|
2433
|
+
name: "add_widget",
|
|
2434
|
+
description: "Append a new widget to a parent container (section, column, or container) on a page. Two-call confirmation.",
|
|
2435
|
+
inputSchema: z8.object({
|
|
2436
|
+
site_id: z8.string().optional(),
|
|
2437
|
+
page_id: z8.number().int().positive(),
|
|
2438
|
+
parent_id: z8.string().min(1).describe("Id of the section/column/container that will receive the widget."),
|
|
2439
|
+
widget_type: z8.string().min(1).describe("e.g., 'heading', 'text-editor', 'button', 'image'."),
|
|
2440
|
+
settings: z8.record(z8.any()).default({}),
|
|
2441
|
+
confirmation: z8.string().optional()
|
|
2442
|
+
}),
|
|
2443
|
+
outputSchema: z8.object({
|
|
2444
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2445
|
+
page_id: z8.number(),
|
|
2446
|
+
parent_id: z8.string(),
|
|
2447
|
+
widget_type: z8.string(),
|
|
2448
|
+
new_widget_id: z8.string().optional(),
|
|
2449
|
+
confirmation_token: z8.string().optional(),
|
|
2450
|
+
backup_meta_key: z8.string().optional(),
|
|
2451
|
+
css_flush: z8.string().optional()
|
|
2452
|
+
}),
|
|
2453
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2454
|
+
async handler(input) {
|
|
2455
|
+
if (!input.confirmation) {
|
|
2456
|
+
const token = issueConfirmation("add_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2457
|
+
return { mode: "dry_run", page_id: input.page_id, parent_id: input.parent_id, widget_type: input.widget_type, confirmation_token: token };
|
|
2458
|
+
}
|
|
2459
|
+
const conf = consumeConfirmation(input.confirmation, "add_widget");
|
|
2460
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2461
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2462
|
+
const r = addWidget(data, input.parent_id, input.widget_type, input.settings);
|
|
2463
|
+
if (!r.ok) throw new Error(`Parent ${input.parent_id} not found`);
|
|
2464
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2465
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2466
|
+
return {
|
|
2467
|
+
mode: "applied",
|
|
2468
|
+
page_id: input.page_id,
|
|
2469
|
+
parent_id: input.parent_id,
|
|
2470
|
+
widget_type: input.widget_type,
|
|
2471
|
+
new_widget_id: r.new_widget_id,
|
|
2472
|
+
backup_meta_key: backup.meta_key,
|
|
2473
|
+
css_flush: w.method
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
var moveWidgetTool = defineTool({
|
|
2478
|
+
name: "move_widget",
|
|
2479
|
+
description: "Move a widget to a different parent (or different position in the same parent). Two-call confirmation.",
|
|
2480
|
+
inputSchema: z8.object({
|
|
2481
|
+
site_id: z8.string().optional(),
|
|
2482
|
+
page_id: z8.number().int().positive(),
|
|
2483
|
+
widget_id: z8.string().min(1),
|
|
2484
|
+
new_parent_id: z8.string().min(1),
|
|
2485
|
+
position: z8.number().int().default(-1).describe("0-based position in the new parent. -1 = append."),
|
|
2486
|
+
confirmation: z8.string().optional()
|
|
2487
|
+
}),
|
|
2488
|
+
outputSchema: z8.object({
|
|
2489
|
+
mode: z8.enum(["dry_run", "applied"]),
|
|
2490
|
+
page_id: z8.number(),
|
|
2491
|
+
widget_id: z8.string(),
|
|
2492
|
+
new_parent_id: z8.string(),
|
|
2493
|
+
confirmation_token: z8.string().optional(),
|
|
2494
|
+
backup_meta_key: z8.string().optional(),
|
|
2495
|
+
css_flush: z8.string().optional()
|
|
2496
|
+
}),
|
|
2497
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2498
|
+
async handler(input) {
|
|
2499
|
+
if (!input.confirmation) {
|
|
2500
|
+
const token = issueConfirmation("move_widget", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2501
|
+
return { mode: "dry_run", page_id: input.page_id, widget_id: input.widget_id, new_parent_id: input.new_parent_id, confirmation_token: token };
|
|
2502
|
+
}
|
|
2503
|
+
const conf = consumeConfirmation(input.confirmation, "move_widget");
|
|
2504
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2505
|
+
const { data } = await fetchData(input.site_id, input.page_id);
|
|
2506
|
+
if (!moveWidget(data, input.widget_id, input.new_parent_id, input.position)) {
|
|
2507
|
+
throw new Error(`Widget or parent not found`);
|
|
2508
|
+
}
|
|
2509
|
+
const backup = await fullBackup(input.site_id, input.page_id);
|
|
2510
|
+
const w = await writeData(input.site_id, input.page_id, data);
|
|
2511
|
+
return {
|
|
2512
|
+
mode: "applied",
|
|
2513
|
+
page_id: input.page_id,
|
|
2514
|
+
widget_id: input.widget_id,
|
|
2515
|
+
new_parent_id: input.new_parent_id,
|
|
2516
|
+
backup_meta_key: backup.meta_key,
|
|
2517
|
+
css_flush: w.method
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
});
|
|
2521
|
+
|
|
2522
|
+
// src/tools/bulk.ts
|
|
2523
|
+
import { z as z9 } from "zod";
|
|
2524
|
+
init_wp_rest();
|
|
2525
|
+
init_config();
|
|
2526
|
+
init_backup();
|
|
2527
|
+
init_css_flush();
|
|
2528
|
+
init_policies();
|
|
2529
|
+
init_confirmation();
|
|
2530
|
+
async function listElementorPageIds(siteId) {
|
|
2531
|
+
const out = [];
|
|
2532
|
+
let page = 1;
|
|
2533
|
+
for (; ; ) {
|
|
2534
|
+
const items = await wpRequest("/wp/v2/pages", {
|
|
2535
|
+
siteId,
|
|
2536
|
+
query: {
|
|
2537
|
+
meta_key: "_elementor_edit_mode",
|
|
2538
|
+
meta_value: "builder",
|
|
2539
|
+
context: "edit",
|
|
2540
|
+
per_page: 100,
|
|
2541
|
+
page,
|
|
2542
|
+
_fields: "id,title"
|
|
2543
|
+
}
|
|
2544
|
+
});
|
|
2545
|
+
if (items.length === 0) break;
|
|
2546
|
+
out.push(...items.map((p) => ({ id: p.id, title: p.title.rendered })));
|
|
2547
|
+
if (items.length < 100) break;
|
|
2548
|
+
page++;
|
|
2549
|
+
if (page > 50) break;
|
|
2550
|
+
}
|
|
2551
|
+
return out;
|
|
2552
|
+
}
|
|
2553
|
+
var bulkFindReplaceSiteTool = defineTool({
|
|
2554
|
+
name: "bulk_find_replace_site",
|
|
2555
|
+
description: "Find/replace plain text in every Elementor page on a single site. TWO-CALL FLOW: dry-run returns per-page match_count + total + confirmation_token. Apply iterates each page (auto-backup + validate + flush per page). Slower than wp_search_replace but works without SSH and gives per-page granularity.",
|
|
2556
|
+
inputSchema: z9.object({
|
|
2557
|
+
site_id: z9.string().optional(),
|
|
2558
|
+
find: z9.string().min(1),
|
|
2559
|
+
replace: z9.string(),
|
|
2560
|
+
widget_type: z9.string().optional(),
|
|
2561
|
+
case_sensitive: z9.boolean().default(false),
|
|
2562
|
+
confirmation: z9.string().optional()
|
|
2563
|
+
}),
|
|
2564
|
+
outputSchema: z9.object({
|
|
2565
|
+
mode: z9.enum(["dry_run", "applied"]),
|
|
2566
|
+
site_id: z9.string(),
|
|
2567
|
+
pages_scanned: z9.number(),
|
|
2568
|
+
total_match_count: z9.number(),
|
|
2569
|
+
pages_with_matches: z9.array(z9.object({
|
|
2570
|
+
page_id: z9.number(),
|
|
2571
|
+
title: z9.string(),
|
|
2572
|
+
match_count: z9.number()
|
|
2573
|
+
})),
|
|
2574
|
+
pages_applied: z9.array(z9.object({
|
|
2575
|
+
page_id: z9.number(),
|
|
2576
|
+
backup_meta_key: z9.string().optional(),
|
|
2577
|
+
css_flush: z9.string().optional(),
|
|
2578
|
+
mode: z9.enum(["applied", "rolled_back", "skipped"]),
|
|
2579
|
+
error: z9.string().optional()
|
|
2580
|
+
})).optional(),
|
|
2581
|
+
confirmation_token: z9.string().optional(),
|
|
2582
|
+
expires_in_seconds: z9.number().optional()
|
|
2583
|
+
}),
|
|
2584
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2585
|
+
async handler(input) {
|
|
2586
|
+
const pages = await listElementorPageIds(input.site_id);
|
|
2587
|
+
const matches = [];
|
|
2588
|
+
for (const p of pages) {
|
|
2589
|
+
try {
|
|
2590
|
+
const page = await wpRequest(`/wp/v2/pages/${p.id}?context=edit&_fields=meta`, { siteId: input.site_id });
|
|
2591
|
+
const v = page.meta?._elementor_data;
|
|
2592
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2593
|
+
const data = parseElementorData(raw);
|
|
2594
|
+
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(data)), input.find, input.replace, {
|
|
2595
|
+
widgetType: input.widget_type,
|
|
2596
|
+
caseSensitive: input.case_sensitive
|
|
2597
|
+
});
|
|
2598
|
+
if (dry.replacementCount > 0) matches.push({ page_id: p.id, title: p.title, match_count: dry.replacementCount });
|
|
2599
|
+
} catch {
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
const total = matches.reduce((s, m) => s + m.match_count, 0);
|
|
2603
|
+
if (!input.confirmation) {
|
|
2604
|
+
if (total === 0) {
|
|
2605
|
+
return {
|
|
2606
|
+
mode: "dry_run",
|
|
2607
|
+
site_id: loadConfig().default_site_id ?? "default",
|
|
2608
|
+
pages_scanned: pages.length,
|
|
2609
|
+
total_match_count: 0,
|
|
2610
|
+
pages_with_matches: []
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
const token = issueConfirmation("bulk_find_replace_site", { find: input.find, replace: input.replace, page_ids: matches.map((m) => m.page_id) }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2614
|
+
return {
|
|
2615
|
+
mode: "dry_run",
|
|
2616
|
+
site_id: input.site_id ?? loadConfig().default_site_id ?? "default",
|
|
2617
|
+
pages_scanned: pages.length,
|
|
2618
|
+
total_match_count: total,
|
|
2619
|
+
pages_with_matches: matches,
|
|
2620
|
+
confirmation_token: token,
|
|
2621
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
const conf = consumeConfirmation(input.confirmation, "bulk_find_replace_site");
|
|
2625
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2626
|
+
const applied = [];
|
|
2627
|
+
for (const m of matches) {
|
|
2628
|
+
try {
|
|
2629
|
+
const page = await wpRequest(`/wp/v2/pages/${m.page_id}?context=edit&_fields=meta`, { siteId: input.site_id });
|
|
2630
|
+
const v = page.meta?._elementor_data;
|
|
2631
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2632
|
+
const data = parseElementorData(raw);
|
|
2633
|
+
findReplaceInWidgets(data, input.find, input.replace, { widgetType: input.widget_type, caseSensitive: input.case_sensitive });
|
|
2634
|
+
const ser = serializeElementorData(data);
|
|
2635
|
+
const validation = validateElementorData(ser);
|
|
2636
|
+
if (!validation.valid) {
|
|
2637
|
+
applied.push({ page_id: m.page_id, mode: "rolled_back", error: validation.errors.join("; ") });
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
const backup = await fullBackup(input.site_id, m.page_id);
|
|
2641
|
+
await wpRequest(`/wp/v2/pages/${m.page_id}`, {
|
|
2642
|
+
siteId: input.site_id,
|
|
2643
|
+
method: "PUT",
|
|
2644
|
+
body: { meta: { _elementor_data: ser } }
|
|
2645
|
+
});
|
|
2646
|
+
const flush = await flushCSS(input.site_id, m.page_id);
|
|
2647
|
+
applied.push({ page_id: m.page_id, backup_meta_key: backup.meta_key, css_flush: flush.method, mode: "applied" });
|
|
2648
|
+
} catch (e) {
|
|
2649
|
+
applied.push({ page_id: m.page_id, mode: "skipped", error: e.message });
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
return {
|
|
2653
|
+
mode: "applied",
|
|
2654
|
+
site_id: input.site_id ?? loadConfig().default_site_id ?? "default",
|
|
2655
|
+
pages_scanned: pages.length,
|
|
2656
|
+
total_match_count: total,
|
|
2657
|
+
pages_with_matches: matches,
|
|
2658
|
+
pages_applied: applied
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
var fleetFindReplaceTool = defineTool({
|
|
2663
|
+
name: "fleet_find_replace",
|
|
2664
|
+
description: "Find/replace plain text across every Elementor page of every site in the pool. Same flow as bulk_find_replace_site but iterates across sites. Returns per-site + grand-total summary. Dry-run first; second call applies. Use sparingly \u2014 this is the nuclear option.",
|
|
2665
|
+
inputSchema: z9.object({
|
|
2666
|
+
find: z9.string().min(1),
|
|
2667
|
+
replace: z9.string(),
|
|
2668
|
+
site_ids: z9.array(z9.string()).optional().describe("Subset of sites to hit. Defaults to all."),
|
|
2669
|
+
widget_type: z9.string().optional(),
|
|
2670
|
+
case_sensitive: z9.boolean().default(false),
|
|
2671
|
+
confirmation: z9.string().optional()
|
|
2672
|
+
}),
|
|
2673
|
+
outputSchema: z9.object({
|
|
2674
|
+
mode: z9.enum(["dry_run", "applied"]),
|
|
2675
|
+
sites_scanned: z9.number(),
|
|
2676
|
+
total_match_count: z9.number(),
|
|
2677
|
+
by_site: z9.array(z9.object({
|
|
2678
|
+
site_id: z9.string(),
|
|
2679
|
+
url: z9.string(),
|
|
2680
|
+
pages_scanned: z9.number(),
|
|
2681
|
+
matches: z9.number(),
|
|
2682
|
+
error: z9.string().optional()
|
|
2683
|
+
})),
|
|
2684
|
+
confirmation_token: z9.string().optional(),
|
|
2685
|
+
expires_in_seconds: z9.number().optional()
|
|
2686
|
+
}),
|
|
2687
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2688
|
+
async handler(input) {
|
|
2689
|
+
const cfg = loadConfig();
|
|
2690
|
+
const targets = input.site_ids ? cfg.sites.filter((s) => input.site_ids?.includes(s.id)) : cfg.sites;
|
|
2691
|
+
const by_site = [];
|
|
2692
|
+
let total = 0;
|
|
2693
|
+
for (const site of targets) {
|
|
2694
|
+
try {
|
|
2695
|
+
const pages = await listElementorPageIds(site.id);
|
|
2696
|
+
let siteMatches = 0;
|
|
2697
|
+
for (const p of pages) {
|
|
2698
|
+
try {
|
|
2699
|
+
const page = await wpRequest(`/wp/v2/pages/${p.id}?context=edit&_fields=meta`, { siteId: site.id });
|
|
2700
|
+
const v = page.meta?._elementor_data;
|
|
2701
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2702
|
+
const data = parseElementorData(raw);
|
|
2703
|
+
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(data)), input.find, input.replace, {
|
|
2704
|
+
widgetType: input.widget_type,
|
|
2705
|
+
caseSensitive: input.case_sensitive
|
|
2706
|
+
});
|
|
2707
|
+
siteMatches += dry.replacementCount;
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
by_site.push({ site_id: site.id, url: site.url, pages_scanned: pages.length, matches: siteMatches });
|
|
2712
|
+
total += siteMatches;
|
|
2713
|
+
} catch (e) {
|
|
2714
|
+
by_site.push({ site_id: site.id, url: site.url, pages_scanned: 0, matches: 0, error: e.message });
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (!input.confirmation) {
|
|
2718
|
+
if (total === 0) {
|
|
2719
|
+
return {
|
|
2720
|
+
mode: "dry_run",
|
|
2721
|
+
sites_scanned: by_site.length,
|
|
2722
|
+
total_match_count: 0,
|
|
2723
|
+
by_site
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
const token = issueConfirmation("fleet_find_replace", { find: input.find, replace: input.replace }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2727
|
+
return {
|
|
2728
|
+
mode: "dry_run",
|
|
2729
|
+
sites_scanned: by_site.length,
|
|
2730
|
+
total_match_count: total,
|
|
2731
|
+
by_site,
|
|
2732
|
+
confirmation_token: token,
|
|
2733
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
const conf = consumeConfirmation(input.confirmation, "fleet_find_replace");
|
|
2737
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2738
|
+
for (const site of targets) {
|
|
2739
|
+
const pages = await listElementorPageIds(site.id);
|
|
2740
|
+
for (const p of pages) {
|
|
2741
|
+
try {
|
|
2742
|
+
const page = await wpRequest(`/wp/v2/pages/${p.id}?context=edit&_fields=meta`, { siteId: site.id });
|
|
2743
|
+
const v = page.meta?._elementor_data;
|
|
2744
|
+
const raw = typeof v === "string" ? v : JSON.stringify(v ?? []);
|
|
2745
|
+
const data = parseElementorData(raw);
|
|
2746
|
+
const r = findReplaceInWidgets(data, input.find, input.replace, { widgetType: input.widget_type, caseSensitive: input.case_sensitive });
|
|
2747
|
+
if (r.replacementCount === 0) continue;
|
|
2748
|
+
const ser = serializeElementorData(data);
|
|
2749
|
+
const validation = validateElementorData(ser);
|
|
2750
|
+
if (!validation.valid) continue;
|
|
2751
|
+
await fullBackup(site.id, p.id);
|
|
2752
|
+
await wpRequest(`/wp/v2/pages/${p.id}`, {
|
|
2753
|
+
siteId: site.id,
|
|
2754
|
+
method: "PUT",
|
|
2755
|
+
body: { meta: { _elementor_data: ser } }
|
|
2756
|
+
});
|
|
2757
|
+
await flushCSS(site.id, p.id);
|
|
2758
|
+
} catch {
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
return {
|
|
2763
|
+
mode: "applied",
|
|
2764
|
+
sites_scanned: by_site.length,
|
|
2765
|
+
total_match_count: total,
|
|
2766
|
+
by_site
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
var restoreFromFileTool = defineTool({
|
|
2771
|
+
name: "restore_from_file",
|
|
2772
|
+
description: "Restore a page from a JSON backup file (created by ANY earlier op with backup_to_file=true or by direct fullBackup with to_file). Requires the file_path returned by that backup. Two-call confirmation.",
|
|
2773
|
+
inputSchema: z9.object({
|
|
2774
|
+
site_id: z9.string().optional(),
|
|
2775
|
+
page_id: z9.number().int().positive(),
|
|
2776
|
+
file_path: z9.string().min(1),
|
|
2777
|
+
confirmation: z9.string().optional()
|
|
2778
|
+
}),
|
|
2779
|
+
outputSchema: z9.object({
|
|
2780
|
+
mode: z9.enum(["dry_run", "restored"]),
|
|
2781
|
+
page_id: z9.number(),
|
|
2782
|
+
file_path: z9.string(),
|
|
2783
|
+
method: z9.enum(["wp-cli", "rest"]).optional(),
|
|
2784
|
+
pre_restore_backup_meta_key: z9.string().optional(),
|
|
2785
|
+
css_flush: z9.string().optional(),
|
|
2786
|
+
confirmation_token: z9.string().optional()
|
|
2787
|
+
}),
|
|
2788
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2789
|
+
async handler(input) {
|
|
2790
|
+
if (!input.confirmation) {
|
|
2791
|
+
const token = issueConfirmation("restore_from_file", input, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
2792
|
+
return {
|
|
2793
|
+
mode: "dry_run",
|
|
2794
|
+
page_id: input.page_id,
|
|
2795
|
+
file_path: input.file_path,
|
|
2796
|
+
confirmation_token: token
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
const conf = consumeConfirmation(input.confirmation, "restore_from_file");
|
|
2800
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
2801
|
+
const pre = await fullBackup(input.site_id, input.page_id);
|
|
2802
|
+
const r = await restoreFromFile(input.site_id, input.page_id, input.file_path);
|
|
2803
|
+
const flush = await flushCSS(input.site_id, input.page_id);
|
|
2804
|
+
return {
|
|
2805
|
+
mode: "restored",
|
|
2806
|
+
page_id: input.page_id,
|
|
2807
|
+
file_path: input.file_path,
|
|
2808
|
+
method: r.method,
|
|
2809
|
+
pre_restore_backup_meta_key: pre.meta_key,
|
|
2810
|
+
css_flush: flush.method
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
// src/tools/index.ts
|
|
2816
|
+
var tools = [
|
|
2817
|
+
// Sites & health
|
|
2818
|
+
listSitesTool,
|
|
2819
|
+
pingSiteTool,
|
|
2820
|
+
siteHealthTool,
|
|
2821
|
+
// Pages
|
|
2822
|
+
listElementorPagesTool,
|
|
2823
|
+
readPageElementorTool,
|
|
2824
|
+
listWidgetsInPageTool,
|
|
2825
|
+
listGlobalWidgetsTool,
|
|
2826
|
+
preflightCheckTool,
|
|
2827
|
+
findReplaceTool,
|
|
2828
|
+
listElementorBackupsTool,
|
|
2829
|
+
restoreElementorBackupTool,
|
|
2830
|
+
duplicateElementorPageTool,
|
|
2831
|
+
// Widget-level CRUD (v1.1)
|
|
2832
|
+
readWidgetTool,
|
|
2833
|
+
updateWidgetSettingsTool,
|
|
2834
|
+
deleteWidgetTool,
|
|
2835
|
+
duplicateWidgetTool,
|
|
2836
|
+
swapWidgetTypeTool,
|
|
2837
|
+
addWidgetTool,
|
|
2838
|
+
moveWidgetTool,
|
|
2839
|
+
// Templates
|
|
2840
|
+
listTemplatesTool,
|
|
2841
|
+
exportTemplateTool,
|
|
2842
|
+
importTemplateTool,
|
|
2843
|
+
applyTemplateToPageTool,
|
|
2844
|
+
// Bulk + fleet (v1.1)
|
|
2845
|
+
bulkFindReplaceSiteTool,
|
|
2846
|
+
fleetFindReplaceTool,
|
|
2847
|
+
restoreFromFileTool,
|
|
2848
|
+
// WP-CLI escape
|
|
2849
|
+
wpCliRunTool,
|
|
2850
|
+
wpSearchReplaceTool,
|
|
2851
|
+
wpElementorFlushCssTool,
|
|
2852
|
+
wpPluginListTool,
|
|
2853
|
+
wpPluginUpdateTool,
|
|
2854
|
+
// Visual
|
|
2855
|
+
screenshotPageTool,
|
|
2856
|
+
compareScreenshotsTool,
|
|
2857
|
+
// Versions
|
|
2858
|
+
checkElementorVersionsTool
|
|
2859
|
+
];
|
|
2860
|
+
|
|
2861
|
+
// src/server.ts
|
|
2862
|
+
init_logger();
|
|
850
2863
|
|
|
851
2864
|
// src/resources/index.ts
|
|
852
|
-
import { readFileSync as
|
|
853
|
-
import { dirname, resolve, join, basename } from "path";
|
|
2865
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3, readdirSync } from "fs";
|
|
2866
|
+
import { dirname, resolve, join as join3, basename } from "path";
|
|
854
2867
|
import { fileURLToPath } from "url";
|
|
855
2868
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
856
2869
|
var DOCS_DIR = resolve(__dirname, "../resources/elementor-docs");
|
|
857
2870
|
async function listResources() {
|
|
858
|
-
if (!
|
|
2871
|
+
if (!existsSync3(DOCS_DIR)) return [];
|
|
859
2872
|
const files = readdirSync(DOCS_DIR).filter((f) => f.endsWith(".md"));
|
|
860
2873
|
return files.map((f) => ({
|
|
861
2874
|
uri: `elementor-docs://${f}`,
|
|
@@ -869,14 +2882,14 @@ async function readResource(uri) {
|
|
|
869
2882
|
throw new Error(`Unknown resource URI: ${uri}`);
|
|
870
2883
|
}
|
|
871
2884
|
const filename = uri.replace("elementor-docs://", "");
|
|
872
|
-
const path =
|
|
873
|
-
if (!
|
|
2885
|
+
const path = join3(DOCS_DIR, filename);
|
|
2886
|
+
if (!existsSync3(path)) throw new Error(`Resource not found: ${filename}`);
|
|
874
2887
|
return {
|
|
875
2888
|
contents: [
|
|
876
2889
|
{
|
|
877
2890
|
uri,
|
|
878
2891
|
mimeType: "text/markdown",
|
|
879
|
-
text:
|
|
2892
|
+
text: readFileSync4(path, "utf8")
|
|
880
2893
|
}
|
|
881
2894
|
]
|
|
882
2895
|
};
|
|
@@ -884,7 +2897,7 @@ async function readResource(uri) {
|
|
|
884
2897
|
|
|
885
2898
|
// src/server.ts
|
|
886
2899
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
887
|
-
var pkg = JSON.parse(
|
|
2900
|
+
var pkg = JSON.parse(readFileSync5(resolve2(__dirname2, "../package.json"), "utf8"));
|
|
888
2901
|
function toMcpInputSchema(zod) {
|
|
889
2902
|
return zodToJsonSchema(zod, { target: "jsonSchema7", $refStrategy: "none" });
|
|
890
2903
|
}
|