elementor-mcp-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js ADDED
@@ -0,0 +1,951 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { readFileSync as readFileSync3 } from "fs";
5
+ import { dirname as dirname2, resolve as resolve2 } from "path";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import {
10
+ CallToolRequestSchema,
11
+ ListToolsRequestSchema,
12
+ ListResourcesRequestSchema,
13
+ ReadResourceRequestSchema
14
+ } from "@modelcontextprotocol/sdk/types.js";
15
+ import { zodToJsonSchema } from "zod-to-json-schema";
16
+
17
+ // src/config.ts
18
+ import { readFileSync } from "fs";
19
+ 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
+ function loadConfig() {
42
+ if (cached) return cached;
43
+ let raw;
44
+ if (process.env.ELEMENTOR_MCP_SITES) {
45
+ try {
46
+ raw = { sites: JSON.parse(process.env.ELEMENTOR_MCP_SITES) };
47
+ } catch (e) {
48
+ throw new Error("ELEMENTOR_MCP_SITES must be valid JSON: " + e.message);
49
+ }
50
+ } else if (process.env.ELEMENTOR_MCP_CONFIG_PATH) {
51
+ try {
52
+ raw = JSON.parse(readFileSync(process.env.ELEMENTOR_MCP_CONFIG_PATH, "utf8"));
53
+ } catch (e) {
54
+ throw new Error("Cannot read ELEMENTOR_MCP_CONFIG_PATH: " + e.message);
55
+ }
56
+ } else {
57
+ throw new Error(
58
+ [
59
+ "No site configuration provided.",
60
+ "",
61
+ "How to set it up:",
62
+ " \u2022 Easiest: set ELEMENTOR_MCP_SITES to a JSON array of sites. Example:",
63
+ ` ELEMENTOR_MCP_SITES='[{"id":"my-site","url":"https://example.com",`,
64
+ ` "username":"admin","application_password":"xxxx xxxx xxxx xxxx xxxx xxxx"}]'`,
65
+ "",
66
+ " \u2022 Or: set ELEMENTOR_MCP_CONFIG_PATH to a JSON file with the same structure.",
67
+ "",
68
+ "Get a WordPress Application Password at:",
69
+ " https://{your-site}/wp-admin/profile.php#application-passwords-section",
70
+ "",
71
+ "Full docs: https://github.com/Mogacode-ma/elementor-mcp-agent#configure"
72
+ ].join("\n")
73
+ );
74
+ }
75
+ const r = raw;
76
+ const merged = {
77
+ sites: r.sites ?? [],
78
+ default_site_id: r.default_site_id ?? process.env.ELEMENTOR_MCP_DEFAULT_SITE_ID,
79
+ rate_limit_per_minute: r.rate_limit_per_minute ?? process.env.ELEMENTOR_MCP_RATE_LIMIT ?? void 0,
80
+ confirmation_ttl_seconds: r.confirmation_ttl_seconds ?? process.env.ELEMENTOR_MCP_CONFIRMATION_TTL ?? void 0,
81
+ log_level: r.log_level ?? process.env.LOG_LEVEL ?? void 0
82
+ };
83
+ const parsed = ConfigSchema.safeParse(merged);
84
+ if (!parsed.success) {
85
+ const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
86
+ throw new Error(
87
+ [
88
+ "Invalid configuration \u2014 required values missing or malformed:",
89
+ issues,
90
+ "",
91
+ "See https://github.com/Mogacode-ma/elementor-mcp-agent#configure"
92
+ ].join("\n")
93
+ );
94
+ }
95
+ cached = parsed.data;
96
+ return cached;
97
+ }
98
+ function getSite(siteId) {
99
+ const cfg = loadConfig();
100
+ if (!siteId) {
101
+ const def = cfg.default_site_id ?? cfg.sites[0]?.id;
102
+ if (!def) throw new Error("No site configured");
103
+ const s2 = cfg.sites.find((x) => x.id === def);
104
+ if (!s2) throw new Error(`Default site '${def}' not found in sites list`);
105
+ return s2;
106
+ }
107
+ const s = cfg.sites.find((x) => x.id === siteId);
108
+ if (!s) {
109
+ const available = cfg.sites.map((x) => x.id).join(", ");
110
+ throw new Error(`Site '${siteId}' not found. Available: ${available}`);
111
+ }
112
+ return s;
113
+ }
114
+
115
+ // src/tools/sites.ts
116
+ import { z as z2 } from "zod";
117
+
118
+ // src/types/tool.ts
119
+ function defineTool(def) {
120
+ return def;
121
+ }
122
+
123
+ // 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
+ function bucketFor(siteId, perMinute) {
156
+ let b = buckets.get(siteId);
157
+ if (!b) {
158
+ b = new TokenBucket(perMinute);
159
+ buckets.set(siteId, b);
160
+ }
161
+ return b;
162
+ }
163
+
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
+ function fromHttp(status, body, fallback = "WordPress API error") {
178
+ const raw = body;
179
+ const code = raw?.code ?? `http_${status}`;
180
+ const msg = raw?.message ?? fallback;
181
+ let kind = "unknown";
182
+ if (status === 401 || status === 403) kind = "auth";
183
+ else if (status === 404) kind = "not_found";
184
+ else if (status === 422 || status === 400) kind = "validation";
185
+ else if (status === 429) kind = "rate_limited";
186
+ else if (status >= 500) kind = "server";
187
+ return new WPError(kind, code, msg, body);
188
+ }
189
+
190
+ // src/utils/logger.ts
191
+ import pino from "pino";
192
+ var logger = pino(
193
+ {
194
+ level: process.env.LOG_LEVEL ?? "info",
195
+ base: { name: "elementor-mcp-agent" }
196
+ },
197
+ // Critical: stdout is reserved for MCP JSON-RPC. All logs MUST go to stderr.
198
+ pino.destination(2)
199
+ );
200
+
201
+ // src/api/wp-rest.ts
202
+ function authHeader(site) {
203
+ const encoded = Buffer.from(`${site.username}:${site.application_password}`).toString("base64");
204
+ return `Basic ${encoded}`;
205
+ }
206
+ async function wpRequest(path, opts = {}) {
207
+ const cfg = loadConfig();
208
+ const site = getSite(opts.siteId);
209
+ await bucketFor(site.id, cfg.rate_limit_per_minute).acquire();
210
+ const url = new URL(path.startsWith("http") ? path : `${site.url.replace(/\/$/, "")}/wp-json${path.startsWith("/") ? "" : "/"}${path}`);
211
+ if (opts.query) {
212
+ for (const [k, v] of Object.entries(opts.query)) {
213
+ if (v !== void 0) url.searchParams.set(k, String(v));
214
+ }
215
+ }
216
+ const method = opts.method ?? (opts.body ? "POST" : "GET");
217
+ const headers = {
218
+ Authorization: authHeader(site),
219
+ Accept: "application/json",
220
+ "User-Agent": "elementor-mcp-agent",
221
+ ...opts.headers ?? {}
222
+ };
223
+ if (opts.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
224
+ logger.debug({ method, url: url.toString(), site_id: site.id }, "wp request");
225
+ let res;
226
+ try {
227
+ res = await fetch(url.toString(), {
228
+ method,
229
+ headers,
230
+ body: opts.body ? JSON.stringify(opts.body) : void 0
231
+ });
232
+ } catch (e) {
233
+ throw new WPError("network", "fetch_failed", `Network error: ${e.message}`, e);
234
+ }
235
+ const text = await res.text();
236
+ let parsed = text;
237
+ if (text && (res.headers.get("content-type") ?? "").includes("application/json")) {
238
+ try {
239
+ parsed = JSON.parse(text);
240
+ } catch {
241
+ }
242
+ }
243
+ if (!res.ok) {
244
+ throw fromHttp(res.status, parsed);
245
+ }
246
+ return parsed;
247
+ }
248
+
249
+ // src/tools/sites.ts
250
+ var listSitesTool = defineTool({
251
+ name: "list_sites",
252
+ description: "List every WordPress site configured in this MCP server's pool. Best called first in a session to discover available sites.",
253
+ inputSchema: z2.object({}),
254
+ outputSchema: z2.object({
255
+ total: z2.number(),
256
+ default_site_id: z2.string().optional(),
257
+ sites: z2.array(
258
+ z2.object({
259
+ id: z2.string(),
260
+ url: z2.string(),
261
+ username: z2.string(),
262
+ has_ssh: z2.boolean()
263
+ })
264
+ )
265
+ }),
266
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
267
+ async handler() {
268
+ const cfg = loadConfig();
269
+ return {
270
+ total: cfg.sites.length,
271
+ 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
+ }))
278
+ };
279
+ }
280
+ });
281
+ var pingSiteTool = defineTool({
282
+ name: "ping_site",
283
+ description: "Verify connectivity + authentication to a WordPress site. Calls /wp-json/wp/v2/users/me to validate credentials and returns the WP version + Elementor version if detected.",
284
+ inputSchema: z2.object({
285
+ site_id: z2.string().optional().describe("Site id from list_sites. Defaults to the default site.")
286
+ }),
287
+ outputSchema: z2.object({
288
+ ok: z2.boolean(),
289
+ site_id: z2.string(),
290
+ url: z2.string(),
291
+ wp_version: z2.string().optional(),
292
+ elementor_version: z2.string().optional(),
293
+ elementor_pro_version: z2.string().optional(),
294
+ user: z2.object({ id: z2.number(), name: z2.string(), roles: z2.array(z2.string()).optional() }).optional(),
295
+ error: z2.string().optional()
296
+ }),
297
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
298
+ async handler(input) {
299
+ const site = getSite(input.site_id);
300
+ try {
301
+ const me = await wpRequest(
302
+ "/wp/v2/users/me?context=edit",
303
+ { siteId: site.id }
304
+ );
305
+ let wp_version;
306
+ try {
307
+ const health = await wpRequest(
308
+ "/wp-site-health/v1/info",
309
+ { siteId: site.id }
310
+ );
311
+ wp_version = health.wordpress?.version;
312
+ } catch {
313
+ }
314
+ let elementor_version;
315
+ let elementor_pro_version;
316
+ try {
317
+ const plugins = await wpRequest(
318
+ "/wp/v2/plugins",
319
+ { siteId: site.id }
320
+ );
321
+ for (const p of plugins) {
322
+ if (p.plugin.startsWith("elementor/") && p.plugin.endsWith("/elementor.php"))
323
+ elementor_version = p.version;
324
+ if (p.plugin.startsWith("elementor-pro/")) elementor_pro_version = p.version;
325
+ }
326
+ } catch {
327
+ }
328
+ return {
329
+ ok: true,
330
+ site_id: site.id,
331
+ url: site.url,
332
+ wp_version,
333
+ elementor_version,
334
+ elementor_pro_version,
335
+ user: { id: me.id, name: me.name, roles: me.roles }
336
+ };
337
+ } catch (e) {
338
+ return {
339
+ ok: false,
340
+ site_id: site.id,
341
+ url: site.url,
342
+ error: e.message
343
+ };
344
+ }
345
+ }
346
+ });
347
+
348
+ // src/tools/pages.ts
349
+ import { z as z3 } from "zod";
350
+
351
+ // src/elementor/data-parser.ts
352
+ function parseElementorData(raw) {
353
+ if (Array.isArray(raw)) return raw;
354
+ if (!raw || raw === "[]") return [];
355
+ try {
356
+ const decoded = JSON.parse(raw);
357
+ if (!Array.isArray(decoded)) throw new Error("not an array");
358
+ return decoded;
359
+ } catch (e) {
360
+ throw new Error(`Failed to parse _elementor_data JSON: ${e.message}`);
361
+ }
362
+ }
363
+ function serializeElementorData(data) {
364
+ return JSON.stringify(data);
365
+ }
366
+ function* walkElements(data, path = [], depth = 0) {
367
+ for (const el of data) {
368
+ const here = [...path, el.id];
369
+ yield { element: el, path: here, depth };
370
+ if (el.elements && el.elements.length > 0) {
371
+ yield* walkElements(el.elements, here, depth + 1);
372
+ }
373
+ }
374
+ }
375
+ function findReplaceInWidgets(data, find, replace, options = {}) {
376
+ let count = 0;
377
+ const flags = options.caseSensitive ? "g" : "gi";
378
+ const pattern = new RegExp(find.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
379
+ function replaceInValue(value) {
380
+ if (typeof value === "string") {
381
+ const next = value.replace(pattern, () => {
382
+ count++;
383
+ return replace;
384
+ });
385
+ return next;
386
+ }
387
+ if (Array.isArray(value)) return value.map(replaceInValue);
388
+ if (value && typeof value === "object") {
389
+ const out = {};
390
+ for (const [k, v] of Object.entries(value)) out[k] = replaceInValue(v);
391
+ return out;
392
+ }
393
+ return value;
394
+ }
395
+ for (const { element } of walkElements(data)) {
396
+ if (element.elType !== "widget") continue;
397
+ if (options.widgetType && element.widgetType !== options.widgetType) continue;
398
+ element.settings = replaceInValue(element.settings);
399
+ }
400
+ return { data, replacementCount: count };
401
+ }
402
+ function summarize(data) {
403
+ let totalElements = 0;
404
+ let sections = 0;
405
+ let containers = 0;
406
+ let columns = 0;
407
+ let widgets = 0;
408
+ let maxDepth = 0;
409
+ const byWidgetType = {};
410
+ for (const { element, depth } of walkElements(data)) {
411
+ totalElements++;
412
+ maxDepth = Math.max(maxDepth, depth);
413
+ if (element.elType === "section") sections++;
414
+ else if (element.elType === "container") containers++;
415
+ else if (element.elType === "column") columns++;
416
+ else if (element.elType === "widget") {
417
+ widgets++;
418
+ const w = element.widgetType ?? "unknown";
419
+ byWidgetType[w] = (byWidgetType[w] ?? 0) + 1;
420
+ }
421
+ }
422
+ return { totalElements, sections, containers, columns, widgets, byWidgetType, maxDepth };
423
+ }
424
+
425
+ // src/elementor/safety.ts
426
+ async function backupElementorData(siteId, postId) {
427
+ const current = await wpRequest(`/wp/v2/pages/${postId}?context=edit&_fields=meta`, { siteId });
428
+ const raw = current.meta?._elementor_data ?? "[]";
429
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
430
+ const meta_key = `_elementor_data_backup_${ts}`;
431
+ await wpRequest(`/wp/v2/pages/${postId}`, {
432
+ siteId,
433
+ method: "PUT",
434
+ body: { meta: { [meta_key]: raw } }
435
+ });
436
+ return { meta_key, size_bytes: raw.length };
437
+ }
438
+ async function flushElementorCSS(siteId, postId) {
439
+ try {
440
+ await wpRequest(`/elementor/v1/css?id=${postId}&action=regenerate`, { siteId, method: "POST" });
441
+ return { method: "rest" };
442
+ } 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
+ }
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;
472
+ }
473
+
474
+ // src/tools/pages.ts
475
+ var listElementorPagesTool = defineTool({
476
+ name: "list_elementor_pages",
477
+ description: "List pages on a site that are built with Elementor (i.e. have _elementor_edit_mode = 'builder'). Returns id, title, slug, status, modified date.",
478
+ inputSchema: z3.object({
479
+ site_id: z3.string().optional(),
480
+ per_page: z3.number().int().min(1).max(100).default(25),
481
+ search: z3.string().optional()
482
+ }),
483
+ outputSchema: z3.object({
484
+ total: z3.number(),
485
+ pages: z3.array(
486
+ z3.object({
487
+ id: z3.number(),
488
+ title: z3.string(),
489
+ slug: z3.string(),
490
+ status: z3.string(),
491
+ link: z3.string(),
492
+ modified: z3.string()
493
+ })
494
+ )
495
+ }),
496
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
497
+ async handler(input) {
498
+ const pages = await wpRequest("/wp/v2/pages", {
499
+ siteId: input.site_id,
500
+ query: {
501
+ per_page: input.per_page,
502
+ search: input.search,
503
+ meta_key: "_elementor_edit_mode",
504
+ meta_value: "builder",
505
+ context: "edit",
506
+ _fields: "id,title,slug,status,link,modified"
507
+ }
508
+ });
509
+ return {
510
+ total: pages.length,
511
+ pages: pages.map((p) => ({
512
+ id: p.id,
513
+ title: p.title.rendered,
514
+ slug: p.slug,
515
+ status: p.status,
516
+ link: p.link,
517
+ modified: p.modified
518
+ }))
519
+ };
520
+ }
521
+ });
522
+ var readPageElementorTool = defineTool({
523
+ name: "read_page_elementor",
524
+ description: "Fetch the raw _elementor_data of a page and return a structured summary (counts by widget type, depth, total elements). Optionally returns the full parsed tree (verbose=true) which may be very large.",
525
+ inputSchema: z3.object({
526
+ site_id: z3.string().optional(),
527
+ page_id: z3.number().int().positive(),
528
+ verbose: z3.boolean().default(false).describe("If true, return the entire parsed Elementor data tree (can be MBs).")
529
+ }),
530
+ outputSchema: z3.object({
531
+ page_id: z3.number(),
532
+ title: z3.string(),
533
+ summary: z3.object({
534
+ totalElements: z3.number(),
535
+ sections: z3.number(),
536
+ containers: z3.number(),
537
+ columns: z3.number(),
538
+ widgets: z3.number(),
539
+ maxDepth: z3.number(),
540
+ byWidgetType: z3.record(z3.number())
541
+ }),
542
+ data: z3.array(z3.any()).optional()
543
+ }),
544
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
545
+ async handler(input) {
546
+ const page = await wpRequest(
547
+ `/wp/v2/pages/${input.page_id}?context=edit`,
548
+ { siteId: input.site_id }
549
+ );
550
+ const raw = page.meta?._elementor_data ?? "[]";
551
+ const data = parseElementorData(raw);
552
+ const summary = summarize(data);
553
+ return {
554
+ page_id: page.id,
555
+ title: page.title.rendered,
556
+ summary,
557
+ data: input.verbose ? data : void 0
558
+ };
559
+ }
560
+ });
561
+ var findReplaceTool = defineTool({
562
+ name: "elementor_find_replace",
563
+ description: "Find/replace plain text in every widget's settings on a single page. TWO-CALL DESTRUCTIVE FLOW: first call without `confirmation` performs a dry-run and returns a confirmation token + match count. Second call with the token actually applies the change after backing up the page's elementor data.",
564
+ inputSchema: z3.object({
565
+ site_id: z3.string().optional(),
566
+ page_id: z3.number().int().positive(),
567
+ find: z3.string().min(1),
568
+ replace: z3.string(),
569
+ widget_type: z3.string().optional().describe("Restrict to one widget type, e.g. 'heading'."),
570
+ case_sensitive: z3.boolean().default(false),
571
+ confirmation: z3.string().optional().describe("Token returned from the dry-run call.")
572
+ }),
573
+ outputSchema: z3.object({
574
+ mode: z3.enum(["dry_run", "applied"]),
575
+ page_id: z3.number(),
576
+ match_count: z3.number(),
577
+ confirmation_token: z3.string().optional(),
578
+ expires_in_seconds: z3.number().optional(),
579
+ backup_meta_key: z3.string().optional(),
580
+ css_flush: z3.string().optional()
581
+ }),
582
+ annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
583
+ async handler(input) {
584
+ const cfg = loadConfig();
585
+ const page = await wpRequest(
586
+ `/wp/v2/pages/${input.page_id}?context=edit`,
587
+ { siteId: input.site_id }
588
+ );
589
+ const raw = page.meta?._elementor_data ?? "[]";
590
+ const data = parseElementorData(raw);
591
+ const dry = findReplaceInWidgets(JSON.parse(JSON.stringify(data)), input.find, input.replace, {
592
+ widgetType: input.widget_type,
593
+ caseSensitive: input.case_sensitive
594
+ });
595
+ if (!input.confirmation) {
596
+ if (dry.replacementCount === 0) {
597
+ return { mode: "dry_run", page_id: input.page_id, match_count: 0 };
598
+ }
599
+ const token = issueConfirmation(
600
+ "elementor_find_replace",
601
+ { page_id: input.page_id, find: input.find, replace: input.replace },
602
+ cfg.confirmation_ttl_seconds
603
+ );
604
+ return {
605
+ mode: "dry_run",
606
+ page_id: input.page_id,
607
+ match_count: dry.replacementCount,
608
+ confirmation_token: token,
609
+ expires_in_seconds: cfg.confirmation_ttl_seconds
610
+ };
611
+ }
612
+ const conf = consumeConfirmation(input.confirmation, "elementor_find_replace");
613
+ if (!conf) throw new Error("Invalid or expired confirmation token");
614
+ const original = conf.payload;
615
+ if (original.page_id !== input.page_id || original.find !== input.find || original.replace !== input.replace) {
616
+ throw new Error("Confirmation parameters don't match the original dry-run");
617
+ }
618
+ const backup = await backupElementorData(input.site_id, input.page_id);
619
+ const applied = findReplaceInWidgets(parseElementorData(raw), input.find, input.replace, {
620
+ widgetType: input.widget_type,
621
+ caseSensitive: input.case_sensitive
622
+ });
623
+ await wpRequest(`/wp/v2/pages/${input.page_id}`, {
624
+ siteId: input.site_id,
625
+ method: "PUT",
626
+ body: { meta: { _elementor_data: serializeElementorData(applied.data) } }
627
+ });
628
+ const flush = await flushElementorCSS(input.site_id, input.page_id);
629
+ return {
630
+ mode: "applied",
631
+ page_id: input.page_id,
632
+ match_count: applied.replacementCount,
633
+ backup_meta_key: backup.meta_key,
634
+ css_flush: flush.method
635
+ };
636
+ }
637
+ });
638
+
639
+ // src/tools/templates.ts
640
+ import { z as z4 } from "zod";
641
+ var listTemplatesTool = defineTool({
642
+ name: "list_elementor_templates",
643
+ description: "List Elementor library templates on a site (saved sections, pages, popups). Type can be filtered: 'section', 'page', 'popup', 'header', 'footer'.",
644
+ inputSchema: z4.object({
645
+ site_id: z4.string().optional(),
646
+ type: z4.enum(["section", "page", "popup", "header", "footer", "any"]).default("any"),
647
+ per_page: z4.number().int().min(1).max(100).default(50)
648
+ }),
649
+ outputSchema: z4.object({
650
+ total: z4.number(),
651
+ templates: z4.array(
652
+ z4.object({
653
+ id: z4.number(),
654
+ title: z4.string(),
655
+ type: z4.string(),
656
+ modified: z4.string()
657
+ })
658
+ )
659
+ }),
660
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
661
+ async handler(input) {
662
+ const query = {
663
+ per_page: input.per_page,
664
+ context: "edit",
665
+ _fields: "id,title,modified,meta"
666
+ };
667
+ if (input.type !== "any") {
668
+ query.meta_key = "_elementor_template_type";
669
+ query.meta_value = input.type;
670
+ }
671
+ const items = await wpRequest("/wp/v2/elementor_library", {
672
+ siteId: input.site_id,
673
+ query
674
+ });
675
+ return {
676
+ total: items.length,
677
+ templates: items.map((t) => ({
678
+ id: t.id,
679
+ title: t.title.rendered,
680
+ type: t.meta?._elementor_template_type ?? "unknown",
681
+ modified: t.modified
682
+ }))
683
+ };
684
+ }
685
+ });
686
+ var exportTemplateTool = defineTool({
687
+ name: "export_elementor_template",
688
+ description: "Export an Elementor template as a portable JSON object. Output is the same structure Elementor expects on import. Use it to copy sections between sites.",
689
+ inputSchema: z4.object({
690
+ site_id: z4.string().optional(),
691
+ template_id: z4.number().int().positive()
692
+ }),
693
+ outputSchema: z4.object({
694
+ template_id: z4.number(),
695
+ title: z4.string(),
696
+ type: z4.string(),
697
+ summary: z4.object({
698
+ totalElements: z4.number(),
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.")
703
+ }),
704
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
705
+ async handler(input) {
706
+ const tpl = await wpRequest(`/wp/v2/elementor_library/${input.template_id}?context=edit`, {
707
+ siteId: input.site_id
708
+ });
709
+ const data = parseElementorData(tpl.meta?._elementor_data ?? "[]");
710
+ const sum = summarize(data);
711
+ const portable = {
712
+ version: "0.4",
713
+ title: tpl.title.rendered,
714
+ type: tpl.meta?._elementor_template_type ?? "page",
715
+ content: data
716
+ };
717
+ return {
718
+ template_id: tpl.id,
719
+ title: tpl.title.rendered,
720
+ type: tpl.meta?._elementor_template_type ?? "unknown",
721
+ summary: { totalElements: sum.totalElements, widgets: sum.widgets, sections: sum.sections },
722
+ portable_json: JSON.stringify(portable)
723
+ };
724
+ }
725
+ });
726
+ var importTemplateTool = defineTool({
727
+ name: "import_elementor_template",
728
+ description: "Import an Elementor template (output of export_elementor_template) into a target site as a new template entry. Useful for syncing reusable sections across an agency's site fleet.",
729
+ inputSchema: z4.object({
730
+ site_id: z4.string().optional().describe("Target site id."),
731
+ portable_json: z4.string().describe("JSON-stringified payload from export_elementor_template."),
732
+ override_title: z4.string().optional()
733
+ }),
734
+ outputSchema: z4.object({
735
+ new_template_id: z4.number(),
736
+ title: z4.string(),
737
+ type: z4.string(),
738
+ url: z4.string()
739
+ }),
740
+ annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
741
+ async handler(input) {
742
+ let payload;
743
+ try {
744
+ payload = JSON.parse(input.portable_json);
745
+ } catch (e) {
746
+ throw new Error("portable_json is not valid JSON: " + e.message);
747
+ }
748
+ const title = input.override_title ?? payload.title;
749
+ const data = Array.isArray(payload.content) ? payload.content : parseElementorData(payload.content);
750
+ const res = await wpRequest(`/wp/v2/elementor_library`, {
751
+ siteId: input.site_id,
752
+ method: "POST",
753
+ body: {
754
+ title,
755
+ status: "publish",
756
+ meta: {
757
+ _elementor_template_type: payload.type,
758
+ _elementor_data: serializeElementorData(data),
759
+ _elementor_edit_mode: "builder"
760
+ }
761
+ }
762
+ });
763
+ return {
764
+ new_template_id: res.id,
765
+ title,
766
+ type: payload.type,
767
+ url: res.link
768
+ };
769
+ }
770
+ });
771
+
772
+ // src/tools/updates.ts
773
+ import { z as z5 } from "zod";
774
+ async function fetchLatestElementor() {
775
+ const free = await fetch("https://api.wordpress.org/plugins/info/1.0/elementor.json").then((r) => r.json()).catch(() => null);
776
+ return { free: free?.version ?? "unknown" };
777
+ }
778
+ var checkElementorVersionsTool = defineTool({
779
+ name: "check_elementor_versions",
780
+ description: "For every configured site, fetch the installed Elementor / Elementor Pro version and compare against the latest available on wordpress.org. Returns a per-site row with 'outdated' flag.",
781
+ inputSchema: z5.object({
782
+ site_ids: z5.array(z5.string()).optional().describe("Subset of sites to check. Defaults to all.")
783
+ }),
784
+ outputSchema: z5.object({
785
+ checked: z5.number(),
786
+ latest_elementor_free: z5.string(),
787
+ sites: z5.array(
788
+ z5.object({
789
+ site_id: z5.string(),
790
+ url: z5.string(),
791
+ elementor_version: z5.string().optional(),
792
+ elementor_pro_version: z5.string().optional(),
793
+ outdated_free: z5.boolean(),
794
+ error: z5.string().optional()
795
+ })
796
+ )
797
+ }),
798
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
799
+ async handler(input) {
800
+ const cfg = loadConfig();
801
+ const latest = await fetchLatestElementor();
802
+ const targets = input.site_ids ? cfg.sites.filter((s) => input.site_ids?.includes(s.id)) : cfg.sites;
803
+ const rows = [];
804
+ for (const site of targets) {
805
+ try {
806
+ const plugins = await wpRequest("/wp/v2/plugins", { siteId: site.id });
807
+ let elementor_version;
808
+ let elementor_pro_version;
809
+ for (const p of plugins) {
810
+ if (p.plugin.startsWith("elementor/") && p.plugin.endsWith("/elementor.php"))
811
+ elementor_version = p.version;
812
+ if (p.plugin.startsWith("elementor-pro/")) elementor_pro_version = p.version;
813
+ }
814
+ rows.push({
815
+ site_id: site.id,
816
+ url: site.url,
817
+ elementor_version,
818
+ elementor_pro_version,
819
+ outdated_free: !!elementor_version && elementor_version !== latest.free && latest.free !== "unknown"
820
+ });
821
+ } catch (e) {
822
+ rows.push({
823
+ site_id: site.id,
824
+ url: site.url,
825
+ outdated_free: false,
826
+ error: e.message
827
+ });
828
+ }
829
+ }
830
+ return {
831
+ checked: rows.length,
832
+ latest_elementor_free: latest.free,
833
+ sites: rows
834
+ };
835
+ }
836
+ });
837
+
838
+ // src/tools/index.ts
839
+ var tools = [
840
+ listSitesTool,
841
+ pingSiteTool,
842
+ listElementorPagesTool,
843
+ readPageElementorTool,
844
+ findReplaceTool,
845
+ listTemplatesTool,
846
+ exportTemplateTool,
847
+ importTemplateTool,
848
+ checkElementorVersionsTool
849
+ ];
850
+
851
+ // src/resources/index.ts
852
+ import { readFileSync as readFileSync2, existsSync, readdirSync } from "fs";
853
+ import { dirname, resolve, join, basename } from "path";
854
+ import { fileURLToPath } from "url";
855
+ var __dirname = dirname(fileURLToPath(import.meta.url));
856
+ var DOCS_DIR = resolve(__dirname, "../resources/elementor-docs");
857
+ async function listResources() {
858
+ if (!existsSync(DOCS_DIR)) return [];
859
+ const files = readdirSync(DOCS_DIR).filter((f) => f.endsWith(".md"));
860
+ return files.map((f) => ({
861
+ uri: `elementor-docs://${f}`,
862
+ name: basename(f, ".md"),
863
+ description: `Elementor documentation snippet (scraped from developer.elementor.com)`,
864
+ mimeType: "text/markdown"
865
+ }));
866
+ }
867
+ async function readResource(uri) {
868
+ if (!uri.startsWith("elementor-docs://")) {
869
+ throw new Error(`Unknown resource URI: ${uri}`);
870
+ }
871
+ const filename = uri.replace("elementor-docs://", "");
872
+ const path = join(DOCS_DIR, filename);
873
+ if (!existsSync(path)) throw new Error(`Resource not found: ${filename}`);
874
+ return {
875
+ contents: [
876
+ {
877
+ uri,
878
+ mimeType: "text/markdown",
879
+ text: readFileSync2(path, "utf8")
880
+ }
881
+ ]
882
+ };
883
+ }
884
+
885
+ // src/server.ts
886
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
887
+ var pkg = JSON.parse(readFileSync3(resolve2(__dirname2, "../package.json"), "utf8"));
888
+ function toMcpInputSchema(zod) {
889
+ return zodToJsonSchema(zod, { target: "jsonSchema7", $refStrategy: "none" });
890
+ }
891
+ async function main() {
892
+ try {
893
+ loadConfig();
894
+ } catch (e) {
895
+ logger.error(e.message);
896
+ process.stderr.write("\n" + e.message + "\n");
897
+ process.exit(1);
898
+ }
899
+ const server = new Server(
900
+ { name: pkg.name, version: pkg.version },
901
+ { capabilities: { tools: {}, resources: {} } }
902
+ );
903
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
904
+ tools: tools.map((t) => ({
905
+ name: t.name,
906
+ description: t.description,
907
+ inputSchema: toMcpInputSchema(t.inputSchema),
908
+ outputSchema: t.outputSchema ? toMcpInputSchema(t.outputSchema) : void 0,
909
+ annotations: t.annotations
910
+ }))
911
+ }));
912
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
913
+ const tool = tools.find((t) => t.name === req.params.name);
914
+ if (!tool) throw new Error(`Unknown tool: ${req.params.name}`);
915
+ const args = req.params.arguments ?? {};
916
+ const parsed = tool.inputSchema.safeParse(args);
917
+ if (!parsed.success) {
918
+ const msg = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
919
+ return {
920
+ content: [{ type: "text", text: `Invalid arguments for ${tool.name}:
921
+ ${msg}` }],
922
+ isError: true
923
+ };
924
+ }
925
+ try {
926
+ const result = await tool.handler(parsed.data);
927
+ return {
928
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
929
+ };
930
+ } catch (e) {
931
+ logger.error({ tool: tool.name, err: e.message }, "tool error");
932
+ return {
933
+ content: [{ type: "text", text: `${tool.name} failed: ${e.message}` }],
934
+ isError: true
935
+ };
936
+ }
937
+ });
938
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
939
+ resources: await listResources()
940
+ }));
941
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => readResource(req.params.uri));
942
+ const transport = new StdioServerTransport();
943
+ await server.connect(transport);
944
+ logger.info({ tools: tools.length }, `${pkg.name} v${pkg.version} ready`);
945
+ }
946
+ main().catch((e) => {
947
+ logger.error(e.message);
948
+ process.stderr.write("\n" + e.message + "\n");
949
+ process.exit(1);
950
+ });
951
+ //# sourceMappingURL=server.js.map