@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/dist/index.js CHANGED
@@ -6,6 +6,10 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
6
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
7
  const plexoClient_js_1 = require("./client/plexoClient.js");
8
8
  const blogTools_js_1 = require("./blogTools.js");
9
+ const commerceTools_js_1 = require("./commerceTools.js");
10
+ const lmsTools_js_1 = require("./lmsTools.js");
11
+ const donationTools_js_1 = require("./donationTools.js");
12
+ const wpMigrationTools_js_1 = require("./wpMigrationTools.js");
9
13
  const server = new index_js_1.Server({
10
14
  name: "plexo-mcp",
11
15
  version: "1.0.0",
@@ -22,55 +26,78 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
22
26
  name: "publish_landing_page",
23
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.
24
28
 
25
- IMPORTANT INSTRUCTION FOR AI CLIENTS:
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:
26
37
  The designJson argument MUST be a fully hydrated layout tree containing body style and rows array. There is no shorthand format —
27
38
  do NOT send rows shaped like { "type": "products", "content": { "items": [...] } }. That will be REJECTED with a validation error.
28
- Every row MUST already contain a 'columns' array with percentage widths ('100%', '50%', '33.33%'), and each column MUST contain an
29
- 'elements' array with fully-styled component objects ('heading', 'paragraph', 'button', 'card', 'image', 'menu', 'social', 'divider',
30
- 'spacer', 'form_container'). For any repeated-item section (products, features, testimonials, pricing tiers, etc.) YOU must expand
31
- each item into its own column with real elements — one column per item — rather than passing a list for the server to interpret.
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.
32
45
 
33
- EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
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:
34
48
  {
35
49
  "body": {
36
- "style": { "backgroundColor": "#08090f", "color": "#f0f2ff", "fontFamily": "Inter, sans-serif", "htmlTitle": "Bulum SaaS Platform" },
50
+ "style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Norrland Coffee Roasters" },
37
51
  "rows": [
38
52
  {
39
- "id": "row-hero",
40
- "style": { "paddingTop": "80px", "paddingBottom": "80px" },
53
+ "id": "row-hero-outer",
54
+ "style": { "backgroundColor": "#1B2430", "padding": "0 clamp(24px, 6vw, 64px)" },
41
55
  "columns": [
42
- {
43
- "id": "col-hero",
44
- "width": "100%",
45
- "elements": [
46
- { "type": "heading", "style": { "fontSize": "48px", "fontWeight": "800", "textAlign": "center", "color": "#ffffff" }, "attributes": { "text": "Build & Scale Faster" } },
47
- { "type": "paragraph", "style": { "fontSize": "18px", "color": "#94a3b8", "textAlign": "center" }, "attributes": { "text": "The ultimate platform for modern digital teams." } },
48
- { "type": "button", "style": { "textAlign": "center", "backgroundColor": "#8b5cf6", "color": "#ffffff", "borderRadius": "12px", "paddingTop": "14px", "paddingBottom": "14px", "paddingLeft": "28px", "paddingRight": "28px" }, "attributes": { "text": "Start Free Trial", "href": "#signup" } }
49
- ]
50
- }
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
+ ] }
51
73
  ]
52
74
  },
53
75
  {
54
- "id": "row-products",
55
- "style": { "paddingTop": "60px", "paddingBottom": "60px" },
76
+ "id": "row-features-outer",
77
+ "style": { "backgroundColor": "#FAF7F1", "padding": "0 clamp(24px, 6vw, 64px)" },
56
78
  "columns": [
57
- { "id": "col-product-1", "width": "33.33%", "elements": [
58
- { "type": "image", "style": { "width": "100%", "borderRadius": "12px" }, "attributes": { "src": "https://...", "alt": "Urban Runner" } },
59
- { "type": "heading", "style": { "fontSize": "20px", "marginTop": "12px" }, "attributes": { "text": "Urban Runner" } },
60
- { "type": "paragraph", "style": { "fontSize": "14px", "color": "#94a3b8" }, "attributes": { "text": "Lightweight sneakers for daily adventures" } },
61
- { "type": "paragraph", "style": { "fontSize": "16px", "fontWeight": "700", "color": "#8b5cf6" }, "attributes": { "text": "$89" } }
62
- ] },
63
- { "id": "col-product-2", "width": "33.33%", "elements": [
64
- { "type": "image", "style": { "width": "100%", "borderRadius": "12px" }, "attributes": { "src": "https://...", "alt": "Classic Court" } },
65
- { "type": "heading", "style": { "fontSize": "20px", "marginTop": "12px" }, "attributes": { "text": "Classic Court" } },
66
- { "type": "paragraph", "style": { "fontSize": "14px", "color": "#94a3b8" }, "attributes": { "text": "Timeless style with modern comfort" } },
67
- { "type": "paragraph", "style": { "fontSize": "16px", "fontWeight": "700", "color": "#8b5cf6" }, "attributes": { "text": "$79" } }
68
- ] },
69
- { "id": "col-product-3", "width": "33.33%", "elements": [
70
- { "type": "image", "style": { "width": "100%", "borderRadius": "12px" }, "attributes": { "src": "https://...", "alt": "Street Flex" } },
71
- { "type": "heading", "style": { "fontSize": "20px", "marginTop": "12px" }, "attributes": { "text": "Street Flex" } },
72
- { "type": "paragraph", "style": { "fontSize": "14px", "color": "#94a3b8" }, "attributes": { "text": "Flexible performance footwear" } },
73
- { "type": "paragraph", "style": { "fontSize": "16px", "fontWeight": "700", "color": "#8b5cf6" }, "attributes": { "text": "$99" } }
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
+ }
74
101
  ] }
75
102
  ]
76
103
  }
@@ -124,6 +151,7 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
124
151
  width: { type: "string", description: "Percentage width (e.g. '100%', '50%', '33.33%')." },
125
152
  elements: {
126
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.",
127
155
  items: {
128
156
  type: "object",
129
157
  required: ["id", "type", "style", "attributes"],
@@ -137,6 +165,15 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
137
165
  },
138
166
  },
139
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
+ },
140
177
  },
141
178
  },
142
179
  },
@@ -506,6 +543,23 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
506
543
  required: ["templateId"],
507
544
  },
508
545
  },
546
+ {
547
+ name: "delete_template",
548
+ 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.",
549
+ inputSchema: {
550
+ type: "object",
551
+ properties: {
552
+ templateId: {
553
+ type: "string",
554
+ description: "ID of the template or page to delete (UUID, or exact/partial template name like 'buzm').",
555
+ },
556
+ name: {
557
+ type: "string",
558
+ description: "Optional template name or search phrase if templateId is unknown (e.g. 'buzm').",
559
+ },
560
+ },
561
+ },
562
+ },
509
563
  {
510
564
  name: "delete_landing_page_page",
511
565
  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.",
@@ -589,6 +643,10 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
589
643
  },
590
644
  },
591
645
  ...blogTools_js_1.BLOG_TOOL_DEFS,
646
+ ...commerceTools_js_1.COMMERCE_TOOL_DEFS,
647
+ ...lmsTools_js_1.LMS_TOOL_DEFS,
648
+ ...donationTools_js_1.DONATION_TOOL_DEFS,
649
+ ...wpMigrationTools_js_1.WP_MIGRATION_TOOL_DEFS,
592
650
  ],
593
651
  };
594
652
  });
@@ -730,8 +788,26 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
730
788
  ],
731
789
  };
732
790
  }
791
+ case "delete_template":
733
792
  case "delete_landing_page_page": {
734
- const result = await client.deleteLandingPagePage(args?.templateId);
793
+ const rawInput = (args?.templateId || args?.name || "").trim();
794
+ if (!rawInput)
795
+ throw new Error("templateId or template name is required.");
796
+ // Same UUID-or-name resolution as the server's own delete_template/
797
+ // delete_landing_page_page handler — try an exact id match first, then fall back to
798
+ // a case-insensitive name search across this account's templates.
799
+ let resolvedId = rawInput;
800
+ const { templates } = await client.listTemplates();
801
+ const byId = (templates || []).find((t) => t.id === rawInput);
802
+ if (!byId) {
803
+ const needle = rawInput.toLowerCase();
804
+ const matches = (templates || []).filter((t) => t.name?.toLowerCase().includes(needle));
805
+ matches.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
806
+ if (!matches[0])
807
+ throw new Error(`No template found matching "${rawInput}".`);
808
+ resolvedId = matches[0].id;
809
+ }
810
+ const result = await client.deleteLandingPagePage(resolvedId);
735
811
  return {
736
812
  content: [
737
813
  {
@@ -825,6 +901,37 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
825
901
  ],
826
902
  };
827
903
  }
904
+ if (commerceTools_js_1.COMMERCE_TOOL_NAMES.has(name)) {
905
+ const result = await (0, commerceTools_js_1.handleCommerceToolCall)(name, args || {}, client);
906
+ return {
907
+ content: [
908
+ {
909
+ type: "text",
910
+ text: JSON.stringify(result, null, 2),
911
+ },
912
+ ],
913
+ };
914
+ }
915
+ if (lmsTools_js_1.LMS_TOOL_NAMES.has(name)) {
916
+ const result = await (0, lmsTools_js_1.handleLmsToolCall)(name, args || {}, client);
917
+ return {
918
+ content: [
919
+ {
920
+ type: "text",
921
+ text: JSON.stringify(result, null, 2),
922
+ },
923
+ ],
924
+ };
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
+ }
828
935
  throw new Error(`Unknown tool: ${name}`);
829
936
  }
830
937
  }
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LMS_TOOL_NAMES = exports.LMS_TOOL_DEFS = void 0;
4
+ exports.handleLmsToolCall = handleLmsToolCall;
5
+ exports.LMS_TOOL_DEFS = [
6
+ {
7
+ name: "create_funnel",
8
+ 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.",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: { templateId: { type: "string", description: "An existing root landing page's template id to turn into a funnel." } },
12
+ required: ["templateId"],
13
+ },
14
+ },
15
+ {
16
+ name: "get_funnel",
17
+ description: "Fetches a funnel's settings: enabled state, signup mode (FREE/PAID), signup product, and welcome-page options.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
21
+ required: ["templateId"],
22
+ },
23
+ },
24
+ {
25
+ name: "update_funnel",
26
+ 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.",
27
+ inputSchema: {
28
+ type: "object",
29
+ properties: {
30
+ templateId: { type: "string", description: "The funnel's home page template id." },
31
+ enabled: { type: "boolean" },
32
+ signupMode: { type: "string", enum: ["FREE", "PAID"] },
33
+ signupProductId: { type: "string", description: "A DIGITAL Commerce product id (see create_commerce_product). Pass an empty string to clear it." },
34
+ requireName: { type: "boolean" },
35
+ collectPhone: { type: "boolean" },
36
+ welcomeRedirectPath: { type: "string", description: "Pass an empty string to clear it." },
37
+ },
38
+ required: ["templateId"],
39
+ },
40
+ },
41
+ {
42
+ name: "delete_funnel",
43
+ description: "Soft-deletes a funnel (enabled:false — hidden from visitors). Pages, enrollments, and progress are kept, so re-enabling later restores everything as-is.",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
47
+ required: ["templateId"],
48
+ },
49
+ },
50
+ {
51
+ name: "list_funnel_pages",
52
+ description: "Lists a funnel's pages in their display order.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
56
+ required: ["templateId"],
57
+ },
58
+ },
59
+ {
60
+ name: "create_funnel_page",
61
+ 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.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ templateId: { type: "string", description: "The funnel's home page template id." },
66
+ title: { type: "string", description: "Defaults to \"Page N\"." },
67
+ kind: { type: "string", enum: ["WELCOME", "VIDEO", "DOCUMENT", "IMAGE"], description: "Defaults to VIDEO." },
68
+ },
69
+ required: ["templateId"],
70
+ },
71
+ },
72
+ {
73
+ name: "update_funnel_page",
74
+ 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.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ templateId: { type: "string", description: "The funnel's home page template id." },
79
+ pageId: { type: "string" },
80
+ title: { type: "string" },
81
+ kind: { type: "string", enum: ["WELCOME", "VIDEO", "DOCUMENT", "IMAGE"] },
82
+ 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." },
83
+ bodyHtml: { type: "string", description: "Sanitized server-side. For a WELCOME page this renders as plain text, not markup." },
84
+ youtubeVideoId: { type: "string", description: "Empty string clears it." },
85
+ assetUrl: { type: "string", description: "Already-uploaded file URL (this tool can't accept raw bytes). Empty string clears it." },
86
+ assetFileName: { type: "string" },
87
+ assetMimeType: { type: "string" },
88
+ assetSizeBytes: { type: "number" },
89
+ unlockRules: { type: "object", description: "See this tool's description for the shape." },
90
+ visible: { type: "boolean" },
91
+ },
92
+ required: ["templateId", "pageId"],
93
+ },
94
+ },
95
+ {
96
+ name: "delete_funnel_page",
97
+ description: "Deletes a funnel page and closes the resulting order gap in the remaining pages.",
98
+ inputSchema: {
99
+ type: "object",
100
+ properties: {
101
+ templateId: { type: "string", description: "The funnel's home page template id." },
102
+ pageId: { type: "string" },
103
+ },
104
+ required: ["templateId", "pageId"],
105
+ },
106
+ },
107
+ {
108
+ name: "reorder_funnel_pages",
109
+ description: "Reorders a funnel's pages. orderedPageIds must contain exactly this funnel's current page ids, in the new order.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ templateId: { type: "string", description: "The funnel's home page template id." },
114
+ orderedPageIds: { type: "array", items: { type: "string" } },
115
+ },
116
+ required: ["templateId", "orderedPageIds"],
117
+ },
118
+ },
119
+ {
120
+ name: "list_funnel_enrollments",
121
+ description: "Lists a funnel's learner enrollments, most recently enrolled first.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
125
+ required: ["templateId"],
126
+ },
127
+ },
128
+ {
129
+ name: "get_funnel_enrollment",
130
+ description: "Fetches one learner's enrollment plus their per-page progress.",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ templateId: { type: "string", description: "The funnel's home page template id." },
135
+ email: { type: "string" },
136
+ },
137
+ required: ["templateId", "email"],
138
+ },
139
+ },
140
+ {
141
+ name: "approve_funnel_enrollment_page",
142
+ 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.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ templateId: { type: "string", description: "The funnel's home page template id." },
147
+ email: { type: "string" },
148
+ pageId: { type: "string" },
149
+ },
150
+ required: ["templateId", "email", "pageId"],
151
+ },
152
+ },
153
+ {
154
+ name: "set_funnel_enrollment_status",
155
+ description: "Revokes or reactivates a learner's enrollment. Revoked takes effect immediately on their next page load (no session to separately invalidate).",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ templateId: { type: "string", description: "The funnel's home page template id." },
160
+ email: { type: "string" },
161
+ status: { type: "string", enum: ["ACTIVE", "REVOKED"] },
162
+ },
163
+ required: ["templateId", "email", "status"],
164
+ },
165
+ },
166
+ {
167
+ name: "resend_funnel_magic_link",
168
+ description: "Re-sends the passwordless login email to an ACTIVE learner. Rate-limited to once per 60 seconds per learner.",
169
+ inputSchema: {
170
+ type: "object",
171
+ properties: {
172
+ templateId: { type: "string", description: "The funnel's home page template id." },
173
+ email: { type: "string" },
174
+ },
175
+ required: ["templateId", "email"],
176
+ },
177
+ },
178
+ {
179
+ name: "get_funnel_addon_slots",
180
+ 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.",
181
+ inputSchema: { type: "object", properties: {} },
182
+ },
183
+ ];
184
+ exports.LMS_TOOL_NAMES = new Set(exports.LMS_TOOL_DEFS.map((t) => t.name));
185
+ async function handleLmsToolCall(name, args, client) {
186
+ const templateId = typeof args?.templateId === "string" ? args.templateId : "";
187
+ switch (name) {
188
+ case "create_funnel": {
189
+ if (!templateId)
190
+ throw new Error("templateId is required.");
191
+ const result = await client.createFunnel(templateId);
192
+ return { success: true, funnel: result.funnel };
193
+ }
194
+ case "get_funnel":
195
+ return await client.getFunnel(templateId);
196
+ case "update_funnel": {
197
+ const { templateId: _t, ...payload } = args;
198
+ const result = await client.updateFunnel(templateId, payload);
199
+ return { success: true, funnel: result.funnel };
200
+ }
201
+ case "delete_funnel":
202
+ await client.deleteFunnel(templateId);
203
+ return { success: true };
204
+ case "list_funnel_pages":
205
+ return await client.listFunnelPages(templateId);
206
+ case "create_funnel_page": {
207
+ const result = await client.createFunnelPage(templateId, { title: args.title, kind: args.kind });
208
+ return { success: true, page: result.page };
209
+ }
210
+ case "update_funnel_page": {
211
+ if (!args.pageId)
212
+ throw new Error("pageId is required.");
213
+ const { templateId: _t, pageId, ...payload } = args;
214
+ const result = await client.updateFunnelPage(templateId, pageId, payload);
215
+ return { success: true, page: result.page };
216
+ }
217
+ case "delete_funnel_page":
218
+ if (!args.pageId)
219
+ throw new Error("pageId is required.");
220
+ await client.deleteFunnelPage(templateId, args.pageId);
221
+ return { success: true, deletedPageId: args.pageId };
222
+ case "reorder_funnel_pages": {
223
+ if (!Array.isArray(args.orderedPageIds) || args.orderedPageIds.length === 0)
224
+ throw new Error("orderedPageIds is required.");
225
+ return await client.reorderFunnelPages(templateId, args.orderedPageIds);
226
+ }
227
+ case "list_funnel_enrollments":
228
+ return await client.listFunnelEnrollments(templateId);
229
+ case "get_funnel_enrollment":
230
+ if (!args.email)
231
+ throw new Error("email is required.");
232
+ return await client.getFunnelEnrollment(templateId, args.email);
233
+ case "approve_funnel_enrollment_page": {
234
+ if (!args.email || !args.pageId)
235
+ throw new Error("email and pageId are required.");
236
+ const result = await client.approveFunnelEnrollmentPage(templateId, args.email, args.pageId);
237
+ return { success: true, progress: result.progress };
238
+ }
239
+ case "set_funnel_enrollment_status": {
240
+ if (!args.email || !args.status)
241
+ throw new Error("email and status are required.");
242
+ const result = await client.setFunnelEnrollmentStatus(templateId, args.email, args.status);
243
+ return { success: true, enrollment: result.enrollment };
244
+ }
245
+ case "resend_funnel_magic_link":
246
+ if (!args.email)
247
+ throw new Error("email is required.");
248
+ return await client.resendFunnelMagicLink(templateId, args.email);
249
+ case "get_funnel_addon_slots":
250
+ return await client.getFunnelAddonSlots();
251
+ default:
252
+ throw new Error(`Unknown tool: ${name}`);
253
+ }
254
+ }