elementor-mcp-agent 0.1.0 → 1.0.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 +108 -119
- package/dist/server.js +1592 -288
- 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 writeData = await sshWpCli(site, `post meta update ${postId} _elementor_data ${shellQuote(data_value)}`, { timeout_ms: 3e4 });
|
|
452
|
+
if (writeData.exitCode !== 0) throw new Error(`Restore write failed: ${writeData.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) {
|
|
@@ -422,59 +904,101 @@ function summarize(data) {
|
|
|
422
904
|
return { totalElements, sections, containers, columns, widgets, byWidgetType, maxDepth };
|
|
423
905
|
}
|
|
424
906
|
|
|
425
|
-
// src/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
907
|
+
// src/tools/pages.ts
|
|
908
|
+
init_backup();
|
|
909
|
+
init_css_flush();
|
|
910
|
+
|
|
911
|
+
// src/elementor/validator.ts
|
|
912
|
+
init_policies();
|
|
913
|
+
function validateElementorData(input) {
|
|
914
|
+
const errors = [];
|
|
915
|
+
const warnings = [];
|
|
916
|
+
if (typeof input === "string" && input.length > POLICIES.MAX_ELEMENTOR_DATA_BYTES) {
|
|
917
|
+
errors.push(`_elementor_data exceeds ${POLICIES.MAX_ELEMENTOR_DATA_BYTES} bytes (got ${input.length})`);
|
|
918
|
+
return { valid: false, errors, warnings };
|
|
919
|
+
}
|
|
920
|
+
let data;
|
|
921
|
+
try {
|
|
922
|
+
data = parseElementorData(input);
|
|
923
|
+
} catch (e) {
|
|
924
|
+
errors.push(`JSON parse failed: ${e.message}`);
|
|
925
|
+
return { valid: false, errors, warnings };
|
|
926
|
+
}
|
|
927
|
+
function validateElement(el, depth, path) {
|
|
928
|
+
const here = [...path, el.id ?? "?"];
|
|
929
|
+
if (typeof el.id !== "string" || el.id.length === 0) {
|
|
930
|
+
errors.push(`element at ${here.join(".")}: missing or empty id`);
|
|
931
|
+
}
|
|
932
|
+
if (!["section", "container", "column", "widget"].includes(el.elType)) {
|
|
933
|
+
errors.push(`element ${el.id}: unknown elType "${el.elType}"`);
|
|
934
|
+
}
|
|
935
|
+
if (el.elType === "widget" && (typeof el.widgetType !== "string" || el.widgetType.length === 0)) {
|
|
936
|
+
errors.push(`widget ${el.id}: missing widgetType`);
|
|
937
|
+
}
|
|
938
|
+
if (el.settings === null || el.settings === void 0 || typeof el.settings !== "object" || Array.isArray(el.settings)) {
|
|
939
|
+
errors.push(`element ${el.id}: settings must be an object (got ${typeof el.settings})`);
|
|
940
|
+
}
|
|
941
|
+
if (depth > 20) warnings.push(`element ${el.id}: depth ${depth} is unusually deep`);
|
|
942
|
+
if (Array.isArray(el.elements)) {
|
|
943
|
+
for (const child of el.elements) validateElement(child, depth + 1, here);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (!Array.isArray(data)) {
|
|
947
|
+
errors.push("Top-level must be an array");
|
|
948
|
+
return { valid: false, errors, warnings };
|
|
949
|
+
}
|
|
950
|
+
for (const el of data) validateElement(el, 0, []);
|
|
951
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/elementor/globals.ts
|
|
955
|
+
init_wp_rest();
|
|
956
|
+
async function listGlobalWidgets(siteId) {
|
|
957
|
+
const items = await wpRequest("/wp/v2/elementor_library", {
|
|
432
958
|
siteId,
|
|
433
|
-
|
|
434
|
-
|
|
959
|
+
query: {
|
|
960
|
+
context: "edit",
|
|
961
|
+
per_page: 100,
|
|
962
|
+
_fields: "id,title,meta"
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
const widgetsOnly = items.filter((t) => t.meta?._elementor_template_type === "widget");
|
|
966
|
+
return widgetsOnly.map((t) => {
|
|
967
|
+
const data = parseElementorData(t.meta?._elementor_data ?? "[]");
|
|
968
|
+
let widget_type;
|
|
969
|
+
for (const { element } of walkElements(data)) {
|
|
970
|
+
if (element.elType === "widget") {
|
|
971
|
+
widget_type = element.widgetType;
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
template_id: t.id,
|
|
977
|
+
title: t.title.rendered,
|
|
978
|
+
widget_type
|
|
979
|
+
};
|
|
435
980
|
});
|
|
436
|
-
return { meta_key, size_bytes: raw.length };
|
|
437
981
|
}
|
|
438
|
-
|
|
982
|
+
function findGlobalReferences(data) {
|
|
983
|
+
const out = [];
|
|
439
984
|
try {
|
|
440
|
-
|
|
441
|
-
|
|
985
|
+
for (const { element } of walkElements(parseElementorData(data))) {
|
|
986
|
+
if (element.elType === "widget" && element.widgetType === "global") {
|
|
987
|
+
const tid = element.settings.template_id ?? element.settings.templateID;
|
|
988
|
+
if (typeof tid === "number") out.push({ widget_id: element.id, template_id: tid });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
442
991
|
} 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
992
|
}
|
|
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;
|
|
993
|
+
return out;
|
|
472
994
|
}
|
|
473
995
|
|
|
474
996
|
// src/tools/pages.ts
|
|
997
|
+
init_confirmation();
|
|
998
|
+
init_policies();
|
|
475
999
|
var listElementorPagesTool = defineTool({
|
|
476
1000
|
name: "list_elementor_pages",
|
|
477
|
-
description: "List pages
|
|
1001
|
+
description: "List pages built with Elementor (have _elementor_edit_mode = 'builder'). Returns id, title, slug, status, modified date.",
|
|
478
1002
|
inputSchema: z3.object({
|
|
479
1003
|
site_id: z3.string().optional(),
|
|
480
1004
|
per_page: z3.number().int().min(1).max(100).default(25),
|
|
@@ -482,16 +1006,14 @@ var listElementorPagesTool = defineTool({
|
|
|
482
1006
|
}),
|
|
483
1007
|
outputSchema: z3.object({
|
|
484
1008
|
total: z3.number(),
|
|
485
|
-
pages: z3.array(
|
|
486
|
-
z3.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
})
|
|
494
|
-
)
|
|
1009
|
+
pages: z3.array(z3.object({
|
|
1010
|
+
id: z3.number(),
|
|
1011
|
+
title: z3.string(),
|
|
1012
|
+
slug: z3.string(),
|
|
1013
|
+
status: z3.string(),
|
|
1014
|
+
link: z3.string(),
|
|
1015
|
+
modified: z3.string()
|
|
1016
|
+
}))
|
|
495
1017
|
}),
|
|
496
1018
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
497
1019
|
async handler(input) {
|
|
@@ -521,11 +1043,11 @@ var listElementorPagesTool = defineTool({
|
|
|
521
1043
|
});
|
|
522
1044
|
var readPageElementorTool = defineTool({
|
|
523
1045
|
name: "read_page_elementor",
|
|
524
|
-
description: "Fetch
|
|
1046
|
+
description: "Fetch a page's Elementor data structure summary. With verbose=true returns the full parsed tree (potentially MBs).",
|
|
525
1047
|
inputSchema: z3.object({
|
|
526
1048
|
site_id: z3.string().optional(),
|
|
527
1049
|
page_id: z3.number().int().positive(),
|
|
528
|
-
verbose: z3.boolean().default(false)
|
|
1050
|
+
verbose: z3.boolean().default(false)
|
|
529
1051
|
}),
|
|
530
1052
|
outputSchema: z3.object({
|
|
531
1053
|
page_id: z3.number(),
|
|
@@ -539,6 +1061,7 @@ var readPageElementorTool = defineTool({
|
|
|
539
1061
|
maxDepth: z3.number(),
|
|
540
1062
|
byWidgetType: z3.record(z3.number())
|
|
541
1063
|
}),
|
|
1064
|
+
global_references: z3.array(z3.object({ widget_id: z3.string(), template_id: z3.number() })),
|
|
542
1065
|
data: z3.array(z3.any()).optional()
|
|
543
1066
|
}),
|
|
544
1067
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
@@ -549,113 +1072,426 @@ var readPageElementorTool = defineTool({
|
|
|
549
1072
|
);
|
|
550
1073
|
const raw = page.meta?._elementor_data ?? "[]";
|
|
551
1074
|
const data = parseElementorData(raw);
|
|
552
|
-
const summary = summarize(data);
|
|
553
1075
|
return {
|
|
554
1076
|
page_id: page.id,
|
|
555
1077
|
title: page.title.rendered,
|
|
556
|
-
summary,
|
|
1078
|
+
summary: summarize(data),
|
|
1079
|
+
global_references: findGlobalReferences(raw),
|
|
557
1080
|
data: input.verbose ? data : void 0
|
|
558
1081
|
};
|
|
559
1082
|
}
|
|
560
1083
|
});
|
|
1084
|
+
var listWidgetsInPageTool = defineTool({
|
|
1085
|
+
name: "list_widgets_in_page",
|
|
1086
|
+
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).",
|
|
1087
|
+
inputSchema: z3.object({
|
|
1088
|
+
site_id: z3.string().optional(),
|
|
1089
|
+
page_id: z3.number().int().positive(),
|
|
1090
|
+
widget_type: z3.string().optional()
|
|
1091
|
+
}),
|
|
1092
|
+
outputSchema: z3.object({
|
|
1093
|
+
page_id: z3.number(),
|
|
1094
|
+
total: z3.number(),
|
|
1095
|
+
widgets: z3.array(z3.object({
|
|
1096
|
+
widget_id: z3.string(),
|
|
1097
|
+
widget_type: z3.string(),
|
|
1098
|
+
path: z3.array(z3.string()),
|
|
1099
|
+
excerpt: z3.string().optional()
|
|
1100
|
+
}))
|
|
1101
|
+
}),
|
|
1102
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1103
|
+
async handler(input) {
|
|
1104
|
+
const page = await wpRequest(
|
|
1105
|
+
`/wp/v2/pages/${input.page_id}?context=edit&_fields=id,meta`,
|
|
1106
|
+
{ siteId: input.site_id }
|
|
1107
|
+
);
|
|
1108
|
+
const data = parseElementorData(page.meta?._elementor_data ?? "[]");
|
|
1109
|
+
const widgets = [];
|
|
1110
|
+
for (const { element, path } of walkElements(data)) {
|
|
1111
|
+
if (element.elType !== "widget") continue;
|
|
1112
|
+
if (input.widget_type && element.widgetType !== input.widget_type) continue;
|
|
1113
|
+
let excerpt;
|
|
1114
|
+
for (const v of Object.values(element.settings ?? {})) {
|
|
1115
|
+
if (typeof v === "string" && v.length > 0) {
|
|
1116
|
+
excerpt = v.replace(/<[^>]+>/g, "").slice(0, 80);
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
widgets.push({
|
|
1121
|
+
widget_id: element.id,
|
|
1122
|
+
widget_type: element.widgetType ?? "unknown",
|
|
1123
|
+
path: path.slice(0, -1),
|
|
1124
|
+
excerpt
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
return { page_id: page.id, total: widgets.length, widgets };
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
var listGlobalWidgetsTool = defineTool({
|
|
1131
|
+
name: "list_global_widgets",
|
|
1132
|
+
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.",
|
|
1133
|
+
inputSchema: z3.object({ site_id: z3.string().optional() }),
|
|
1134
|
+
outputSchema: z3.object({
|
|
1135
|
+
total: z3.number(),
|
|
1136
|
+
globals: z3.array(z3.object({
|
|
1137
|
+
template_id: z3.number(),
|
|
1138
|
+
title: z3.string(),
|
|
1139
|
+
widget_type: z3.string().optional()
|
|
1140
|
+
}))
|
|
1141
|
+
}),
|
|
1142
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1143
|
+
async handler(input) {
|
|
1144
|
+
const globals = await listGlobalWidgets(input.site_id);
|
|
1145
|
+
return { total: globals.length, globals };
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
var preflightCheckTool = defineTool({
|
|
1149
|
+
name: "preflight_check",
|
|
1150
|
+
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.",
|
|
1151
|
+
inputSchema: z3.object({
|
|
1152
|
+
site_id: z3.string().optional(),
|
|
1153
|
+
page_id: z3.number().int().positive()
|
|
1154
|
+
}),
|
|
1155
|
+
outputSchema: z3.object({
|
|
1156
|
+
safe_to_edit: z3.boolean(),
|
|
1157
|
+
page_id: z3.number(),
|
|
1158
|
+
title: z3.string(),
|
|
1159
|
+
issues: z3.array(z3.string()),
|
|
1160
|
+
warnings: z3.array(z3.string()),
|
|
1161
|
+
data_bytes: z3.number(),
|
|
1162
|
+
global_widget_references: z3.number()
|
|
1163
|
+
}),
|
|
1164
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1165
|
+
async handler(input) {
|
|
1166
|
+
const issues = [];
|
|
1167
|
+
const warnings = [];
|
|
1168
|
+
let title = "";
|
|
1169
|
+
let data_bytes = 0;
|
|
1170
|
+
let global_widget_references = 0;
|
|
1171
|
+
try {
|
|
1172
|
+
const page = await wpRequest(
|
|
1173
|
+
`/wp/v2/pages/${input.page_id}?context=edit`,
|
|
1174
|
+
{ siteId: input.site_id }
|
|
1175
|
+
);
|
|
1176
|
+
title = page.title.rendered;
|
|
1177
|
+
const raw = page.meta?._elementor_data ?? "[]";
|
|
1178
|
+
data_bytes = raw.length;
|
|
1179
|
+
if (page.meta?._elementor_edit_mode !== "builder") {
|
|
1180
|
+
issues.push("Page is not in Elementor builder mode");
|
|
1181
|
+
}
|
|
1182
|
+
const v = validateElementorData(raw);
|
|
1183
|
+
if (!v.valid) issues.push(...v.errors);
|
|
1184
|
+
warnings.push(...v.warnings);
|
|
1185
|
+
global_widget_references = findGlobalReferences(raw).length;
|
|
1186
|
+
if (global_widget_references > 0) {
|
|
1187
|
+
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.`);
|
|
1188
|
+
}
|
|
1189
|
+
if (data_bytes > POLICIES.MAX_ELEMENTOR_DATA_BYTES) {
|
|
1190
|
+
issues.push(`Elementor data exceeds policy max (${POLICIES.MAX_ELEMENTOR_DATA_BYTES} bytes)`);
|
|
1191
|
+
}
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
issues.push(`Cannot fetch page: ${e.message}`);
|
|
1194
|
+
}
|
|
1195
|
+
return { safe_to_edit: issues.length === 0, page_id: input.page_id, title, issues, warnings, data_bytes, global_widget_references };
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
561
1198
|
var findReplaceTool = defineTool({
|
|
562
1199
|
name: "elementor_find_replace",
|
|
563
|
-
description: "Find/replace plain text in every widget
|
|
1200
|
+
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
1201
|
inputSchema: z3.object({
|
|
565
1202
|
site_id: z3.string().optional(),
|
|
566
1203
|
page_id: z3.number().int().positive(),
|
|
567
1204
|
find: z3.string().min(1),
|
|
568
1205
|
replace: z3.string(),
|
|
569
|
-
widget_type: z3.string().optional()
|
|
1206
|
+
widget_type: z3.string().optional(),
|
|
570
1207
|
case_sensitive: z3.boolean().default(false),
|
|
571
|
-
|
|
1208
|
+
backup_to_file: z3.boolean().default(false).describe("Also dump backup to /tmp/elementor-mcp-backups/"),
|
|
1209
|
+
confirmation: z3.string().optional()
|
|
572
1210
|
}),
|
|
573
1211
|
outputSchema: z3.object({
|
|
574
|
-
mode: z3.enum(["dry_run", "applied"]),
|
|
1212
|
+
mode: z3.enum(["dry_run", "applied", "rolled_back"]),
|
|
575
1213
|
page_id: z3.number(),
|
|
576
1214
|
match_count: z3.number(),
|
|
1215
|
+
affected_widgets: z3.array(z3.object({
|
|
1216
|
+
widget_id: z3.string(),
|
|
1217
|
+
widget_type: z3.string(),
|
|
1218
|
+
before: z3.string(),
|
|
1219
|
+
after: z3.string()
|
|
1220
|
+
})).optional(),
|
|
577
1221
|
confirmation_token: z3.string().optional(),
|
|
578
1222
|
expires_in_seconds: z3.number().optional(),
|
|
579
1223
|
backup_meta_key: z3.string().optional(),
|
|
580
|
-
|
|
1224
|
+
backup_file: z3.string().optional(),
|
|
1225
|
+
css_flush: z3.string().optional(),
|
|
1226
|
+
validation_error: z3.string().optional()
|
|
581
1227
|
}),
|
|
582
1228
|
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
583
1229
|
async handler(input) {
|
|
584
|
-
const cfg = loadConfig();
|
|
585
1230
|
const page = await wpRequest(
|
|
586
|
-
`/wp/v2/pages/${input.page_id}?context=edit`,
|
|
1231
|
+
`/wp/v2/pages/${input.page_id}?context=edit&_fields=id,meta`,
|
|
587
1232
|
{ siteId: input.site_id }
|
|
588
1233
|
);
|
|
589
1234
|
const raw = page.meta?._elementor_data ?? "[]";
|
|
590
|
-
const
|
|
591
|
-
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(
|
|
1235
|
+
const dryData = parseElementorData(raw);
|
|
1236
|
+
const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(dryData)), input.find, input.replace, {
|
|
592
1237
|
widgetType: input.widget_type,
|
|
593
1238
|
caseSensitive: input.case_sensitive
|
|
594
1239
|
});
|
|
1240
|
+
const affected = [];
|
|
1241
|
+
const beforeData = parseElementorData(raw);
|
|
1242
|
+
const afterCopy = JSON.parse(JSON.stringify(beforeData));
|
|
1243
|
+
findReplaceInWidgets(afterCopy, input.find, input.replace, {
|
|
1244
|
+
widgetType: input.widget_type,
|
|
1245
|
+
caseSensitive: input.case_sensitive
|
|
1246
|
+
});
|
|
1247
|
+
function collectAffected(orig, modified) {
|
|
1248
|
+
const beforeMap = /* @__PURE__ */ new Map();
|
|
1249
|
+
for (const { element } of walkElements(orig)) {
|
|
1250
|
+
if (element.elType !== "widget") continue;
|
|
1251
|
+
const firstStr = Object.values(element.settings ?? {}).find((v) => typeof v === "string");
|
|
1252
|
+
if (firstStr) beforeMap.set(element.id, { type: element.widgetType ?? "?", first: firstStr });
|
|
1253
|
+
}
|
|
1254
|
+
for (const { element } of walkElements(modified)) {
|
|
1255
|
+
if (element.elType !== "widget") continue;
|
|
1256
|
+
const firstStr = Object.values(element.settings ?? {}).find((v) => typeof v === "string");
|
|
1257
|
+
const b = beforeMap.get(element.id);
|
|
1258
|
+
if (b && firstStr && b.first !== firstStr) {
|
|
1259
|
+
affected.push({
|
|
1260
|
+
widget_id: element.id,
|
|
1261
|
+
widget_type: element.widgetType ?? "?",
|
|
1262
|
+
before: b.first.slice(0, 120),
|
|
1263
|
+
after: firstStr.slice(0, 120)
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
collectAffected(beforeData, afterCopy);
|
|
595
1269
|
if (!input.confirmation) {
|
|
596
1270
|
if (dry.replacementCount === 0) {
|
|
597
|
-
return {
|
|
1271
|
+
return {
|
|
1272
|
+
mode: "dry_run",
|
|
1273
|
+
page_id: input.page_id,
|
|
1274
|
+
match_count: 0,
|
|
1275
|
+
affected_widgets: []
|
|
1276
|
+
};
|
|
598
1277
|
}
|
|
599
1278
|
const token = issueConfirmation(
|
|
600
1279
|
"elementor_find_replace",
|
|
601
1280
|
{ page_id: input.page_id, find: input.find, replace: input.replace },
|
|
602
|
-
|
|
1281
|
+
POLICIES.CONFIRMATION_TTL_SECONDS
|
|
603
1282
|
);
|
|
604
1283
|
return {
|
|
605
1284
|
mode: "dry_run",
|
|
606
1285
|
page_id: input.page_id,
|
|
607
1286
|
match_count: dry.replacementCount,
|
|
1287
|
+
affected_widgets: affected.slice(0, 25),
|
|
608
1288
|
confirmation_token: token,
|
|
609
|
-
expires_in_seconds:
|
|
1289
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
610
1290
|
};
|
|
611
1291
|
}
|
|
612
1292
|
const conf = consumeConfirmation(input.confirmation, "elementor_find_replace");
|
|
613
1293
|
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
614
|
-
const
|
|
615
|
-
if (
|
|
1294
|
+
const o = conf.payload;
|
|
1295
|
+
if (o.page_id !== input.page_id || o.find !== input.find || o.replace !== input.replace) {
|
|
616
1296
|
throw new Error("Confirmation parameters don't match the original dry-run");
|
|
617
1297
|
}
|
|
618
|
-
const backup = await
|
|
619
|
-
const
|
|
1298
|
+
const backup = await fullBackup(input.site_id, input.page_id, { to_file: input.backup_to_file });
|
|
1299
|
+
const newData = parseElementorData(raw);
|
|
1300
|
+
findReplaceInWidgets(newData, input.find, input.replace, {
|
|
620
1301
|
widgetType: input.widget_type,
|
|
621
1302
|
caseSensitive: input.case_sensitive
|
|
622
1303
|
});
|
|
1304
|
+
const serialized = serializeElementorData(newData);
|
|
1305
|
+
const validation = validateElementorData(serialized);
|
|
1306
|
+
if (!validation.valid) {
|
|
1307
|
+
return {
|
|
1308
|
+
mode: "rolled_back",
|
|
1309
|
+
page_id: input.page_id,
|
|
1310
|
+
match_count: dry.replacementCount,
|
|
1311
|
+
affected_widgets: affected.slice(0, 25),
|
|
1312
|
+
backup_meta_key: backup.meta_key,
|
|
1313
|
+
validation_error: validation.errors.join("; ")
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
623
1316
|
await wpRequest(`/wp/v2/pages/${input.page_id}`, {
|
|
624
1317
|
siteId: input.site_id,
|
|
625
1318
|
method: "PUT",
|
|
626
|
-
body: { meta: { _elementor_data:
|
|
1319
|
+
body: { meta: { _elementor_data: serialized } }
|
|
627
1320
|
});
|
|
628
|
-
const flush = await
|
|
1321
|
+
const flush = POLICIES.FLUSH_CSS_AFTER_WRITE ? await flushCSS(input.site_id, input.page_id) : { method: "none" };
|
|
629
1322
|
return {
|
|
630
1323
|
mode: "applied",
|
|
631
1324
|
page_id: input.page_id,
|
|
632
|
-
match_count:
|
|
1325
|
+
match_count: dry.replacementCount,
|
|
1326
|
+
affected_widgets: affected.slice(0, 25),
|
|
633
1327
|
backup_meta_key: backup.meta_key,
|
|
1328
|
+
backup_file: backup.file_path,
|
|
1329
|
+
css_flush: flush.method
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
var listElementorBackupsTool = defineTool({
|
|
1334
|
+
name: "list_elementor_backups",
|
|
1335
|
+
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.",
|
|
1336
|
+
inputSchema: z3.object({
|
|
1337
|
+
site_id: z3.string().optional(),
|
|
1338
|
+
page_id: z3.number().int().positive()
|
|
1339
|
+
}),
|
|
1340
|
+
outputSchema: z3.object({
|
|
1341
|
+
page_id: z3.number(),
|
|
1342
|
+
total: z3.number(),
|
|
1343
|
+
backups: z3.array(z3.object({
|
|
1344
|
+
meta_key: z3.string(),
|
|
1345
|
+
settings_key: z3.string().optional(),
|
|
1346
|
+
timestamp: z3.string()
|
|
1347
|
+
}))
|
|
1348
|
+
}),
|
|
1349
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1350
|
+
async handler(input) {
|
|
1351
|
+
const backups = await listBackups(input.site_id, input.page_id);
|
|
1352
|
+
return {
|
|
1353
|
+
page_id: input.page_id,
|
|
1354
|
+
total: backups.length,
|
|
1355
|
+
backups
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
var restoreElementorBackupTool = defineTool({
|
|
1360
|
+
name: "restore_elementor_backup",
|
|
1361
|
+
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.",
|
|
1362
|
+
inputSchema: z3.object({
|
|
1363
|
+
site_id: z3.string().optional(),
|
|
1364
|
+
page_id: z3.number().int().positive(),
|
|
1365
|
+
backup_meta_key: z3.string().describe("From list_elementor_backups."),
|
|
1366
|
+
settings_meta_key: z3.string().optional(),
|
|
1367
|
+
confirmation: z3.string().optional()
|
|
1368
|
+
}),
|
|
1369
|
+
outputSchema: z3.object({
|
|
1370
|
+
mode: z3.enum(["dry_run", "restored"]),
|
|
1371
|
+
page_id: z3.number(),
|
|
1372
|
+
backup_meta_key: z3.string(),
|
|
1373
|
+
settings_meta_key: z3.string().optional(),
|
|
1374
|
+
confirmation_token: z3.string().optional(),
|
|
1375
|
+
expires_in_seconds: z3.number().optional(),
|
|
1376
|
+
css_flush: z3.string().optional(),
|
|
1377
|
+
pre_restore_backup_meta_key: z3.string().optional()
|
|
1378
|
+
}),
|
|
1379
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1380
|
+
async handler(input) {
|
|
1381
|
+
if (!input.confirmation) {
|
|
1382
|
+
const token = issueConfirmation("restore_elementor_backup", {
|
|
1383
|
+
page_id: input.page_id,
|
|
1384
|
+
backup_meta_key: input.backup_meta_key
|
|
1385
|
+
}, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
1386
|
+
return {
|
|
1387
|
+
mode: "dry_run",
|
|
1388
|
+
page_id: input.page_id,
|
|
1389
|
+
backup_meta_key: input.backup_meta_key,
|
|
1390
|
+
settings_meta_key: input.settings_meta_key,
|
|
1391
|
+
confirmation_token: token,
|
|
1392
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
const conf = consumeConfirmation(input.confirmation, "restore_elementor_backup");
|
|
1396
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1397
|
+
const pre = await fullBackup(input.site_id, input.page_id);
|
|
1398
|
+
await restoreBackup(input.site_id, input.page_id, input.backup_meta_key, input.settings_meta_key);
|
|
1399
|
+
const flush = await flushCSS(input.site_id, input.page_id);
|
|
1400
|
+
return {
|
|
1401
|
+
mode: "restored",
|
|
1402
|
+
page_id: input.page_id,
|
|
1403
|
+
backup_meta_key: input.backup_meta_key,
|
|
1404
|
+
settings_meta_key: input.settings_meta_key,
|
|
1405
|
+
css_flush: flush.method,
|
|
1406
|
+
pre_restore_backup_meta_key: pre.meta_key
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
var duplicateElementorPageTool = defineTool({
|
|
1411
|
+
name: "duplicate_elementor_page",
|
|
1412
|
+
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.",
|
|
1413
|
+
inputSchema: z3.object({
|
|
1414
|
+
site_id: z3.string().optional(),
|
|
1415
|
+
source_page_id: z3.number().int().positive(),
|
|
1416
|
+
new_title: z3.string().optional().describe("Defaults to '<original> (Copy)'"),
|
|
1417
|
+
status: z3.enum(["draft", "publish", "private"]).default("draft")
|
|
1418
|
+
}),
|
|
1419
|
+
outputSchema: z3.object({
|
|
1420
|
+
new_page_id: z3.number(),
|
|
1421
|
+
new_page_url: z3.string(),
|
|
1422
|
+
source_page_id: z3.number(),
|
|
1423
|
+
title: z3.string(),
|
|
1424
|
+
status: z3.string(),
|
|
1425
|
+
css_flush: z3.string().optional()
|
|
1426
|
+
}),
|
|
1427
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1428
|
+
async handler(input) {
|
|
1429
|
+
const source = await wpRequest(
|
|
1430
|
+
`/wp/v2/pages/${input.source_page_id}?context=edit`,
|
|
1431
|
+
{ siteId: input.site_id }
|
|
1432
|
+
);
|
|
1433
|
+
const title = input.new_title ?? `${source.title.rendered} (Copy)`;
|
|
1434
|
+
const data_raw = source.meta?._elementor_data ?? "[]";
|
|
1435
|
+
const settings_raw = source.meta?._elementor_page_settings ?? "{}";
|
|
1436
|
+
const created = await wpRequest("/wp/v2/pages", {
|
|
1437
|
+
siteId: input.site_id,
|
|
1438
|
+
method: "POST",
|
|
1439
|
+
body: {
|
|
1440
|
+
title,
|
|
1441
|
+
status: input.status,
|
|
1442
|
+
meta: {
|
|
1443
|
+
_elementor_data: data_raw,
|
|
1444
|
+
_elementor_page_settings: parseSettingsForRest(settings_raw),
|
|
1445
|
+
_elementor_edit_mode: "builder"
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
const flush = await flushCSS(input.site_id, created.id);
|
|
1450
|
+
return {
|
|
1451
|
+
new_page_id: created.id,
|
|
1452
|
+
new_page_url: created.link,
|
|
1453
|
+
source_page_id: input.source_page_id,
|
|
1454
|
+
title,
|
|
1455
|
+
status: input.status,
|
|
634
1456
|
css_flush: flush.method
|
|
635
1457
|
};
|
|
636
1458
|
}
|
|
637
1459
|
});
|
|
1460
|
+
function parseSettingsForRest(raw) {
|
|
1461
|
+
if (typeof raw === "object" && raw !== null) return raw;
|
|
1462
|
+
if (typeof raw === "string") {
|
|
1463
|
+
if (!raw.trim()) return {};
|
|
1464
|
+
try {
|
|
1465
|
+
return JSON.parse(raw);
|
|
1466
|
+
} catch {
|
|
1467
|
+
return {};
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return {};
|
|
1471
|
+
}
|
|
638
1472
|
|
|
639
1473
|
// src/tools/templates.ts
|
|
640
1474
|
import { z as z4 } from "zod";
|
|
1475
|
+
init_wp_rest();
|
|
1476
|
+
var TEMPLATE_TYPES = ["section", "page", "popup", "header", "footer", "archive", "single", "widget", "any"];
|
|
641
1477
|
var listTemplatesTool = defineTool({
|
|
642
1478
|
name: "list_elementor_templates",
|
|
643
|
-
description: "List Elementor library
|
|
1479
|
+
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
1480
|
inputSchema: z4.object({
|
|
645
1481
|
site_id: z4.string().optional(),
|
|
646
|
-
type: z4.enum(
|
|
1482
|
+
type: z4.enum(TEMPLATE_TYPES).default("any"),
|
|
647
1483
|
per_page: z4.number().int().min(1).max(100).default(50)
|
|
648
1484
|
}),
|
|
649
1485
|
outputSchema: z4.object({
|
|
650
1486
|
total: z4.number(),
|
|
651
|
-
templates: z4.array(
|
|
652
|
-
z4.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
)
|
|
1487
|
+
templates: z4.array(z4.object({
|
|
1488
|
+
id: z4.number(),
|
|
1489
|
+
title: z4.string(),
|
|
1490
|
+
type: z4.string(),
|
|
1491
|
+
is_theme_builder: z4.boolean(),
|
|
1492
|
+
display_conditions_count: z4.number(),
|
|
1493
|
+
modified: z4.string()
|
|
1494
|
+
}))
|
|
659
1495
|
}),
|
|
660
1496
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
661
1497
|
async handler(input) {
|
|
@@ -664,28 +1500,35 @@ var listTemplatesTool = defineTool({
|
|
|
664
1500
|
context: "edit",
|
|
665
1501
|
_fields: "id,title,modified,meta"
|
|
666
1502
|
};
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
const items = await wpRequest("/wp/v2/elementor_library", {
|
|
672
|
-
siteId: input.site_id,
|
|
673
|
-
query
|
|
674
|
-
});
|
|
1503
|
+
const allItems = await wpRequest("/wp/v2/elementor_library", { siteId: input.site_id, query });
|
|
1504
|
+
const items = input.type === "any" ? allItems : allItems.filter((t) => t.meta?._elementor_template_type === input.type);
|
|
1505
|
+
const themeBuilderTypes = /* @__PURE__ */ new Set(["header", "footer", "archive", "single"]);
|
|
675
1506
|
return {
|
|
676
1507
|
total: items.length,
|
|
677
|
-
templates: items.map((t) =>
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1508
|
+
templates: items.map((t) => {
|
|
1509
|
+
const type = t.meta?._elementor_template_type ?? "unknown";
|
|
1510
|
+
let conds = 0;
|
|
1511
|
+
try {
|
|
1512
|
+
const c = t.meta?._elementor_conditions;
|
|
1513
|
+
if (typeof c === "string") conds = JSON.parse(c).length;
|
|
1514
|
+
else if (Array.isArray(c)) conds = c.length;
|
|
1515
|
+
} catch {
|
|
1516
|
+
}
|
|
1517
|
+
return {
|
|
1518
|
+
id: t.id,
|
|
1519
|
+
title: t.title.rendered,
|
|
1520
|
+
type,
|
|
1521
|
+
is_theme_builder: themeBuilderTypes.has(type),
|
|
1522
|
+
display_conditions_count: conds,
|
|
1523
|
+
modified: t.modified
|
|
1524
|
+
};
|
|
1525
|
+
})
|
|
683
1526
|
};
|
|
684
1527
|
}
|
|
685
1528
|
});
|
|
686
1529
|
var exportTemplateTool = defineTool({
|
|
687
1530
|
name: "export_elementor_template",
|
|
688
|
-
description: "Export an Elementor template as a portable JSON object. Output
|
|
1531
|
+
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
1532
|
inputSchema: z4.object({
|
|
690
1533
|
site_id: z4.string().optional(),
|
|
691
1534
|
template_id: z4.number().int().positive()
|
|
@@ -694,18 +1537,12 @@ var exportTemplateTool = defineTool({
|
|
|
694
1537
|
template_id: z4.number(),
|
|
695
1538
|
title: z4.string(),
|
|
696
1539
|
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.")
|
|
1540
|
+
summary: z4.object({ totalElements: z4.number(), widgets: z4.number(), sections: z4.number() }),
|
|
1541
|
+
portable_json: z4.string()
|
|
703
1542
|
}),
|
|
704
1543
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
705
1544
|
async handler(input) {
|
|
706
|
-
const tpl = await wpRequest(`/wp/v2/elementor_library/${input.template_id}?context=edit`, {
|
|
707
|
-
siteId: input.site_id
|
|
708
|
-
});
|
|
1545
|
+
const tpl = await wpRequest(`/wp/v2/elementor_library/${input.template_id}?context=edit`, { siteId: input.site_id });
|
|
709
1546
|
const data = parseElementorData(tpl.meta?._elementor_data ?? "[]");
|
|
710
1547
|
const sum = summarize(data);
|
|
711
1548
|
const portable = {
|
|
@@ -725,10 +1562,10 @@ var exportTemplateTool = defineTool({
|
|
|
725
1562
|
});
|
|
726
1563
|
var importTemplateTool = defineTool({
|
|
727
1564
|
name: "import_elementor_template",
|
|
728
|
-
description: "Import
|
|
1565
|
+
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
1566
|
inputSchema: z4.object({
|
|
730
|
-
site_id: z4.string().optional()
|
|
731
|
-
portable_json: z4.string()
|
|
1567
|
+
site_id: z4.string().optional(),
|
|
1568
|
+
portable_json: z4.string(),
|
|
732
1569
|
override_title: z4.string().optional()
|
|
733
1570
|
}),
|
|
734
1571
|
outputSchema: z4.object({
|
|
@@ -760,40 +1597,493 @@ var importTemplateTool = defineTool({
|
|
|
760
1597
|
}
|
|
761
1598
|
}
|
|
762
1599
|
});
|
|
1600
|
+
return { new_template_id: res.id, title, type: payload.type, url: res.link };
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
var applyTemplateToPageTool = defineTool({
|
|
1604
|
+
name: "apply_template_to_page",
|
|
1605
|
+
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.",
|
|
1606
|
+
inputSchema: z4.object({
|
|
1607
|
+
site_id: z4.string().optional(),
|
|
1608
|
+
source_id: z4.number().int().positive().describe("Source post id: a template or a page."),
|
|
1609
|
+
target_page_id: z4.number().int().positive(),
|
|
1610
|
+
backup_to_file: z4.boolean().default(false),
|
|
1611
|
+
confirmation: z4.string().optional()
|
|
1612
|
+
}),
|
|
1613
|
+
outputSchema: z4.object({
|
|
1614
|
+
mode: z4.enum(["dry_run", "applied"]),
|
|
1615
|
+
target_page_id: z4.number(),
|
|
1616
|
+
source_id: z4.number(),
|
|
1617
|
+
confirmation_token: z4.string().optional(),
|
|
1618
|
+
expires_in_seconds: z4.number().optional(),
|
|
1619
|
+
backup_meta_key: z4.string().optional(),
|
|
1620
|
+
backup_file: z4.string().optional(),
|
|
1621
|
+
css_flush: z4.string().optional()
|
|
1622
|
+
}),
|
|
1623
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1624
|
+
async handler(input) {
|
|
1625
|
+
const { issueConfirmation: issueConfirmation2, consumeConfirmation: consumeConfirmation2 } = await Promise.resolve().then(() => (init_confirmation(), confirmation_exports));
|
|
1626
|
+
const { POLICIES: POLICIES2 } = await Promise.resolve().then(() => (init_policies(), policies_exports));
|
|
1627
|
+
const { fullBackup: fullBackup2 } = await Promise.resolve().then(() => (init_backup(), backup_exports));
|
|
1628
|
+
const { flushCSS: flushCSS2 } = await Promise.resolve().then(() => (init_css_flush(), css_flush_exports));
|
|
1629
|
+
if (!input.confirmation) {
|
|
1630
|
+
const token = issueConfirmation2("apply_template_to_page", { src: input.source_id, tgt: input.target_page_id, site: input.site_id }, POLICIES2.CONFIRMATION_TTL_SECONDS);
|
|
1631
|
+
return {
|
|
1632
|
+
mode: "dry_run",
|
|
1633
|
+
target_page_id: input.target_page_id,
|
|
1634
|
+
source_id: input.source_id,
|
|
1635
|
+
confirmation_token: token,
|
|
1636
|
+
expires_in_seconds: POLICIES2.CONFIRMATION_TTL_SECONDS
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
const conf = consumeConfirmation2(input.confirmation, "apply_template_to_page");
|
|
1640
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1641
|
+
let src_meta;
|
|
1642
|
+
try {
|
|
1643
|
+
const src = await wpRequest(`/wp/v2/elementor_library/${input.source_id}?context=edit`, { siteId: input.site_id });
|
|
1644
|
+
src_meta = src.meta;
|
|
1645
|
+
} catch {
|
|
1646
|
+
const src = await wpRequest(`/wp/v2/pages/${input.source_id}?context=edit`, { siteId: input.site_id });
|
|
1647
|
+
src_meta = src.meta;
|
|
1648
|
+
}
|
|
1649
|
+
const data_raw = src_meta?._elementor_data ?? "[]";
|
|
1650
|
+
const settings_raw = src_meta?._elementor_page_settings ?? "{}";
|
|
1651
|
+
const backup = await fullBackup2(input.site_id, input.target_page_id, { to_file: input.backup_to_file });
|
|
1652
|
+
await wpRequest(`/wp/v2/pages/${input.target_page_id}`, {
|
|
1653
|
+
siteId: input.site_id,
|
|
1654
|
+
method: "PUT",
|
|
1655
|
+
body: {
|
|
1656
|
+
meta: {
|
|
1657
|
+
_elementor_data: data_raw,
|
|
1658
|
+
_elementor_page_settings: parseSettingsForRest2(settings_raw),
|
|
1659
|
+
_elementor_edit_mode: "builder"
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
const flush = await flushCSS2(input.site_id, input.target_page_id);
|
|
763
1664
|
return {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1665
|
+
mode: "applied",
|
|
1666
|
+
target_page_id: input.target_page_id,
|
|
1667
|
+
source_id: input.source_id,
|
|
1668
|
+
backup_meta_key: backup.meta_key,
|
|
1669
|
+
backup_file: backup.file_path,
|
|
1670
|
+
css_flush: flush.method
|
|
768
1671
|
};
|
|
769
1672
|
}
|
|
770
1673
|
});
|
|
1674
|
+
function parseSettingsForRest2(raw) {
|
|
1675
|
+
if (typeof raw === "object" && raw !== null) return raw;
|
|
1676
|
+
if (typeof raw === "string") {
|
|
1677
|
+
if (!raw.trim()) return {};
|
|
1678
|
+
try {
|
|
1679
|
+
return JSON.parse(raw);
|
|
1680
|
+
} catch {
|
|
1681
|
+
return {};
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return {};
|
|
1685
|
+
}
|
|
771
1686
|
|
|
772
|
-
// src/tools/
|
|
1687
|
+
// src/tools/wpcli.ts
|
|
773
1688
|
import { z as z5 } from "zod";
|
|
1689
|
+
init_config();
|
|
1690
|
+
init_ssh_wpcli();
|
|
1691
|
+
init_policies();
|
|
1692
|
+
init_confirmation();
|
|
1693
|
+
var wpCliRunTool = defineTool({
|
|
1694
|
+
name: "wp_cli_run",
|
|
1695
|
+
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.",
|
|
1696
|
+
inputSchema: z5.object({
|
|
1697
|
+
site_id: z5.string().optional(),
|
|
1698
|
+
args: z5.string().min(1).describe("WP-CLI args without leading 'wp', e.g. 'post list --post_type=page'"),
|
|
1699
|
+
timeout_ms: z5.number().int().min(1e3).max(3e5).default(6e4),
|
|
1700
|
+
confirmation: z5.string().optional()
|
|
1701
|
+
}),
|
|
1702
|
+
outputSchema: z5.object({
|
|
1703
|
+
mode: z5.enum(["dry_run_destructive", "executed"]),
|
|
1704
|
+
command: z5.string(),
|
|
1705
|
+
stdout: z5.string().optional(),
|
|
1706
|
+
stderr: z5.string().optional(),
|
|
1707
|
+
exit_code: z5.number().optional(),
|
|
1708
|
+
duration_ms: z5.number().optional(),
|
|
1709
|
+
destructive_pattern_detected: z5.boolean().optional(),
|
|
1710
|
+
confirmation_token: z5.string().optional(),
|
|
1711
|
+
expires_in_seconds: z5.number().optional()
|
|
1712
|
+
}),
|
|
1713
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1714
|
+
async handler(input) {
|
|
1715
|
+
const forbidden = isForbiddenWpCli(input.args);
|
|
1716
|
+
if (forbidden.forbidden) {
|
|
1717
|
+
throw new Error(`Forbidden wp-cli command. ${forbidden.reason}`);
|
|
1718
|
+
}
|
|
1719
|
+
const isDestructive = isDestructiveWpCli(input.args);
|
|
1720
|
+
if (isDestructive && !input.confirmation) {
|
|
1721
|
+
const token = issueConfirmation("wp_cli_run", { site_id: input.site_id, args: input.args }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
1722
|
+
return {
|
|
1723
|
+
mode: "dry_run_destructive",
|
|
1724
|
+
command: `wp <path> ${input.args}`,
|
|
1725
|
+
destructive_pattern_detected: true,
|
|
1726
|
+
confirmation_token: token,
|
|
1727
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
if (isDestructive && input.confirmation) {
|
|
1731
|
+
const conf = consumeConfirmation(input.confirmation, "wp_cli_run");
|
|
1732
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1733
|
+
const o = conf.payload;
|
|
1734
|
+
if (o.args !== input.args || o.site_id !== input.site_id) {
|
|
1735
|
+
throw new Error("Confirmation parameters don't match the original dry-run");
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
const site = getSite(input.site_id);
|
|
1739
|
+
const r = await sshWpCli(site, input.args, { timeout_ms: input.timeout_ms });
|
|
1740
|
+
return {
|
|
1741
|
+
mode: "executed",
|
|
1742
|
+
command: r.command,
|
|
1743
|
+
stdout: r.stdout,
|
|
1744
|
+
stderr: r.stderr,
|
|
1745
|
+
exit_code: r.exitCode,
|
|
1746
|
+
duration_ms: r.duration_ms,
|
|
1747
|
+
destructive_pattern_detected: isDestructive
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
var wpSearchReplaceTool = defineTool({
|
|
1752
|
+
name: "wp_search_replace",
|
|
1753
|
+
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'.",
|
|
1754
|
+
inputSchema: z5.object({
|
|
1755
|
+
site_id: z5.string().optional(),
|
|
1756
|
+
find: z5.string().min(1),
|
|
1757
|
+
replace: z5.string(),
|
|
1758
|
+
table: z5.string().default("wp_postmeta"),
|
|
1759
|
+
include_columns: z5.string().optional().describe("e.g. 'meta_value'. Default: meta_value when table=wp_postmeta."),
|
|
1760
|
+
precise: z5.boolean().default(true),
|
|
1761
|
+
confirmation: z5.string().optional()
|
|
1762
|
+
}),
|
|
1763
|
+
outputSchema: z5.object({
|
|
1764
|
+
mode: z5.enum(["dry_run", "applied"]),
|
|
1765
|
+
command: z5.string(),
|
|
1766
|
+
stdout: z5.string().optional(),
|
|
1767
|
+
stderr: z5.string().optional(),
|
|
1768
|
+
exit_code: z5.number().optional(),
|
|
1769
|
+
replacement_count: z5.number().optional(),
|
|
1770
|
+
confirmation_token: z5.string().optional(),
|
|
1771
|
+
expires_in_seconds: z5.number().optional()
|
|
1772
|
+
}),
|
|
1773
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1774
|
+
async handler(input) {
|
|
1775
|
+
const includeCols = input.include_columns ?? (input.table === "wp_postmeta" ? "meta_value" : "");
|
|
1776
|
+
const baseArgs = [
|
|
1777
|
+
"search-replace",
|
|
1778
|
+
`'${input.find.replace(/'/g, "'\\''")}'`,
|
|
1779
|
+
`'${input.replace.replace(/'/g, "'\\''")}'`,
|
|
1780
|
+
input.table,
|
|
1781
|
+
includeCols ? `--include-columns=${includeCols}` : "",
|
|
1782
|
+
input.precise ? "--precise" : ""
|
|
1783
|
+
].filter(Boolean).join(" ");
|
|
1784
|
+
const site = getSite(input.site_id);
|
|
1785
|
+
if (!input.confirmation) {
|
|
1786
|
+
const dry = await sshWpCli(site, baseArgs + " --dry-run");
|
|
1787
|
+
const m2 = dry.stdout.match(/Success: (\d+) replacement/);
|
|
1788
|
+
const count = m2 ? parseInt(m2[1], 10) : 0;
|
|
1789
|
+
if (count === 0) {
|
|
1790
|
+
return { mode: "dry_run", command: dry.command, stdout: dry.stdout, exit_code: dry.exitCode, replacement_count: 0 };
|
|
1791
|
+
}
|
|
1792
|
+
const token = issueConfirmation("wp_search_replace", { args: baseArgs, site_id: input.site_id }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
1793
|
+
return {
|
|
1794
|
+
mode: "dry_run",
|
|
1795
|
+
command: dry.command,
|
|
1796
|
+
stdout: dry.stdout,
|
|
1797
|
+
replacement_count: count,
|
|
1798
|
+
confirmation_token: token,
|
|
1799
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
const conf = consumeConfirmation(input.confirmation, "wp_search_replace");
|
|
1803
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1804
|
+
const o = conf.payload;
|
|
1805
|
+
if (o.args !== baseArgs || o.site_id !== input.site_id) throw new Error("Confirmation params mismatch");
|
|
1806
|
+
const r = await sshWpCli(site, baseArgs);
|
|
1807
|
+
const m = r.stdout.match(/Success: (\d+) replacement/);
|
|
1808
|
+
return {
|
|
1809
|
+
mode: "applied",
|
|
1810
|
+
command: r.command,
|
|
1811
|
+
stdout: r.stdout,
|
|
1812
|
+
stderr: r.stderr,
|
|
1813
|
+
exit_code: r.exitCode,
|
|
1814
|
+
replacement_count: m ? parseInt(m[1], 10) : 0
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
var wpElementorFlushCssTool = defineTool({
|
|
1819
|
+
name: "wp_elementor_flush_css",
|
|
1820
|
+
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.",
|
|
1821
|
+
inputSchema: z5.object({
|
|
1822
|
+
site_id: z5.string().optional(),
|
|
1823
|
+
page_id: z5.number().int().positive().optional().describe("Optional: flush only this page's cache. If omitted, flushes site-wide.")
|
|
1824
|
+
}),
|
|
1825
|
+
outputSchema: z5.object({
|
|
1826
|
+
method: z5.enum(["rest", "wp-cli", "option-delete", "resave", "none"]),
|
|
1827
|
+
details: z5.string().optional()
|
|
1828
|
+
}),
|
|
1829
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1830
|
+
async handler(input) {
|
|
1831
|
+
const { flushCSS: flushCSS2 } = await Promise.resolve().then(() => (init_css_flush(), css_flush_exports));
|
|
1832
|
+
return flushCSS2(input.site_id, input.page_id);
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
var wpPluginListTool = defineTool({
|
|
1836
|
+
name: "wp_plugin_list",
|
|
1837
|
+
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.",
|
|
1838
|
+
inputSchema: z5.object({
|
|
1839
|
+
site_id: z5.string().optional(),
|
|
1840
|
+
only_outdated: z5.boolean().default(false)
|
|
1841
|
+
}),
|
|
1842
|
+
outputSchema: z5.object({
|
|
1843
|
+
total: z5.number(),
|
|
1844
|
+
plugins: z5.array(z5.object({
|
|
1845
|
+
name: z5.string(),
|
|
1846
|
+
status: z5.string(),
|
|
1847
|
+
version: z5.string(),
|
|
1848
|
+
update: z5.string().optional(),
|
|
1849
|
+
update_version: z5.string().optional()
|
|
1850
|
+
}))
|
|
1851
|
+
}),
|
|
1852
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
1853
|
+
async handler(input) {
|
|
1854
|
+
const site = getSite(input.site_id);
|
|
1855
|
+
const r = await sshWpCli(site, "plugin list --format=json");
|
|
1856
|
+
if (r.exitCode !== 0) throw new Error(`wp plugin list failed: ${r.stderr}`);
|
|
1857
|
+
const arr = JSON.parse(r.stdout);
|
|
1858
|
+
const filtered = input.only_outdated ? arr.filter((p) => p.update === "available") : arr;
|
|
1859
|
+
return { total: filtered.length, plugins: filtered };
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
var wpPluginUpdateTool = defineTool({
|
|
1863
|
+
name: "wp_plugin_update",
|
|
1864
|
+
description: "Update one or more plugins on a site to their latest version. Requires confirmation token (uses wp-cli).",
|
|
1865
|
+
inputSchema: z5.object({
|
|
1866
|
+
site_id: z5.string().optional(),
|
|
1867
|
+
plugins: z5.array(z5.string()).min(1).describe("Plugin slugs to update, e.g. ['elementor', 'elementor-pro']. Use 'all' for everything outdated."),
|
|
1868
|
+
confirmation: z5.string().optional()
|
|
1869
|
+
}),
|
|
1870
|
+
outputSchema: z5.object({
|
|
1871
|
+
mode: z5.enum(["dry_run", "applied"]),
|
|
1872
|
+
plugins: z5.array(z5.string()),
|
|
1873
|
+
command: z5.string().optional(),
|
|
1874
|
+
stdout: z5.string().optional(),
|
|
1875
|
+
stderr: z5.string().optional(),
|
|
1876
|
+
exit_code: z5.number().optional(),
|
|
1877
|
+
confirmation_token: z5.string().optional(),
|
|
1878
|
+
expires_in_seconds: z5.number().optional()
|
|
1879
|
+
}),
|
|
1880
|
+
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1881
|
+
async handler(input) {
|
|
1882
|
+
if (!input.confirmation) {
|
|
1883
|
+
const token = issueConfirmation("wp_plugin_update", { site_id: input.site_id, plugins: input.plugins }, POLICIES.CONFIRMATION_TTL_SECONDS);
|
|
1884
|
+
return {
|
|
1885
|
+
mode: "dry_run",
|
|
1886
|
+
plugins: input.plugins,
|
|
1887
|
+
confirmation_token: token,
|
|
1888
|
+
expires_in_seconds: POLICIES.CONFIRMATION_TTL_SECONDS
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
const conf = consumeConfirmation(input.confirmation, "wp_plugin_update");
|
|
1892
|
+
if (!conf) throw new Error("Invalid or expired confirmation token");
|
|
1893
|
+
const site = getSite(input.site_id);
|
|
1894
|
+
const target = input.plugins.includes("all") ? "--all" : input.plugins.map((p) => `'${p}'`).join(" ");
|
|
1895
|
+
const r = await sshWpCli(site, `plugin update ${target}`, { timeout_ms: 18e4 });
|
|
1896
|
+
return {
|
|
1897
|
+
mode: "applied",
|
|
1898
|
+
plugins: input.plugins,
|
|
1899
|
+
command: r.command,
|
|
1900
|
+
stdout: r.stdout,
|
|
1901
|
+
stderr: r.stderr,
|
|
1902
|
+
exit_code: r.exitCode
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
// src/tools/visual.ts
|
|
1908
|
+
import { z as z6 } from "zod";
|
|
1909
|
+
|
|
1910
|
+
// src/transport/screenshot.ts
|
|
1911
|
+
init_logger();
|
|
1912
|
+
import { spawn as spawn2 } from "child_process";
|
|
1913
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1914
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1915
|
+
import { join as join2 } from "path";
|
|
1916
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1917
|
+
var CHROME_PATHS = [
|
|
1918
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1919
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1920
|
+
"/usr/bin/google-chrome",
|
|
1921
|
+
"/usr/bin/chromium",
|
|
1922
|
+
"/usr/bin/chromium-browser"
|
|
1923
|
+
];
|
|
1924
|
+
function findChrome() {
|
|
1925
|
+
if (process.env.ELEMENTOR_MCP_CHROME) {
|
|
1926
|
+
return existsSync2(process.env.ELEMENTOR_MCP_CHROME) ? process.env.ELEMENTOR_MCP_CHROME : null;
|
|
1927
|
+
}
|
|
1928
|
+
for (const p of CHROME_PATHS) {
|
|
1929
|
+
if (existsSync2(p)) return p;
|
|
1930
|
+
}
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
async function screenshotUrl(url, opts = {}) {
|
|
1934
|
+
const chrome = findChrome();
|
|
1935
|
+
if (!chrome) {
|
|
1936
|
+
throw new Error(
|
|
1937
|
+
"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(", ")
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
const filename = `elementor-mcp-${randomBytes2(4).toString("hex")}-${Date.now()}.png`;
|
|
1941
|
+
const path = join2(tmpdir2(), filename);
|
|
1942
|
+
const args = [
|
|
1943
|
+
"--headless=new",
|
|
1944
|
+
"--disable-gpu",
|
|
1945
|
+
"--no-sandbox",
|
|
1946
|
+
"--hide-scrollbars",
|
|
1947
|
+
`--window-size=${opts.width ?? 1440},${opts.height ?? 900}`,
|
|
1948
|
+
`--screenshot=${path}`,
|
|
1949
|
+
...opts.full_page ? ["--virtual-time-budget=10000"] : [],
|
|
1950
|
+
"--default-background-color=00000000",
|
|
1951
|
+
url
|
|
1952
|
+
];
|
|
1953
|
+
const timeout = opts.timeout_ms ?? 6e4;
|
|
1954
|
+
logger.debug({ url, chrome }, "screenshot");
|
|
1955
|
+
await new Promise((resolve3, reject) => {
|
|
1956
|
+
const child = spawn2(chrome, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1957
|
+
let stderr = "";
|
|
1958
|
+
const killer = setTimeout(() => {
|
|
1959
|
+
child.kill("SIGKILL");
|
|
1960
|
+
reject(new Error(`Screenshot timed out after ${timeout}ms`));
|
|
1961
|
+
}, timeout);
|
|
1962
|
+
child.stderr.on("data", (b) => {
|
|
1963
|
+
stderr += b.toString();
|
|
1964
|
+
});
|
|
1965
|
+
child.on("close", (code) => {
|
|
1966
|
+
clearTimeout(killer);
|
|
1967
|
+
if (code !== 0) reject(new Error(`Chrome exited ${code}: ${stderr.slice(0, 300)}`));
|
|
1968
|
+
else resolve3();
|
|
1969
|
+
});
|
|
1970
|
+
child.on("error", (e) => {
|
|
1971
|
+
clearTimeout(killer);
|
|
1972
|
+
reject(e);
|
|
1973
|
+
});
|
|
1974
|
+
});
|
|
1975
|
+
if (!existsSync2(path)) throw new Error(`Screenshot file not created: ${path}`);
|
|
1976
|
+
const fs = await import("fs");
|
|
1977
|
+
const stat = fs.statSync(path);
|
|
1978
|
+
return { path, bytes: stat.size };
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// src/tools/visual.ts
|
|
1982
|
+
init_wp_rest();
|
|
1983
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1984
|
+
import { createHash } from "crypto";
|
|
1985
|
+
var screenshotPageTool = defineTool({
|
|
1986
|
+
name: "screenshot_page",
|
|
1987
|
+
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.",
|
|
1988
|
+
inputSchema: z6.object({
|
|
1989
|
+
site_id: z6.string().optional(),
|
|
1990
|
+
page_id: z6.number().int().positive().optional(),
|
|
1991
|
+
url: z6.string().url().optional().describe("Alternative to page_id: hit any URL directly."),
|
|
1992
|
+
width: z6.number().int().min(320).max(3840).default(1440),
|
|
1993
|
+
height: z6.number().int().min(240).max(2400).default(900),
|
|
1994
|
+
full_page: z6.boolean().default(false)
|
|
1995
|
+
}),
|
|
1996
|
+
outputSchema: z6.object({
|
|
1997
|
+
url: z6.string(),
|
|
1998
|
+
file_path: z6.string(),
|
|
1999
|
+
bytes: z6.number(),
|
|
2000
|
+
sha256: z6.string(),
|
|
2001
|
+
width: z6.number(),
|
|
2002
|
+
height: z6.number()
|
|
2003
|
+
}),
|
|
2004
|
+
annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: true },
|
|
2005
|
+
async handler(input) {
|
|
2006
|
+
let target = input.url;
|
|
2007
|
+
if (!target) {
|
|
2008
|
+
if (!input.page_id) throw new Error("Provide either 'url' or 'page_id'.");
|
|
2009
|
+
const page = await wpRequest(`/wp/v2/pages/${input.page_id}?_fields=link`, { siteId: input.site_id });
|
|
2010
|
+
target = page.link;
|
|
2011
|
+
}
|
|
2012
|
+
const shot = await screenshotUrl(target, { width: input.width, height: input.height, full_page: input.full_page });
|
|
2013
|
+
const hash = createHash("sha256").update(readFileSync3(shot.path)).digest("hex");
|
|
2014
|
+
return {
|
|
2015
|
+
url: target,
|
|
2016
|
+
file_path: shot.path,
|
|
2017
|
+
bytes: shot.bytes,
|
|
2018
|
+
sha256: hash,
|
|
2019
|
+
width: input.width,
|
|
2020
|
+
height: input.height
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
var compareScreenshotsTool = defineTool({
|
|
2025
|
+
name: "compare_screenshots",
|
|
2026
|
+
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.",
|
|
2027
|
+
inputSchema: z6.object({
|
|
2028
|
+
before_path: z6.string(),
|
|
2029
|
+
after_path: z6.string()
|
|
2030
|
+
}),
|
|
2031
|
+
outputSchema: z6.object({
|
|
2032
|
+
identical: z6.boolean(),
|
|
2033
|
+
before_bytes: z6.number(),
|
|
2034
|
+
after_bytes: z6.number(),
|
|
2035
|
+
delta_bytes: z6.number(),
|
|
2036
|
+
before_sha256: z6.string(),
|
|
2037
|
+
after_sha256: z6.string()
|
|
2038
|
+
}),
|
|
2039
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
2040
|
+
async handler(input) {
|
|
2041
|
+
const a = readFileSync3(input.before_path);
|
|
2042
|
+
const b = readFileSync3(input.after_path);
|
|
2043
|
+
const ah = createHash("sha256").update(a).digest("hex");
|
|
2044
|
+
const bh = createHash("sha256").update(b).digest("hex");
|
|
2045
|
+
return {
|
|
2046
|
+
identical: ah === bh,
|
|
2047
|
+
before_bytes: a.length,
|
|
2048
|
+
after_bytes: b.length,
|
|
2049
|
+
delta_bytes: b.length - a.length,
|
|
2050
|
+
before_sha256: ah,
|
|
2051
|
+
after_sha256: bh
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
// src/tools/updates.ts
|
|
2057
|
+
import { z as z7 } from "zod";
|
|
2058
|
+
init_wp_rest();
|
|
2059
|
+
init_config();
|
|
774
2060
|
async function fetchLatestElementor() {
|
|
775
|
-
|
|
776
|
-
|
|
2061
|
+
try {
|
|
2062
|
+
const free = await fetch("https://api.wordpress.org/plugins/info/1.0/elementor.json", {
|
|
2063
|
+
headers: { "User-Agent": "elementor-mcp-agent" }
|
|
2064
|
+
}).then((r) => r.json());
|
|
2065
|
+
return { free: free.version ?? "unknown" };
|
|
2066
|
+
} catch {
|
|
2067
|
+
return { free: "unknown" };
|
|
2068
|
+
}
|
|
777
2069
|
}
|
|
778
2070
|
var checkElementorVersionsTool = defineTool({
|
|
779
2071
|
name: "check_elementor_versions",
|
|
780
|
-
description: "For every
|
|
781
|
-
inputSchema:
|
|
782
|
-
site_ids:
|
|
2072
|
+
description: "Fleet-wide Elementor version audit. For every site, fetches installed Elementor/Pro versions and compares against wordpress.org latest. Flags outdated installs.",
|
|
2073
|
+
inputSchema: z7.object({
|
|
2074
|
+
site_ids: z7.array(z7.string()).optional()
|
|
783
2075
|
}),
|
|
784
|
-
outputSchema:
|
|
785
|
-
checked:
|
|
786
|
-
latest_elementor_free:
|
|
787
|
-
sites:
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
})
|
|
796
|
-
)
|
|
2076
|
+
outputSchema: z7.object({
|
|
2077
|
+
checked: z7.number(),
|
|
2078
|
+
latest_elementor_free: z7.string(),
|
|
2079
|
+
sites: z7.array(z7.object({
|
|
2080
|
+
site_id: z7.string(),
|
|
2081
|
+
url: z7.string(),
|
|
2082
|
+
elementor_version: z7.string().optional(),
|
|
2083
|
+
elementor_pro_version: z7.string().optional(),
|
|
2084
|
+
outdated_free: z7.boolean(),
|
|
2085
|
+
error: z7.string().optional()
|
|
2086
|
+
}))
|
|
797
2087
|
}),
|
|
798
2088
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
799
2089
|
async handler(input) {
|
|
@@ -807,8 +2097,7 @@ var checkElementorVersionsTool = defineTool({
|
|
|
807
2097
|
let elementor_version;
|
|
808
2098
|
let elementor_pro_version;
|
|
809
2099
|
for (const p of plugins) {
|
|
810
|
-
if (p.plugin.startsWith("elementor/") && p.plugin.endsWith("/elementor.php"))
|
|
811
|
-
elementor_version = p.version;
|
|
2100
|
+
if (p.plugin.startsWith("elementor/") && p.plugin.endsWith("/elementor.php")) elementor_version = p.version;
|
|
812
2101
|
if (p.plugin.startsWith("elementor-pro/")) elementor_pro_version = p.version;
|
|
813
2102
|
}
|
|
814
2103
|
rows.push({
|
|
@@ -819,43 +2108,58 @@ var checkElementorVersionsTool = defineTool({
|
|
|
819
2108
|
outdated_free: !!elementor_version && elementor_version !== latest.free && latest.free !== "unknown"
|
|
820
2109
|
});
|
|
821
2110
|
} catch (e) {
|
|
822
|
-
rows.push({
|
|
823
|
-
site_id: site.id,
|
|
824
|
-
url: site.url,
|
|
825
|
-
outdated_free: false,
|
|
826
|
-
error: e.message
|
|
827
|
-
});
|
|
2111
|
+
rows.push({ site_id: site.id, url: site.url, outdated_free: false, error: e.message });
|
|
828
2112
|
}
|
|
829
2113
|
}
|
|
830
|
-
return {
|
|
831
|
-
checked: rows.length,
|
|
832
|
-
latest_elementor_free: latest.free,
|
|
833
|
-
sites: rows
|
|
834
|
-
};
|
|
2114
|
+
return { checked: rows.length, latest_elementor_free: latest.free, sites: rows };
|
|
835
2115
|
}
|
|
836
2116
|
});
|
|
837
2117
|
|
|
838
2118
|
// src/tools/index.ts
|
|
839
2119
|
var tools = [
|
|
2120
|
+
// Sites & health
|
|
840
2121
|
listSitesTool,
|
|
841
2122
|
pingSiteTool,
|
|
2123
|
+
siteHealthTool,
|
|
2124
|
+
// Pages
|
|
842
2125
|
listElementorPagesTool,
|
|
843
2126
|
readPageElementorTool,
|
|
2127
|
+
listWidgetsInPageTool,
|
|
2128
|
+
listGlobalWidgetsTool,
|
|
2129
|
+
preflightCheckTool,
|
|
844
2130
|
findReplaceTool,
|
|
2131
|
+
listElementorBackupsTool,
|
|
2132
|
+
restoreElementorBackupTool,
|
|
2133
|
+
duplicateElementorPageTool,
|
|
2134
|
+
// Templates
|
|
845
2135
|
listTemplatesTool,
|
|
846
2136
|
exportTemplateTool,
|
|
847
2137
|
importTemplateTool,
|
|
2138
|
+
applyTemplateToPageTool,
|
|
2139
|
+
// WP-CLI escape
|
|
2140
|
+
wpCliRunTool,
|
|
2141
|
+
wpSearchReplaceTool,
|
|
2142
|
+
wpElementorFlushCssTool,
|
|
2143
|
+
wpPluginListTool,
|
|
2144
|
+
wpPluginUpdateTool,
|
|
2145
|
+
// Visual
|
|
2146
|
+
screenshotPageTool,
|
|
2147
|
+
compareScreenshotsTool,
|
|
2148
|
+
// Versions
|
|
848
2149
|
checkElementorVersionsTool
|
|
849
2150
|
];
|
|
850
2151
|
|
|
2152
|
+
// src/server.ts
|
|
2153
|
+
init_logger();
|
|
2154
|
+
|
|
851
2155
|
// src/resources/index.ts
|
|
852
|
-
import { readFileSync as
|
|
853
|
-
import { dirname, resolve, join, basename } from "path";
|
|
2156
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3, readdirSync } from "fs";
|
|
2157
|
+
import { dirname, resolve, join as join3, basename } from "path";
|
|
854
2158
|
import { fileURLToPath } from "url";
|
|
855
2159
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
856
2160
|
var DOCS_DIR = resolve(__dirname, "../resources/elementor-docs");
|
|
857
2161
|
async function listResources() {
|
|
858
|
-
if (!
|
|
2162
|
+
if (!existsSync3(DOCS_DIR)) return [];
|
|
859
2163
|
const files = readdirSync(DOCS_DIR).filter((f) => f.endsWith(".md"));
|
|
860
2164
|
return files.map((f) => ({
|
|
861
2165
|
uri: `elementor-docs://${f}`,
|
|
@@ -869,14 +2173,14 @@ async function readResource(uri) {
|
|
|
869
2173
|
throw new Error(`Unknown resource URI: ${uri}`);
|
|
870
2174
|
}
|
|
871
2175
|
const filename = uri.replace("elementor-docs://", "");
|
|
872
|
-
const path =
|
|
873
|
-
if (!
|
|
2176
|
+
const path = join3(DOCS_DIR, filename);
|
|
2177
|
+
if (!existsSync3(path)) throw new Error(`Resource not found: ${filename}`);
|
|
874
2178
|
return {
|
|
875
2179
|
contents: [
|
|
876
2180
|
{
|
|
877
2181
|
uri,
|
|
878
2182
|
mimeType: "text/markdown",
|
|
879
|
-
text:
|
|
2183
|
+
text: readFileSync4(path, "utf8")
|
|
880
2184
|
}
|
|
881
2185
|
]
|
|
882
2186
|
};
|
|
@@ -884,7 +2188,7 @@ async function readResource(uri) {
|
|
|
884
2188
|
|
|
885
2189
|
// src/server.ts
|
|
886
2190
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
887
|
-
var pkg = JSON.parse(
|
|
2191
|
+
var pkg = JSON.parse(readFileSync5(resolve2(__dirname2, "../package.json"), "utf8"));
|
|
888
2192
|
function toMcpInputSchema(zod) {
|
|
889
2193
|
return zodToJsonSchema(zod, { target: "jsonSchema7", $refStrategy: "none" });
|
|
890
2194
|
}
|