@charisol/plexo-mcp 1.0.9 → 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 +269 -0
- package/dist/commerceTools.js +523 -0
- package/dist/donationTools.js +93 -0
- package/dist/index.js +145 -38
- package/dist/lmsTools.js +254 -0
- package/dist/wpMigrationTools.js +194 -0
- package/package.json +1 -1
- package/src/blogTools.ts +14 -11
- package/src/client/plexoClient.ts +321 -0
- package/src/commerceTools.ts +538 -0
- package/src/donationTools.ts +98 -0
- package/src/index.ts +146 -39
- package/src/lmsTools.ts +262 -0
- package/src/wpMigrationTools.ts +198 -0
package/src/index.ts
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import { PlexoClient } from "./client/plexoClient.js";
|
|
10
10
|
import { BLOG_TOOL_DEFS, BLOG_TOOL_NAMES, handleBlogToolCall } from "./blogTools.js";
|
|
11
|
+
import { COMMERCE_TOOL_DEFS, COMMERCE_TOOL_NAMES, handleCommerceToolCall } from "./commerceTools.js";
|
|
12
|
+
import { LMS_TOOL_DEFS, LMS_TOOL_NAMES, handleLmsToolCall } from "./lmsTools.js";
|
|
13
|
+
import { DONATION_TOOL_DEFS, DONATION_TOOL_NAMES, handleDonationToolCall } from "./donationTools.js";
|
|
14
|
+
import { WP_MIGRATION_TOOL_DEFS, WP_MIGRATION_TOOL_NAMES, handleWpMigrationToolCall } from "./wpMigrationTools.js";
|
|
11
15
|
|
|
12
16
|
const server = new Server(
|
|
13
17
|
{
|
|
@@ -29,55 +33,78 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
29
33
|
name: "publish_landing_page",
|
|
30
34
|
description: `Creates, compiles, and publishes a landing page to Plexo in one atomic step. Returns the published live URL and editable visual builder URL.
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
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.
|
|
37
|
+
- 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.
|
|
38
|
+
- 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.
|
|
39
|
+
- 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.
|
|
40
|
+
- 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.
|
|
41
|
+
- 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.
|
|
42
|
+
|
|
43
|
+
STRUCTURAL RULES:
|
|
33
44
|
The designJson argument MUST be a fully hydrated layout tree containing body style and rows array. There is no shorthand format —
|
|
34
45
|
do NOT send rows shaped like { "type": "products", "content": { "items": [...] } }. That will be REJECTED with a validation error.
|
|
35
|
-
Every row MUST already contain a 'columns' array with percentage widths ('100%', '50%', '33.33%'), and each column MUST contain
|
|
36
|
-
'elements' array
|
|
37
|
-
'spacer', 'form_container')
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
Every row MUST already contain a 'columns' array with percentage widths ('100%', '50%', '33.33%'), and each column MUST contain either
|
|
47
|
+
an 'elements' array of fully-styled component objects ('heading', 'paragraph', 'button', 'card', 'image', 'menu', 'social', 'divider',
|
|
48
|
+
'spacer', 'form_container') and/or a 'nestedRows' array (same shape as a top-level row — id/style/columns — used for the outer/inner
|
|
49
|
+
full-bleed section shell described above; a column may have nestedRows, elements, or both). For any repeated-item section (products,
|
|
50
|
+
features, testimonials, pricing tiers, etc.) YOU must expand each item into its own column with real elements — one column per item —
|
|
51
|
+
rather than passing a list for the server to interpret.
|
|
52
|
+
|
|
53
|
+
EXAMPLE VALID designJson PAYLOAD (full-bleed hero with an asymmetric 55/45 split, plus a full-bleed 3-up feature row) — note the
|
|
54
|
+
restrained ink/paper/terracotta palette, the serif/sans/mono type system, near-zero border radius, and the outer/inner row shell:
|
|
41
55
|
{
|
|
42
56
|
"body": {
|
|
43
|
-
"style": { "backgroundColor": "#
|
|
57
|
+
"style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Norrland Coffee Roasters" },
|
|
44
58
|
"rows": [
|
|
45
59
|
{
|
|
46
|
-
"id": "row-hero",
|
|
47
|
-
"style": { "
|
|
60
|
+
"id": "row-hero-outer",
|
|
61
|
+
"style": { "backgroundColor": "#1B2430", "padding": "0 clamp(24px, 6vw, 64px)" },
|
|
48
62
|
"columns": [
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
{ "id": "col-hero-outer", "width": "100%", "elements": [], "nestedRows": [
|
|
64
|
+
{
|
|
65
|
+
"id": "row-hero-inner",
|
|
66
|
+
"style": { "maxWidth": "1200px", "margin": "0 auto" },
|
|
67
|
+
"columns": [
|
|
68
|
+
{ "id": "col-hero-text", "width": "55%", "styles": { "padding": "96px 32px 96px 0" }, "elements": [
|
|
69
|
+
{ "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" } },
|
|
70
|
+
{ "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" } },
|
|
71
|
+
{ "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." } },
|
|
72
|
+
{ "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" } }
|
|
73
|
+
] },
|
|
74
|
+
{ "id": "col-hero-image", "width": "45%", "elements": [
|
|
75
|
+
{ "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" } }
|
|
76
|
+
] }
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
] }
|
|
58
80
|
]
|
|
59
81
|
},
|
|
60
82
|
{
|
|
61
|
-
"id": "row-
|
|
62
|
-
"style": { "
|
|
83
|
+
"id": "row-features-outer",
|
|
84
|
+
"style": { "backgroundColor": "#FAF7F1", "padding": "0 clamp(24px, 6vw, 64px)" },
|
|
63
85
|
"columns": [
|
|
64
|
-
{ "id": "col-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
{ "id": "col-features-outer", "width": "100%", "elements": [], "nestedRows": [
|
|
87
|
+
{
|
|
88
|
+
"id": "row-features-inner",
|
|
89
|
+
"style": { "maxWidth": "1200px", "margin": "0 auto", "padding": "80px 0" },
|
|
90
|
+
"columns": [
|
|
91
|
+
{ "id": "col-feature-1", "width": "33.33%", "elements": [
|
|
92
|
+
{ "id": "el-icon-1", "type": "icon", "style": { "color": "#C4623B" }, "attributes": { "iconName": "leaf", "iconSize": 30 } },
|
|
93
|
+
{ "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" } },
|
|
94
|
+
{ "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." } }
|
|
95
|
+
] },
|
|
96
|
+
{ "id": "col-feature-2", "width": "33.33%", "elements": [
|
|
97
|
+
{ "id": "el-icon-2", "type": "icon", "style": { "color": "#C4623B" }, "attributes": { "iconName": "flame", "iconSize": 30 } },
|
|
98
|
+
{ "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" } },
|
|
99
|
+
{ "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." } }
|
|
100
|
+
] },
|
|
101
|
+
{ "id": "col-feature-3", "width": "33.33%", "elements": [
|
|
102
|
+
{ "id": "el-icon-3", "type": "icon", "style": { "color": "#C4623B" }, "attributes": { "iconName": "truck", "iconSize": 30 } },
|
|
103
|
+
{ "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" } },
|
|
104
|
+
{ "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." } }
|
|
105
|
+
] }
|
|
106
|
+
]
|
|
107
|
+
}
|
|
81
108
|
] }
|
|
82
109
|
]
|
|
83
110
|
}
|
|
@@ -131,6 +158,7 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
|
|
|
131
158
|
width: { type: "string", description: "Percentage width (e.g. '100%', '50%', '33.33%')." },
|
|
132
159
|
elements: {
|
|
133
160
|
type: "array",
|
|
161
|
+
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.",
|
|
134
162
|
items: {
|
|
135
163
|
type: "object",
|
|
136
164
|
required: ["id", "type", "style", "attributes"],
|
|
@@ -144,6 +172,15 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
|
|
|
144
172
|
},
|
|
145
173
|
},
|
|
146
174
|
},
|
|
175
|
+
nestedRows: {
|
|
176
|
+
type: "array",
|
|
177
|
+
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.",
|
|
178
|
+
items: {
|
|
179
|
+
type: "object",
|
|
180
|
+
required: ["id", "style", "columns"],
|
|
181
|
+
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.",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
147
184
|
},
|
|
148
185
|
},
|
|
149
186
|
},
|
|
@@ -513,6 +550,23 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
|
|
|
513
550
|
required: ["templateId"],
|
|
514
551
|
},
|
|
515
552
|
},
|
|
553
|
+
{
|
|
554
|
+
name: "delete_template",
|
|
555
|
+
description: "Permanently deletes any landing page or email template by templateId or template name (e.g. 'buzm'). Same underlying operation as delete_landing_page_page. Confirm with user before calling as this action cannot be undone.",
|
|
556
|
+
inputSchema: {
|
|
557
|
+
type: "object",
|
|
558
|
+
properties: {
|
|
559
|
+
templateId: {
|
|
560
|
+
type: "string",
|
|
561
|
+
description: "ID of the template or page to delete (UUID, or exact/partial template name like 'buzm').",
|
|
562
|
+
},
|
|
563
|
+
name: {
|
|
564
|
+
type: "string",
|
|
565
|
+
description: "Optional template name or search phrase if templateId is unknown (e.g. 'buzm').",
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
516
570
|
{
|
|
517
571
|
name: "delete_landing_page_page",
|
|
518
572
|
description: "Deletes a page. If it has sub-pages nested under it, they're deleted too (cascade) — the response reports how many. Deleting a site's home page deletes the whole site, including every page nested under it and its published domain link — this can't be undone, confirm with the user first.",
|
|
@@ -596,6 +650,10 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
|
|
|
596
650
|
},
|
|
597
651
|
},
|
|
598
652
|
...BLOG_TOOL_DEFS,
|
|
653
|
+
...COMMERCE_TOOL_DEFS,
|
|
654
|
+
...LMS_TOOL_DEFS,
|
|
655
|
+
...DONATION_TOOL_DEFS,
|
|
656
|
+
...WP_MIGRATION_TOOL_DEFS,
|
|
599
657
|
],
|
|
600
658
|
};
|
|
601
659
|
});
|
|
@@ -751,8 +809,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
751
809
|
};
|
|
752
810
|
}
|
|
753
811
|
|
|
812
|
+
case "delete_template":
|
|
754
813
|
case "delete_landing_page_page": {
|
|
755
|
-
const
|
|
814
|
+
const rawInput = ((args as any)?.templateId || (args as any)?.name || "").trim();
|
|
815
|
+
if (!rawInput) throw new Error("templateId or template name is required.");
|
|
816
|
+
|
|
817
|
+
// Same UUID-or-name resolution as the server's own delete_template/
|
|
818
|
+
// delete_landing_page_page handler — try an exact id match first, then fall back to
|
|
819
|
+
// a case-insensitive name search across this account's templates.
|
|
820
|
+
let resolvedId = rawInput;
|
|
821
|
+
const { templates } = await client.listTemplates();
|
|
822
|
+
const byId = (templates || []).find((t: any) => t.id === rawInput);
|
|
823
|
+
if (!byId) {
|
|
824
|
+
const needle = rawInput.toLowerCase();
|
|
825
|
+
const matches = (templates || []).filter((t: any) => t.name?.toLowerCase().includes(needle));
|
|
826
|
+
matches.sort((a: any, b: any) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
827
|
+
if (!matches[0]) throw new Error(`No template found matching "${rawInput}".`);
|
|
828
|
+
resolvedId = matches[0].id;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const result = await client.deleteLandingPagePage(resolvedId);
|
|
756
832
|
return {
|
|
757
833
|
content: [
|
|
758
834
|
{
|
|
@@ -861,6 +937,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
861
937
|
],
|
|
862
938
|
};
|
|
863
939
|
}
|
|
940
|
+
if (COMMERCE_TOOL_NAMES.has(name)) {
|
|
941
|
+
const result = await handleCommerceToolCall(name, (args as any) || {}, client);
|
|
942
|
+
return {
|
|
943
|
+
content: [
|
|
944
|
+
{
|
|
945
|
+
type: "text",
|
|
946
|
+
text: JSON.stringify(result, null, 2),
|
|
947
|
+
},
|
|
948
|
+
],
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
if (LMS_TOOL_NAMES.has(name)) {
|
|
952
|
+
const result = await handleLmsToolCall(name, (args as any) || {}, client);
|
|
953
|
+
return {
|
|
954
|
+
content: [
|
|
955
|
+
{
|
|
956
|
+
type: "text",
|
|
957
|
+
text: JSON.stringify(result, null, 2),
|
|
958
|
+
},
|
|
959
|
+
],
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
for (const [names, handler] of [
|
|
963
|
+
[DONATION_TOOL_NAMES, handleDonationToolCall],
|
|
964
|
+
[WP_MIGRATION_TOOL_NAMES, handleWpMigrationToolCall],
|
|
965
|
+
] as const) {
|
|
966
|
+
if (names.has(name)) {
|
|
967
|
+
const result = await handler(name, (args as any) || {}, client);
|
|
968
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
864
971
|
throw new Error(`Unknown tool: ${name}`);
|
|
865
972
|
}
|
|
866
973
|
}
|
package/src/lmsTools.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { PlexoClient } from "./client/plexoClient.js";
|
|
2
|
+
|
|
3
|
+
export const LMS_TOOL_DEFS = [
|
|
4
|
+
{
|
|
5
|
+
name: "create_funnel",
|
|
6
|
+
description: "Attaches Funnel/LMS capability to an existing root landing page (see create_landing_page / list_landing_pages for how to get one), seeding a single Welcome page. Gated by the organization's funnel slot allowance (get_funnel_addon_slots shows current usage) — fails with the slot summary if none is available.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: { templateId: { type: "string", description: "An existing root landing page's template id to turn into a funnel." } },
|
|
10
|
+
required: ["templateId"],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "get_funnel",
|
|
15
|
+
description: "Fetches a funnel's settings: enabled state, signup mode (FREE/PAID), signup product, and welcome-page options.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
|
|
19
|
+
required: ["templateId"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "update_funnel",
|
|
24
|
+
description: "Updates a funnel's settings. Only the fields provided are changed. Switching signupMode to PAID requires signupProductId (a DIGITAL Commerce product) to already be set or provided here.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
templateId: { type: "string", description: "The funnel's home page template id." },
|
|
29
|
+
enabled: { type: "boolean" },
|
|
30
|
+
signupMode: { type: "string", enum: ["FREE", "PAID"] },
|
|
31
|
+
signupProductId: { type: "string", description: "A DIGITAL Commerce product id (see create_commerce_product). Pass an empty string to clear it." },
|
|
32
|
+
requireName: { type: "boolean" },
|
|
33
|
+
collectPhone: { type: "boolean" },
|
|
34
|
+
welcomeRedirectPath: { type: "string", description: "Pass an empty string to clear it." },
|
|
35
|
+
},
|
|
36
|
+
required: ["templateId"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "delete_funnel",
|
|
41
|
+
description: "Soft-deletes a funnel (enabled:false — hidden from visitors). Pages, enrollments, and progress are kept, so re-enabling later restores everything as-is.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
|
|
45
|
+
required: ["templateId"],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "list_funnel_pages",
|
|
50
|
+
description: "Lists a funnel's pages in their display order.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
|
|
54
|
+
required: ["templateId"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "create_funnel_page",
|
|
59
|
+
description: "Appends a new page to a funnel. Defaults to kind VIDEO and unlocks after the current last page — use update_funnel_page to customize unlockRules afterward.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
templateId: { type: "string", description: "The funnel's home page template id." },
|
|
64
|
+
title: { type: "string", description: "Defaults to \"Page N\"." },
|
|
65
|
+
kind: { type: "string", enum: ["WELCOME", "VIDEO", "DOCUMENT", "IMAGE"], description: "Defaults to VIDEO." },
|
|
66
|
+
},
|
|
67
|
+
required: ["templateId"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "update_funnel_page",
|
|
72
|
+
description: "Updates a funnel page's content and/or unlock rules. Only the fields provided are changed. unlockRules shape: { mode: \"ALL\", conditions: [...] } where each condition is one of PREREQUISITE_PAGES ({type,pageIds}), TIME_DRIP_RELATIVE ({type,days}), TIME_DRIP_FIXED_DATE ({type,date}), MANUAL_APPROVAL ({type}) — satisfied via approve_funnel_enrollment_page.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
templateId: { type: "string", description: "The funnel's home page template id." },
|
|
77
|
+
pageId: { type: "string" },
|
|
78
|
+
title: { type: "string" },
|
|
79
|
+
kind: { type: "string", enum: ["WELCOME", "VIDEO", "DOCUMENT", "IMAGE"] },
|
|
80
|
+
contentMode: { type: "string", enum: ["SIMPLE", "RAW_HTML", "ADVANCED"], description: "SIMPLE/RAW_HTML both write to bodyHtml; ADVANCED is a full-builder page and isn't settable from here." },
|
|
81
|
+
bodyHtml: { type: "string", description: "Sanitized server-side. For a WELCOME page this renders as plain text, not markup." },
|
|
82
|
+
youtubeVideoId: { type: "string", description: "Empty string clears it." },
|
|
83
|
+
assetUrl: { type: "string", description: "Already-uploaded file URL (this tool can't accept raw bytes). Empty string clears it." },
|
|
84
|
+
assetFileName: { type: "string" },
|
|
85
|
+
assetMimeType: { type: "string" },
|
|
86
|
+
assetSizeBytes: { type: "number" },
|
|
87
|
+
unlockRules: { type: "object", description: "See this tool's description for the shape." },
|
|
88
|
+
visible: { type: "boolean" },
|
|
89
|
+
},
|
|
90
|
+
required: ["templateId", "pageId"],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "delete_funnel_page",
|
|
95
|
+
description: "Deletes a funnel page and closes the resulting order gap in the remaining pages.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
templateId: { type: "string", description: "The funnel's home page template id." },
|
|
100
|
+
pageId: { type: "string" },
|
|
101
|
+
},
|
|
102
|
+
required: ["templateId", "pageId"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "reorder_funnel_pages",
|
|
107
|
+
description: "Reorders a funnel's pages. orderedPageIds must contain exactly this funnel's current page ids, in the new order.",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
templateId: { type: "string", description: "The funnel's home page template id." },
|
|
112
|
+
orderedPageIds: { type: "array", items: { type: "string" } },
|
|
113
|
+
},
|
|
114
|
+
required: ["templateId", "orderedPageIds"],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "list_funnel_enrollments",
|
|
119
|
+
description: "Lists a funnel's learner enrollments, most recently enrolled first.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
|
|
123
|
+
required: ["templateId"],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "get_funnel_enrollment",
|
|
128
|
+
description: "Fetches one learner's enrollment plus their per-page progress.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
templateId: { type: "string", description: "The funnel's home page template id." },
|
|
133
|
+
email: { type: "string" },
|
|
134
|
+
},
|
|
135
|
+
required: ["templateId", "email"],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "approve_funnel_enrollment_page",
|
|
140
|
+
description: "Marks one page as manually approved for one learner — the way a MANUAL_APPROVAL unlock condition gets satisfied (see update_funnel_page's unlockRules description). Reversible: calling it again just re-stamps the approval.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
templateId: { type: "string", description: "The funnel's home page template id." },
|
|
145
|
+
email: { type: "string" },
|
|
146
|
+
pageId: { type: "string" },
|
|
147
|
+
},
|
|
148
|
+
required: ["templateId", "email", "pageId"],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "set_funnel_enrollment_status",
|
|
153
|
+
description: "Revokes or reactivates a learner's enrollment. Revoked takes effect immediately on their next page load (no session to separately invalidate).",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
templateId: { type: "string", description: "The funnel's home page template id." },
|
|
158
|
+
email: { type: "string" },
|
|
159
|
+
status: { type: "string", enum: ["ACTIVE", "REVOKED"] },
|
|
160
|
+
},
|
|
161
|
+
required: ["templateId", "email", "status"],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "resend_funnel_magic_link",
|
|
166
|
+
description: "Re-sends the passwordless login email to an ACTIVE learner. Rate-limited to once per 60 seconds per learner.",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
templateId: { type: "string", description: "The funnel's home page template id." },
|
|
171
|
+
email: { type: "string" },
|
|
172
|
+
},
|
|
173
|
+
required: ["templateId", "email"],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "get_funnel_addon_slots",
|
|
178
|
+
description: "Reports the organization's funnel slot usage (plan-included + purchased addon slots vs. funnels currently using one) and any active addon-slot subscriptions. Org-scoped, not per-site.",
|
|
179
|
+
inputSchema: { type: "object", properties: {} },
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
export const LMS_TOOL_NAMES = new Set(LMS_TOOL_DEFS.map((t) => t.name));
|
|
184
|
+
|
|
185
|
+
export async function handleLmsToolCall(name: string, args: any, client: PlexoClient): Promise<any> {
|
|
186
|
+
const templateId = typeof args?.templateId === "string" ? args.templateId : "";
|
|
187
|
+
|
|
188
|
+
switch (name) {
|
|
189
|
+
case "create_funnel": {
|
|
190
|
+
if (!templateId) throw new Error("templateId is required.");
|
|
191
|
+
const result = await client.createFunnel(templateId);
|
|
192
|
+
return { success: true, funnel: result.funnel };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case "get_funnel":
|
|
196
|
+
return await client.getFunnel(templateId);
|
|
197
|
+
|
|
198
|
+
case "update_funnel": {
|
|
199
|
+
const { templateId: _t, ...payload } = args;
|
|
200
|
+
const result = await client.updateFunnel(templateId, payload);
|
|
201
|
+
return { success: true, funnel: result.funnel };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case "delete_funnel":
|
|
205
|
+
await client.deleteFunnel(templateId);
|
|
206
|
+
return { success: true };
|
|
207
|
+
|
|
208
|
+
case "list_funnel_pages":
|
|
209
|
+
return await client.listFunnelPages(templateId);
|
|
210
|
+
|
|
211
|
+
case "create_funnel_page": {
|
|
212
|
+
const result = await client.createFunnelPage(templateId, { title: args.title, kind: args.kind });
|
|
213
|
+
return { success: true, page: result.page };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case "update_funnel_page": {
|
|
217
|
+
if (!args.pageId) throw new Error("pageId is required.");
|
|
218
|
+
const { templateId: _t, pageId, ...payload } = args;
|
|
219
|
+
const result = await client.updateFunnelPage(templateId, pageId, payload);
|
|
220
|
+
return { success: true, page: result.page };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case "delete_funnel_page":
|
|
224
|
+
if (!args.pageId) throw new Error("pageId is required.");
|
|
225
|
+
await client.deleteFunnelPage(templateId, args.pageId);
|
|
226
|
+
return { success: true, deletedPageId: args.pageId };
|
|
227
|
+
|
|
228
|
+
case "reorder_funnel_pages": {
|
|
229
|
+
if (!Array.isArray(args.orderedPageIds) || args.orderedPageIds.length === 0) throw new Error("orderedPageIds is required.");
|
|
230
|
+
return await client.reorderFunnelPages(templateId, args.orderedPageIds);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case "list_funnel_enrollments":
|
|
234
|
+
return await client.listFunnelEnrollments(templateId);
|
|
235
|
+
|
|
236
|
+
case "get_funnel_enrollment":
|
|
237
|
+
if (!args.email) throw new Error("email is required.");
|
|
238
|
+
return await client.getFunnelEnrollment(templateId, args.email);
|
|
239
|
+
|
|
240
|
+
case "approve_funnel_enrollment_page": {
|
|
241
|
+
if (!args.email || !args.pageId) throw new Error("email and pageId are required.");
|
|
242
|
+
const result = await client.approveFunnelEnrollmentPage(templateId, args.email, args.pageId);
|
|
243
|
+
return { success: true, progress: result.progress };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case "set_funnel_enrollment_status": {
|
|
247
|
+
if (!args.email || !args.status) throw new Error("email and status are required.");
|
|
248
|
+
const result = await client.setFunnelEnrollmentStatus(templateId, args.email, args.status);
|
|
249
|
+
return { success: true, enrollment: result.enrollment };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case "resend_funnel_magic_link":
|
|
253
|
+
if (!args.email) throw new Error("email is required.");
|
|
254
|
+
return await client.resendFunnelMagicLink(templateId, args.email);
|
|
255
|
+
|
|
256
|
+
case "get_funnel_addon_slots":
|
|
257
|
+
return await client.getFunnelAddonSlots();
|
|
258
|
+
|
|
259
|
+
default:
|
|
260
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
261
|
+
}
|
|
262
|
+
}
|