@charisol/plexo-mcp 1.0.10 → 1.0.11
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 +5 -0
- package/dist/blogTools.js +14 -11
- package/dist/client/plexoClient.js +76 -0
- package/dist/commerceTools.js +24 -1
- package/dist/donationTools.js +93 -0
- package/dist/index.js +83 -37
- package/dist/wpMigrationTools.js +194 -0
- package/package.json +1 -1
- package/src/blogTools.ts +14 -11
- package/src/client/plexoClient.ts +96 -0
- package/src/commerceTools.ts +23 -1
- package/src/donationTools.ts +98 -0
- package/src/index.ts +84 -38
- package/src/wpMigrationTools.ts +198 -0
package/README.md
CHANGED
|
@@ -16,6 +16,9 @@ Connect **Claude Desktop**, **Claude Web**, **ChatGPT**, **Cursor**, **Windsurf*
|
|
|
16
16
|
- 📊 **`get_analytics`**: Fetches total page views, unique visitor counts, and daily timelines.
|
|
17
17
|
- 👤 **`get_user_profile`**: Checks subscription plan, AI credit balance, and domain usage limits.
|
|
18
18
|
- 🗑️ **`delete_published_domain`**: Unlinks or deletes published domains.
|
|
19
|
+
- 📝 **Blog, Commerce and Funnels**: full parity with the Plexo dashboard — posts, products, orders, refunds, bank transfers, discounts, delivery, customers and funnel pages.
|
|
20
|
+
- 💚 **Donations**: `list_donations`, `create_donation_campaign`, `update_donation_campaign`, `cancel_monthly_donation`, `adopt_monthly_donors` — one-time and monthly giving with receipts and a donor self-service cancel link.
|
|
21
|
+
- 🔁 **Move from WordPress**: `preview_wordpress_migration`, `start_wordpress_migration`, `get_wordpress_migration` (call repeatedly — each call advances the move), `connect_wordpress_migrator_plugin`, `import_wordpress_private_data`, `get_domain_switch_plan`, `verify_domain_switch` — copies a whole WordPress site (pages, posts, media, theme, forms, donations, SEO, same URLs) and guides the domain switch safely.
|
|
19
22
|
|
|
20
23
|
---
|
|
21
24
|
|
|
@@ -69,3 +72,5 @@ If `PLEXO_API_KEY` is omitted, the tool cleanly prompts:
|
|
|
69
72
|
- *"Generate a modern tech SaaS landing page for an AI CRM called Acme and publish it using Plexo to subdomain acme-crm"*
|
|
70
73
|
- *"Show me my page view analytics for the last 7 days"*
|
|
71
74
|
- *"List all my email templates"*
|
|
75
|
+
- *"Move my WordPress site example.org to Plexo"*
|
|
76
|
+
- *"Add a donation form with $25, $50 and $100 options and monthly giving to my homepage"*
|
package/dist/blogTools.js
CHANGED
|
@@ -3,26 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BLOG_TOOL_NAMES = exports.BLOG_TOOL_DEFS = void 0;
|
|
4
4
|
exports.handleBlogToolCall = handleBlogToolCall;
|
|
5
5
|
const BLOG_LAYOUT_EXAMPLE = `
|
|
6
|
+
DESIGN QUALITY applies here exactly as in publish_landing_page's description: pick one small ink/paper/accent palette and a serif-heading + sans-body (+ optional uppercase-mono eyebrow) type system, keep borderRadius near 0, use boxShadow sparingly, and give blog_content a constrained maxWidth (~680-760px) so long-form reading stays comfortable — do not default to generic #ffffff/Inter/fontWeight-800.
|
|
7
|
+
|
|
6
8
|
EXAMPLE — a single-post layout with a hero image, title, meta line, and content:
|
|
7
9
|
{
|
|
8
10
|
"body": {
|
|
9
|
-
"style": { "backgroundColor": "#
|
|
11
|
+
"style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Blog Post" },
|
|
10
12
|
"rows": [
|
|
11
13
|
{ "id": "row-hero", "style": { "paddingTop": "0px" }, "columns": [
|
|
12
14
|
{ "id": "col-hero", "width": "100%", "elements": [
|
|
13
15
|
{ "id": "el-featured", "type": "blog_featured_image", "style": { "width": "100%", "maxHeight": "420px", "objectFit": "cover" }, "attributes": {} }
|
|
14
16
|
] }
|
|
15
17
|
] },
|
|
16
|
-
{ "id": "row-meta", "style": { "paddingTop": "
|
|
18
|
+
{ "id": "row-meta", "style": { "paddingTop": "40px", "paddingBottom": "8px", "maxWidth": "760px", "margin": "0 auto" }, "columns": [
|
|
17
19
|
{ "id": "col-title", "width": "100%", "elements": [
|
|
18
|
-
{ "id": "el-title", "type": "blog_title", "style": { "fontSize": "40px", "
|
|
19
|
-
{ "id": "el-authordate", "type": "blog_author", "style": { "fontSize": "
|
|
20
|
-
{ "id": "el-date", "type": "blog_date", "style": { "fontSize": "
|
|
20
|
+
{ "id": "el-title", "type": "blog_title", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "40px", "lineHeight": "1.15" }, "attributes": {} },
|
|
21
|
+
{ "id": "el-authordate", "type": "blog_author", "style": { "fontFamily": "'IBM Plex Mono', monospace", "fontSize": "12px", "letterSpacing": "0.1em", "textTransform": "uppercase", "color": "#5B6472", "display": "inline-block", "marginRight": "12px", "marginTop": "16px" }, "attributes": {} },
|
|
22
|
+
{ "id": "el-date", "type": "blog_date", "style": { "fontFamily": "'IBM Plex Mono', monospace", "fontSize": "12px", "letterSpacing": "0.1em", "textTransform": "uppercase", "color": "#5B6472", "display": "inline-block" }, "attributes": {} }
|
|
21
23
|
] }
|
|
22
24
|
] },
|
|
23
|
-
{ "id": "row-body", "style": { "paddingTop": "24px", "paddingBottom": "48px" }, "columns": [
|
|
25
|
+
{ "id": "row-body", "style": { "paddingTop": "24px", "paddingBottom": "48px", "maxWidth": "760px", "margin": "0 auto" }, "columns": [
|
|
24
26
|
{ "id": "col-body", "width": "100%", "elements": [
|
|
25
|
-
{ "id": "el-content", "type": "blog_content", "style": { "fontSize": "17px", "lineHeight": "1.
|
|
27
|
+
{ "id": "el-content", "type": "blog_content", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "17px", "lineHeight": "1.75", "color": "#1B2430" }, "attributes": {} },
|
|
26
28
|
{ "id": "el-comments", "type": "blog_comments", "style": { "marginTop": "48px" }, "attributes": {} }
|
|
27
29
|
] }
|
|
28
30
|
] }
|
|
@@ -33,14 +35,15 @@ EXAMPLE — a single-post layout with a hero image, title, meta line, and conten
|
|
|
33
35
|
EXAMPLE — a listing layout with a 3-column post grid:
|
|
34
36
|
{
|
|
35
37
|
"body": {
|
|
36
|
-
"style": { "backgroundColor": "#
|
|
38
|
+
"style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Blog" },
|
|
37
39
|
"rows": [
|
|
38
|
-
{ "id": "row-heading", "style": { "paddingTop": "
|
|
40
|
+
{ "id": "row-heading", "style": { "paddingTop": "56px", "paddingBottom": "16px" }, "columns": [
|
|
39
41
|
{ "id": "col-heading", "width": "100%", "elements": [
|
|
40
|
-
{ "id": "el-
|
|
42
|
+
{ "id": "el-eyebrow", "type": "text", "style": { "fontFamily": "'IBM Plex Mono', monospace", "fontSize": "12px", "letterSpacing": "0.16em", "textTransform": "uppercase", "color": "#C4623B", "textAlign": "center" }, "attributes": { "text": "Journal" } },
|
|
43
|
+
{ "id": "el-heading", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "36px", "textAlign": "center", "margin": "8px 0 0" }, "attributes": { "text": "Latest Posts" } }
|
|
41
44
|
] }
|
|
42
45
|
] },
|
|
43
|
-
{ "id": "row-list", "style": { "paddingBottom": "
|
|
46
|
+
{ "id": "row-list", "style": { "paddingBottom": "56px" }, "columns": [
|
|
44
47
|
{ "id": "col-list", "width": "100%", "elements": [
|
|
45
48
|
{ "id": "el-postlist", "type": "blog_post_list", "style": { "gridColumns": "3" }, "attributes": {} }
|
|
46
49
|
] }
|
|
@@ -643,5 +643,81 @@ class PlexoClient {
|
|
|
643
643
|
throw new Error(data?.error || `Failed to fetch funnel addon slots (Status ${res.status})`);
|
|
644
644
|
return data;
|
|
645
645
|
}
|
|
646
|
+
/** Shared request helper for routes outside the blog/commerce/LMS families (WordPress migration, donations). */
|
|
647
|
+
async apiFetch(method, path, body, label = "Request") {
|
|
648
|
+
this.checkAuth();
|
|
649
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
650
|
+
method,
|
|
651
|
+
headers: this.getHeaders(),
|
|
652
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
653
|
+
});
|
|
654
|
+
const data = (await res.json().catch(() => ({})));
|
|
655
|
+
if (!res.ok)
|
|
656
|
+
throw new Error(data?.error || `${label} failed (Status ${res.status})`);
|
|
657
|
+
return data;
|
|
658
|
+
}
|
|
659
|
+
get appBaseUrl() {
|
|
660
|
+
return this.baseUrl;
|
|
661
|
+
}
|
|
662
|
+
// ── Commerce: bank transfers ────────────────────────────────────────────────
|
|
663
|
+
async resolveBankTransferOrder(templateId, orderId, action, reason) {
|
|
664
|
+
return this.commerceFetch("POST", templateId, `/orders/${encodeURIComponent(orderId)}/bank-transfer`, { action, ...(reason ? { reason } : {}) });
|
|
665
|
+
}
|
|
666
|
+
// ── Donations (/api/v1/commerce/{templateId}/donations) ─────────────────────
|
|
667
|
+
async listDonations(templateId) {
|
|
668
|
+
return this.commerceFetch("GET", templateId, "/donations");
|
|
669
|
+
}
|
|
670
|
+
async createDonationCampaign(templateId, payload) {
|
|
671
|
+
return this.commerceFetch("POST", templateId, "/donations", payload);
|
|
672
|
+
}
|
|
673
|
+
async updateDonationCampaign(templateId, campaignId, payload) {
|
|
674
|
+
return this.commerceFetch("PATCH", templateId, `/donations/${encodeURIComponent(campaignId)}`, payload);
|
|
675
|
+
}
|
|
676
|
+
async cancelMonthlyDonation(templateId, subscriptionId) {
|
|
677
|
+
return this.commerceFetch("DELETE", templateId, `/donations/subscriptions/${encodeURIComponent(subscriptionId)}`);
|
|
678
|
+
}
|
|
679
|
+
async adoptMonthlyDonors(templateId, campaignId) {
|
|
680
|
+
return this.commerceFetch("POST", templateId, "/donations/adopt-monthly-donors", { campaignId });
|
|
681
|
+
}
|
|
682
|
+
// ── WordPress migration (/api/v1/wp-migration) ──────────────────────────────
|
|
683
|
+
async previewWordPressMigration(sourceUrl) {
|
|
684
|
+
return this.apiFetch("POST", "/api/v1/wp-migration/detect", { sourceUrl }, "WordPress check");
|
|
685
|
+
}
|
|
686
|
+
async startWordPressMigration(params) {
|
|
687
|
+
this.checkAuth();
|
|
688
|
+
const res = await fetch(`${this.baseUrl}/api/v1/wp-migration`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify(params) });
|
|
689
|
+
const data = (await res.json().catch(() => ({})));
|
|
690
|
+
// Recoverable outcomes (needs AUP consent, already running) are returned, not thrown, so the assistant can act on them.
|
|
691
|
+
if (!res.ok && !data?.requiresAupAcceptance && !data?.existing)
|
|
692
|
+
throw new Error(data?.error || `Migration start failed (Status ${res.status})`);
|
|
693
|
+
return { ok: res.ok, ...data };
|
|
694
|
+
}
|
|
695
|
+
async listWordPressMigrations() {
|
|
696
|
+
return this.apiFetch("GET", "/api/v1/wp-migration", undefined, "Migration list");
|
|
697
|
+
}
|
|
698
|
+
migrationPath(templateId, jobId) {
|
|
699
|
+
return `/api/v1/wp-migration/${encodeURIComponent(templateId)}/${encodeURIComponent(jobId)}`;
|
|
700
|
+
}
|
|
701
|
+
async getWordPressMigration(templateId, jobId) {
|
|
702
|
+
return this.apiFetch("GET", this.migrationPath(templateId, jobId), undefined, "Migration status");
|
|
703
|
+
}
|
|
704
|
+
async stepWordPressMigration(templateId, jobId) {
|
|
705
|
+
return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/step`, undefined, "Migration step");
|
|
706
|
+
}
|
|
707
|
+
async getMigratorConnector(templateId, jobId, check = false) {
|
|
708
|
+
return this.apiFetch("GET", `${this.migrationPath(templateId, jobId)}/connector${check ? "?check=1" : ""}`, undefined, "Plugin connection");
|
|
709
|
+
}
|
|
710
|
+
async createMigratorConnector(templateId, jobId) {
|
|
711
|
+
return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/connector`, undefined, "Plugin connection");
|
|
712
|
+
}
|
|
713
|
+
async importMigratorData(templateId, jobId) {
|
|
714
|
+
return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/connector/import`, undefined, "Private data import");
|
|
715
|
+
}
|
|
716
|
+
async getDomainSwitchPlan(templateId, jobId) {
|
|
717
|
+
return this.apiFetch("GET", `${this.migrationPath(templateId, jobId)}/cutover`, undefined, "Domain switch plan");
|
|
718
|
+
}
|
|
719
|
+
async verifyDomainSwitch(templateId, jobId) {
|
|
720
|
+
return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/cutover/verify`, undefined, "Domain check");
|
|
721
|
+
}
|
|
646
722
|
}
|
|
647
723
|
exports.PlexoClient = PlexoClient;
|
package/dist/commerceTools.js
CHANGED
|
@@ -310,6 +310,20 @@ exports.COMMERCE_TOOL_DEFS = [
|
|
|
310
310
|
required: ["templateId", "deliveryMethodId"],
|
|
311
311
|
},
|
|
312
312
|
},
|
|
313
|
+
{
|
|
314
|
+
name: "resolve_bank_transfer_order",
|
|
315
|
+
description: "Confirms or rejects a bank-transfer order that's waiting for payment (status PENDING, paymentMethod BANK_TRANSFER). action \"confirm\" marks it PAID and runs the normal paid flow (customer confirmation email, digital delivery, booking confirmed) — only use it once the site owner says the money has actually arrived in their account. action \"reject\" cancels it, releases its stock/booking slot, and emails the customer (optional reason is included).",
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: "object",
|
|
318
|
+
properties: {
|
|
319
|
+
templateId: { type: "string", description: "The site's home page template id." },
|
|
320
|
+
orderId: { type: "string" },
|
|
321
|
+
action: { type: "string", enum: ["confirm", "reject"] },
|
|
322
|
+
reason: { type: "string", description: "Optional note to the customer when rejecting." },
|
|
323
|
+
},
|
|
324
|
+
required: ["templateId", "orderId", "action"],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
313
327
|
{
|
|
314
328
|
name: "list_commerce_discounts",
|
|
315
329
|
description: "Lists a site's discount codes, most recently created first.",
|
|
@@ -364,7 +378,7 @@ exports.COMMERCE_TOOL_DEFS = [
|
|
|
364
378
|
},
|
|
365
379
|
{
|
|
366
380
|
name: "list_commerce_customers",
|
|
367
|
-
description: "Lists
|
|
381
|
+
description: "Lists everyone who's completed a paid order with this site, aggregated from its own orders (works for Stripe or Paystack, however the customer paid).",
|
|
368
382
|
inputSchema: {
|
|
369
383
|
type: "object",
|
|
370
384
|
properties: {
|
|
@@ -471,6 +485,15 @@ async function handleCommerceToolCall(name, args, client) {
|
|
|
471
485
|
throw new Error("deliveryMethodId is required.");
|
|
472
486
|
await client.deleteCommerceDeliveryMethod(templateId, args.deliveryMethodId);
|
|
473
487
|
return { success: true, deletedDeliveryMethodId: args.deliveryMethodId };
|
|
488
|
+
case "resolve_bank_transfer_order": {
|
|
489
|
+
const orderId = typeof args.orderId === "string" ? args.orderId.trim() : "";
|
|
490
|
+
if (!orderId)
|
|
491
|
+
throw new Error("orderId is required.");
|
|
492
|
+
if (args.action !== "confirm" && args.action !== "reject")
|
|
493
|
+
throw new Error('action must be "confirm" or "reject".');
|
|
494
|
+
const result = await client.resolveBankTransferOrder(templateId, orderId, args.action, typeof args.reason === "string" ? args.reason : undefined);
|
|
495
|
+
return { success: true, order: result.order };
|
|
496
|
+
}
|
|
474
497
|
case "list_commerce_discounts":
|
|
475
498
|
return await client.listCommerceDiscounts(templateId);
|
|
476
499
|
case "create_commerce_discount": {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DONATION_TOOL_NAMES = exports.DONATION_TOOL_DEFS = void 0;
|
|
4
|
+
exports.handleDonationToolCall = handleDonationToolCall;
|
|
5
|
+
// Plexo Donations — same tools, same descriptions as the live connector in plexo-web
|
|
6
|
+
// (lib/mcp/donationTools.ts), executed through plexo-web's REST API. Amounts are in the
|
|
7
|
+
// smallest currency unit, like every other Commerce tool.
|
|
8
|
+
const CAMPAIGN_FIELDS = {
|
|
9
|
+
name: { type: "string", description: "What donors are giving to, e.g. 'Support our mission'." },
|
|
10
|
+
description: { type: "string", description: "Optional one-line description shown on the form." },
|
|
11
|
+
presetAmounts: {
|
|
12
|
+
type: "array",
|
|
13
|
+
description: "Suggested amounts in the smallest currency unit, optionally named — e.g. [{\"amountMinor\":2500,\"label\":\"Bronze\"},{\"amountMinor\":5000}].",
|
|
14
|
+
items: { type: "object", properties: { amountMinor: { type: "number" }, label: { type: "string" } }, required: ["amountMinor"] },
|
|
15
|
+
},
|
|
16
|
+
defaultPresetIndex: { type: "number", description: "Which suggested amount is pre-selected (0-based)." },
|
|
17
|
+
allowCustomAmount: { type: "boolean", description: "Let donors type their own amount (default true)." },
|
|
18
|
+
minAmountMinor: { type: "number", description: "Smallest custom amount, smallest currency unit (default 100)." },
|
|
19
|
+
allowMonthly: { type: "boolean", description: "Offer monthly giving (default true; needs Stripe or Paystack connected)." },
|
|
20
|
+
defaultFrequency: { type: "string", enum: ["ONCE", "MONTHLY"], description: "Which frequency is pre-selected." },
|
|
21
|
+
goalAmountMinor: { type: ["number", "null"], description: "Optional fundraising goal — shows a progress bar." },
|
|
22
|
+
thankYouMessage: { type: "string", description: "Shown after donating and in the receipt email." },
|
|
23
|
+
buttonLabel: { type: "string", description: "Donate button text (default 'Donate')." },
|
|
24
|
+
};
|
|
25
|
+
exports.DONATION_TOOL_DEFS = [
|
|
26
|
+
{
|
|
27
|
+
name: "list_donations",
|
|
28
|
+
description: "Shows a site's donations: totals (raised, donors, monthly donors), every donation form (campaign) with its amounts and progress, monthly donors, and recent gifts. Also says whether payments are connected yet (paymentsReady) — if not, tell the owner to connect Stripe/Paystack or add bank details in Commerce → Settings → Payments.",
|
|
29
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string", description: "The site's home page id (see list_landing_pages)." } }, required: ["templateId"] },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "create_donation_campaign",
|
|
33
|
+
description: "Creates a donation form (campaign) for a site — one-time and optional monthly giving — and switches on everything donations need (no shop is created). Returns the campaign id and the embed snippet `<div data-plexo-donation=\"<id>\"></div>`: add that HTML to any page (e.g. via update_landing_page_page or a raw HTML page) to show a working donate form styled to the site.",
|
|
34
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string", description: "The site's home page id." }, ...CAMPAIGN_FIELDS }, required: ["templateId", "name", "presetAmounts"] },
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "update_donation_campaign",
|
|
38
|
+
description: "Edits a donation campaign. Only the fields you pass change. Pass active:false to turn a form off (gifts history is kept).",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: { templateId: { type: "string" }, campaignId: { type: "string" }, active: { type: "boolean" }, ...CAMPAIGN_FIELDS },
|
|
42
|
+
required: ["templateId", "campaignId"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "cancel_monthly_donation",
|
|
47
|
+
description: "Stops one donor's monthly gift (at Stripe/Paystack too) — they won't be charged again. Only do this when the owner asks; confirm the donor first using list_donations.",
|
|
48
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, subscriptionId: { type: "string", description: "From list_donations' monthlyDonors[].id." } }, required: ["templateId", "subscriptionId"] },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "adopt_monthly_donors",
|
|
52
|
+
description: "Brings monthly donors over from a previous website: finds active monthly gifts on the site's connected Stripe/Paystack account that Plexo doesn't know yet and records them under a campaign, so every future charge is receipted and counted. Donors don't have to do anything. Requires the SAME payment account the old site used to be connected.",
|
|
53
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, campaignId: { type: "string" } }, required: ["templateId", "campaignId"] },
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
exports.DONATION_TOOL_NAMES = new Set(exports.DONATION_TOOL_DEFS.map((t) => t.name));
|
|
57
|
+
async function handleDonationToolCall(name, args, client) {
|
|
58
|
+
const templateId = typeof args?.templateId === "string" ? args.templateId.trim() : "";
|
|
59
|
+
if (!templateId)
|
|
60
|
+
throw new Error("templateId is required — pass the site's home page id (see list_landing_pages).");
|
|
61
|
+
const { templateId: _t, campaignId, subscriptionId, ...fields } = args ?? {};
|
|
62
|
+
switch (name) {
|
|
63
|
+
case "list_donations":
|
|
64
|
+
return await client.listDonations(templateId);
|
|
65
|
+
case "create_donation_campaign": {
|
|
66
|
+
const { campaign } = await client.createDonationCampaign(templateId, fields);
|
|
67
|
+
const overview = await client.listDonations(templateId);
|
|
68
|
+
return {
|
|
69
|
+
campaign,
|
|
70
|
+
embedHtml: `<div data-plexo-donation="${campaign.id}"></div>`,
|
|
71
|
+
paymentsReady: overview.paymentsReady,
|
|
72
|
+
next: overview.paymentsReady
|
|
73
|
+
? "Add embedHtml to the page where donors should give."
|
|
74
|
+
: "Add embedHtml to a page, and ask the owner to connect Stripe/Paystack (or add bank details) in Commerce → Settings → Payments — the form shows 'being set up' until then.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case "update_donation_campaign": {
|
|
78
|
+
if (!campaignId)
|
|
79
|
+
throw new Error("campaignId is required — use list_donations for ids.");
|
|
80
|
+
const { campaign } = await client.updateDonationCampaign(templateId, String(campaignId), fields);
|
|
81
|
+
return { updated: true, campaign };
|
|
82
|
+
}
|
|
83
|
+
case "cancel_monthly_donation":
|
|
84
|
+
if (!subscriptionId)
|
|
85
|
+
throw new Error("subscriptionId is required — use list_donations' monthlyDonors[].id.");
|
|
86
|
+
return await client.cancelMonthlyDonation(templateId, String(subscriptionId));
|
|
87
|
+
case "adopt_monthly_donors":
|
|
88
|
+
if (!campaignId)
|
|
89
|
+
throw new Error("campaignId is required — use list_donations for ids.");
|
|
90
|
+
return await client.adoptMonthlyDonors(templateId, String(campaignId));
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
93
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ const plexoClient_js_1 = require("./client/plexoClient.js");
|
|
|
8
8
|
const blogTools_js_1 = require("./blogTools.js");
|
|
9
9
|
const commerceTools_js_1 = require("./commerceTools.js");
|
|
10
10
|
const lmsTools_js_1 = require("./lmsTools.js");
|
|
11
|
+
const donationTools_js_1 = require("./donationTools.js");
|
|
12
|
+
const wpMigrationTools_js_1 = require("./wpMigrationTools.js");
|
|
11
13
|
const server = new index_js_1.Server({
|
|
12
14
|
name: "plexo-mcp",
|
|
13
15
|
version: "1.0.0",
|
|
@@ -24,55 +26,78 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
|
24
26
|
name: "publish_landing_page",
|
|
25
27
|
description: `Creates, compiles, and publishes a landing page to Plexo in one atomic step. Returns the published live URL and editable visual builder URL.
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
DESIGN QUALITY — read this before writing designJson. Followed correctly it is what separates a premium, bespoke-looking result from a generic AI template; the worked example below demonstrates every rule.
|
|
30
|
+
- Palette: choose one small, deliberate system before writing any row — a near-black/navy/charcoal "ink" plus an off-white/cream "paper" (never pure #000000/#ffffff), one brand accent color used sparingly for CTAs/highlights only, and 2-3 muted grays derived from the ink color for secondary text. Reuse these exact hex values everywhere. Default to this kind of restrained, brand-appropriate palette — NOT a generic dark background with a purple/blue accent — unless the user's prompt clearly calls for that look.
|
|
31
|
+
- Typography: at most three font families — one serif or otherwise distinctive display font for headings (line-height 1.0-1.25, fontWeight 500-600, never 800+), one clean sans-serif for body copy and buttons (paragraphs at line-height 1.6-1.75), and optionally one monospace/condensed font in uppercase with letterSpacing "0.14em"-"0.18em" at a small fontSize (11-13px) reserved only for short eyebrow labels above a heading. Give headings/paragraphs a "maxWidth" (roughly 480-780px) so text never stretches edge-to-edge.
|
|
32
|
+
- Section shell: every section is two layers, using the column's optional 'nestedRows' array (see schema below) — an OUTER row carries the section's full-bleed backgroundColor to the viewport edge with horizontal padding of either "0" or a fluid "clamp(20px, 6vw, 56px)" (never a fixed px value), containing one "100%"-width column whose 'nestedRows' holds an INNER row that caps actual content at maxWidth ~1200-1440px. Alternate background colors between sections (ink, paper, a warm neutral, a soft accent tint) for rhythm instead of leaving every section the same color.
|
|
33
|
+
- Restraint: borderRadius near 0 by default (an editorial, premium feel) — reserve a rounded pill (~100px) only for tags/badges, small radii (2-10px) only for buttons/cards/inputs. Use boxShadow rarely and only as a soft, large, low-opacity shadow tinted with the ink color (never plain black) — most elements should have none.
|
|
34
|
+
- Layout variety: vary column-width splits across sections (an asymmetric ~55/45 hero, a 33.33/33.33/33.33 feature grid, alternating ~60/40 image-and-text rows) instead of repeating the same centered block or identical grid everywhere. Buttons: understated solid-accent fill or a text link with a colored underline/arrow, fontWeight 600, padding ~"14px 26px", sharp-to-small corner radius, specific action copy ("Book a Consultation →") not generic "Click Here". Images: descriptive specific "alt" text, an intentional width/maxWidth so nothing stretches full-bleed by accident.
|
|
35
|
+
|
|
36
|
+
STRUCTURAL RULES:
|
|
28
37
|
The designJson argument MUST be a fully hydrated layout tree containing body style and rows array. There is no shorthand format —
|
|
29
38
|
do NOT send rows shaped like { "type": "products", "content": { "items": [...] } }. That will be REJECTED with a validation error.
|
|
30
|
-
Every row MUST already contain a 'columns' array with percentage widths ('100%', '50%', '33.33%'), and each column MUST contain
|
|
31
|
-
'elements' array
|
|
32
|
-
'spacer', 'form_container')
|
|
33
|
-
|
|
39
|
+
Every row MUST already contain a 'columns' array with percentage widths ('100%', '50%', '33.33%'), and each column MUST contain either
|
|
40
|
+
an 'elements' array of fully-styled component objects ('heading', 'paragraph', 'button', 'card', 'image', 'menu', 'social', 'divider',
|
|
41
|
+
'spacer', 'form_container') and/or a 'nestedRows' array (same shape as a top-level row — id/style/columns — used for the outer/inner
|
|
42
|
+
full-bleed section shell described above; a column may have nestedRows, elements, or both). For any repeated-item section (products,
|
|
43
|
+
features, testimonials, pricing tiers, etc.) YOU must expand each item into its own column with real elements — one column per item —
|
|
44
|
+
rather than passing a list for the server to interpret.
|
|
34
45
|
|
|
35
|
-
EXAMPLE VALID designJson PAYLOAD (hero
|
|
46
|
+
EXAMPLE VALID designJson PAYLOAD (full-bleed hero with an asymmetric 55/45 split, plus a full-bleed 3-up feature row) — note the
|
|
47
|
+
restrained ink/paper/terracotta palette, the serif/sans/mono type system, near-zero border radius, and the outer/inner row shell:
|
|
36
48
|
{
|
|
37
49
|
"body": {
|
|
38
|
-
"style": { "backgroundColor": "#
|
|
50
|
+
"style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Norrland Coffee Roasters" },
|
|
39
51
|
"rows": [
|
|
40
52
|
{
|
|
41
|
-
"id": "row-hero",
|
|
42
|
-
"style": { "
|
|
53
|
+
"id": "row-hero-outer",
|
|
54
|
+
"style": { "backgroundColor": "#1B2430", "padding": "0 clamp(24px, 6vw, 64px)" },
|
|
43
55
|
"columns": [
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
{ "id": "col-hero-outer", "width": "100%", "elements": [], "nestedRows": [
|
|
57
|
+
{
|
|
58
|
+
"id": "row-hero-inner",
|
|
59
|
+
"style": { "maxWidth": "1200px", "margin": "0 auto" },
|
|
60
|
+
"columns": [
|
|
61
|
+
{ "id": "col-hero-text", "width": "55%", "styles": { "padding": "96px 32px 96px 0" }, "elements": [
|
|
62
|
+
{ "id": "el-eyebrow", "type": "text", "style": { "fontFamily": "'IBM Plex Mono', monospace", "fontSize": "12px", "letterSpacing": "0.16em", "textTransform": "uppercase", "color": "#C4623B" }, "attributes": { "text": "Small-Batch Roastery" } },
|
|
63
|
+
{ "id": "el-heading", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "48px", "lineHeight": "1.1", "color": "#FAF7F1", "maxWidth": "560px", "margin": "16px 0 0" }, "attributes": { "text": "Coffee, Roasted With Intention" } },
|
|
64
|
+
{ "id": "el-sub", "type": "paragraph", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "17px", "lineHeight": "1.7", "color": "#B7BEC9", "maxWidth": "480px", "margin": "20px 0 0" }, "attributes": { "text": "Single-origin beans sourced direct from growers, roasted weekly in small batches for a cup that actually tastes like where it came from." } },
|
|
65
|
+
{ "id": "el-cta", "type": "button", "style": { "backgroundColor": "#C4623B", "color": "#FAF7F1", "border": "none", "borderRadius": "2px", "fontFamily": "'Inter', sans-serif", "fontWeight": "600", "fontSize": "15px", "padding": "14px 26px", "margin": "28px 0 0" }, "attributes": { "text": "Shop This Week's Roast →", "href": "/shop" } }
|
|
66
|
+
] },
|
|
67
|
+
{ "id": "col-hero-image", "width": "45%", "elements": [
|
|
68
|
+
{ "id": "el-hero-img", "type": "image", "style": { "width": "100%", "borderRadius": "4px", "boxShadow": "0 18px 34px -14px rgba(27,36,48,0.45)" }, "attributes": { "src": "https://...", "alt": "Roaster pouring fresh-roasted beans into a cooling tray" } }
|
|
69
|
+
] }
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
] }
|
|
53
73
|
]
|
|
54
74
|
},
|
|
55
75
|
{
|
|
56
|
-
"id": "row-
|
|
57
|
-
"style": { "
|
|
76
|
+
"id": "row-features-outer",
|
|
77
|
+
"style": { "backgroundColor": "#FAF7F1", "padding": "0 clamp(24px, 6vw, 64px)" },
|
|
58
78
|
"columns": [
|
|
59
|
-
{ "id": "col-
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
{ "id": "col-features-outer", "width": "100%", "elements": [], "nestedRows": [
|
|
80
|
+
{
|
|
81
|
+
"id": "row-features-inner",
|
|
82
|
+
"style": { "maxWidth": "1200px", "margin": "0 auto", "padding": "80px 0" },
|
|
83
|
+
"columns": [
|
|
84
|
+
{ "id": "col-feature-1", "width": "33.33%", "elements": [
|
|
85
|
+
{ "id": "el-icon-1", "type": "icon", "style": { "color": "#C4623B" }, "attributes": { "iconName": "leaf", "iconSize": 30 } },
|
|
86
|
+
{ "id": "el-h-1", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "22px", "color": "#1B2430", "margin": "16px 0 0" }, "attributes": { "text": "Direct Trade" } },
|
|
87
|
+
{ "id": "el-p-1", "type": "paragraph", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "15px", "lineHeight": "1.65", "color": "#5B6472", "margin": "8px 0 0" }, "attributes": { "text": "Every farm relationship is ours, not a broker's." } }
|
|
88
|
+
] },
|
|
89
|
+
{ "id": "col-feature-2", "width": "33.33%", "elements": [
|
|
90
|
+
{ "id": "el-icon-2", "type": "icon", "style": { "color": "#C4623B" }, "attributes": { "iconName": "flame", "iconSize": 30 } },
|
|
91
|
+
{ "id": "el-h-2", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "22px", "color": "#1B2430", "margin": "16px 0 0" }, "attributes": { "text": "Roasted Weekly" } },
|
|
92
|
+
{ "id": "el-p-2", "type": "paragraph", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "15px", "lineHeight": "1.65", "color": "#5B6472", "margin": "8px 0 0" }, "attributes": { "text": "Never more than seven days off the roaster." } }
|
|
93
|
+
] },
|
|
94
|
+
{ "id": "col-feature-3", "width": "33.33%", "elements": [
|
|
95
|
+
{ "id": "el-icon-3", "type": "icon", "style": { "color": "#C4623B" }, "attributes": { "iconName": "truck", "iconSize": 30 } },
|
|
96
|
+
{ "id": "el-h-3", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "22px", "color": "#1B2430", "margin": "16px 0 0" }, "attributes": { "text": "Shipped Fresh" } },
|
|
97
|
+
{ "id": "el-p-3", "type": "paragraph", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "15px", "lineHeight": "1.65", "color": "#5B6472", "margin": "8px 0 0" }, "attributes": { "text": "Out the door within 24 hours of roasting." } }
|
|
98
|
+
] }
|
|
99
|
+
]
|
|
100
|
+
}
|
|
76
101
|
] }
|
|
77
102
|
]
|
|
78
103
|
}
|
|
@@ -126,6 +151,7 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
|
|
|
126
151
|
width: { type: "string", description: "Percentage width (e.g. '100%', '50%', '33.33%')." },
|
|
127
152
|
elements: {
|
|
128
153
|
type: "array",
|
|
154
|
+
description: "This column's own content elements. May be empty ([]) when the column instead uses 'nestedRows' below (the full-bleed section-shell pattern) — a column may have elements, nestedRows, or both.",
|
|
129
155
|
items: {
|
|
130
156
|
type: "object",
|
|
131
157
|
required: ["id", "type", "style", "attributes"],
|
|
@@ -139,6 +165,15 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
|
|
|
139
165
|
},
|
|
140
166
|
},
|
|
141
167
|
},
|
|
168
|
+
nestedRows: {
|
|
169
|
+
type: "array",
|
|
170
|
+
description: "OPTIONAL. Nested row(s) inside this column, each the SAME shape as a top-level row (id/style/columns). This is how you build a full-bleed section: the outer row/column carries the background color edge-to-edge, and a nestedRow here caps content at a readable maxWidth. Use this instead of putting a 'maxWidth' directly on the outer row.",
|
|
171
|
+
items: {
|
|
172
|
+
type: "object",
|
|
173
|
+
required: ["id", "style", "columns"],
|
|
174
|
+
description: "Same shape as a top-level row: { id, style, columns: [{ id, width, elements, nestedRows? }] } — nesting may repeat if genuinely needed, but one level (outer band + inner capped row) covers nearly every section.",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
142
177
|
},
|
|
143
178
|
},
|
|
144
179
|
},
|
|
@@ -610,6 +645,8 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
|
|
|
610
645
|
...blogTools_js_1.BLOG_TOOL_DEFS,
|
|
611
646
|
...commerceTools_js_1.COMMERCE_TOOL_DEFS,
|
|
612
647
|
...lmsTools_js_1.LMS_TOOL_DEFS,
|
|
648
|
+
...donationTools_js_1.DONATION_TOOL_DEFS,
|
|
649
|
+
...wpMigrationTools_js_1.WP_MIGRATION_TOOL_DEFS,
|
|
613
650
|
],
|
|
614
651
|
};
|
|
615
652
|
});
|
|
@@ -886,6 +923,15 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
886
923
|
],
|
|
887
924
|
};
|
|
888
925
|
}
|
|
926
|
+
for (const [names, handler] of [
|
|
927
|
+
[donationTools_js_1.DONATION_TOOL_NAMES, donationTools_js_1.handleDonationToolCall],
|
|
928
|
+
[wpMigrationTools_js_1.WP_MIGRATION_TOOL_NAMES, wpMigrationTools_js_1.handleWpMigrationToolCall],
|
|
929
|
+
]) {
|
|
930
|
+
if (names.has(name)) {
|
|
931
|
+
const result = await handler(name, args || {}, client);
|
|
932
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
933
|
+
}
|
|
934
|
+
}
|
|
889
935
|
throw new Error(`Unknown tool: ${name}`);
|
|
890
936
|
}
|
|
891
937
|
}
|