@charisol/plexo-mcp 1.0.8 → 1.0.10

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,8 @@ 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");
9
11
  const server = new index_js_1.Server({
10
12
  name: "plexo-mcp",
11
13
  version: "1.0.0",
@@ -278,6 +280,8 @@ Map the returned tokens into designJson style fields: color-type tokens into bac
278
280
  name: "update_template",
279
281
  description: `Edits an existing landing page or email template in place, under the same template ID. Recompiles and re-saves the HTML from the new designJson — the editable URL stays the same, and for a published landing page the live URL and domain are unchanged too (this does not publish/unpublish or change the domain; call publish_landing_page again with a domain for that).
280
282
 
283
+ This is for drag-and-drop BUILDER pages only — it REJECTS a page created via create_raw_landing_page/publish_raw_landing_page/create_landing_page_subpage's htmlContent option. Use update_raw_landing_page for those instead. If unsure which kind an existing page is, check its sourceType via list_landing_pages/get_landing_page_pages first.
284
+
281
285
  Uses the SAME fully-hydrated layout schema as publish_landing_page/create_email_template — designJson MUST be { "body": { "style": {...}, "rows": [{ "id", "style", "columns": [{ "id", "width", "elements": [{ "id", "type", "style", "attributes" }] }] }] } }. There is no shorthand row format; rows without a 'columns' array are rejected, and this replaces the ENTIRE designJson — to edit one section, fetch the current template's designJson first (via list_landing_pages/list_email_templates, or the Plexo dashboard) and send back the full modified tree, not just the changed part. See publish_landing_page's description for a full worked example.`,
282
286
  inputSchema: {
283
287
  type: "object",
@@ -302,6 +306,30 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page/create_email_
302
306
  required: ["templateId", "designJson"],
303
307
  },
304
308
  },
309
+ {
310
+ name: "update_raw_landing_page",
311
+ description: `Replaces an existing RAW_UPLOAD page's HTML in place, under the same template ID — the editable URL, live URL, and domain (if published) are all unchanged. This is for pages created via create_raw_landing_page/publish_raw_landing_page/create_landing_page_subpage's htmlContent option ONLY — it REJECTS a drag-and-drop BUILDER page (use update_template for those). If unsure which kind an existing page is, check its sourceType via list_landing_pages/get_landing_page_pages first.
312
+
313
+ This replaces the ENTIRE HTML document, not a patch — send back the full modified document. Same 1MB single-document scope as create_raw_landing_page.`,
314
+ inputSchema: {
315
+ type: "object",
316
+ properties: {
317
+ templateId: {
318
+ type: "string",
319
+ description: "ID of the raw-upload page to update — the templateId returned by create_raw_landing_page, publish_raw_landing_page, list_landing_pages, or get_landing_page_pages.",
320
+ },
321
+ htmlContent: {
322
+ type: "string",
323
+ description: "The FULL replacement raw HTML document (up to 1MB) — not a partial patch.",
324
+ },
325
+ name: {
326
+ type: "string",
327
+ description: "Optional new title for the page. Omit to keep the existing name.",
328
+ },
329
+ },
330
+ required: ["templateId", "htmlContent"],
331
+ },
332
+ },
305
333
  {
306
334
  name: "create_landing_page",
307
335
  description: `Creates and saves a landing page template WITHOUT publishing it to a domain — use this when the user wants to build a page (or start a multi-page site) first and go live later. Call publish_existing_landing_page afterward with the returned templateId to link a subdomain or custom domain whenever they're ready. To create a page AND publish it in one step, use publish_landing_page instead.
@@ -322,6 +350,63 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page — see its d
322
350
  required: ["designJson"],
323
351
  },
324
352
  },
353
+ {
354
+ name: "create_raw_landing_page",
355
+ description: `Creates a landing page from a RAW HTML document you supply directly, WITHOUT publishing it to a domain — for when the user wants a page built from hand-written or externally-sourced HTML/CSS/JS instead of Plexo's drag-and-drop builder. The page is served byte-for-byte as given, completely unsanitized (no designJson, no compilation) — this is a DIFFERENT storage mode from create_landing_page/publish_landing_page/update_template, which are for the drag-and-drop BUILDER pages built from designJson. Do NOT use this tool for a page you want to build with designJson — use create_landing_page instead.
356
+
357
+ Scope: a single self-contained HTML document only (inline or CDN-linked CSS/JS), up to 1MB — no zip/multi-file sites with separate local asset files (that's a dashboard-only "import a website" feature with no MCP equivalent yet).
358
+
359
+ Because this serves unsanitized content, the account must accept the Acceptable Use Policy once — see the acceptAup argument. Call publish_existing_landing_page afterward with the returned templateId to link a subdomain or custom domain, or use publish_raw_landing_page to create and publish in one step. To edit this page's HTML later, use update_raw_landing_page (NOT update_template — that tool rejects raw-upload pages).`,
360
+ inputSchema: {
361
+ type: "object",
362
+ properties: {
363
+ name: {
364
+ type: "string",
365
+ description: "Title of the landing page (e.g. 'Kicks Store').",
366
+ },
367
+ htmlContent: {
368
+ type: "string",
369
+ description: "The full, self-contained raw HTML document to serve as-is (up to 1MB). Inline or CDN-linked CSS/JS only — no separate local asset files.",
370
+ },
371
+ acceptAup: {
372
+ type: "boolean",
373
+ description: "Pass true ONLY after explicitly telling the user this publishes unsanitized HTML/CSS/JS as-is and getting their confirmation of the Acceptable Use Policy (/legal/acceptable-use). Not needed once the account has accepted it once before.",
374
+ },
375
+ },
376
+ required: ["htmlContent"],
377
+ },
378
+ },
379
+ {
380
+ name: "publish_raw_landing_page",
381
+ description: `Creates, and publishes to a domain, a landing page from a RAW HTML document you supply directly — the combined create+publish equivalent of create_raw_landing_page. See create_raw_landing_page's description for the unsanitized-content, AUP, and scope caveats (single self-contained HTML document, up to 1MB, no zip/multi-file sites). For drag-and-drop BUILDER pages built from designJson, use publish_landing_page instead — this tool is a different storage mode.`,
382
+ inputSchema: {
383
+ type: "object",
384
+ properties: {
385
+ name: {
386
+ type: "string",
387
+ description: "Title of the landing page (e.g. 'Kicks Store').",
388
+ },
389
+ htmlContent: {
390
+ type: "string",
391
+ description: "The full, self-contained raw HTML document to serve as-is (up to 1MB). Inline or CDN-linked CSS/JS only — no separate local asset files.",
392
+ },
393
+ acceptAup: {
394
+ type: "boolean",
395
+ description: "Pass true ONLY after explicitly telling the user this publishes unsanitized HTML/CSS/JS as-is and getting their confirmation of the Acceptable Use Policy (/legal/acceptable-use). Not needed once the account has accepted it once before.",
396
+ },
397
+ domain: {
398
+ type: "string",
399
+ description: "Subdomain slug (e.g. 'kicks') or custom domain ('kicks.com').",
400
+ },
401
+ type: {
402
+ type: "string",
403
+ enum: ["SUBDOMAIN", "CUSTOM"],
404
+ description: "Domain routing type. Inferred from the domain string if omitted.",
405
+ },
406
+ },
407
+ required: ["htmlContent", "domain"],
408
+ },
409
+ },
325
410
  {
326
411
  name: "publish_existing_landing_page",
327
412
  description: `Links a subdomain or custom domain to an already-existing landing page template — for a template created via create_landing_page, or the home page of a multi-page site (see create_landing_page_subpage) once its sub-pages are ready. Unlike publish_landing_page, this does NOT create a new template — it publishes the one you already have. Every sub-page nested under templateId automatically becomes reachable at this domain too (e.g. domain.com/about) — no separate publish call is needed per sub-page.`,
@@ -347,9 +432,9 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page — see its d
347
432
  },
348
433
  {
349
434
  name: "create_landing_page_subpage",
350
- description: `Adds a new page nested under an existing landing page, reachable at <site domain>/<slug> — or nested deeper (e.g. /blog/post-1) if parentTemplateId is itself a sub-page rather than the home page. This is how a single landing page becomes a multi-page site. No separate publish step is needed for sub-pages: they automatically become reachable once the site's home page has a domain linked (see publish_existing_landing_page). Requires an Ultra subscription plan — sub-pages are otherwise unlimited per site once enabled.
435
+ description: `Adds a new page nested under an existing landing page, reachable at <site domain>/<slug> — or nested deeper (e.g. /blog/post-1) if parentTemplateId is itself a sub-page rather than the home page. This is how a single landing page becomes a multi-page site (BUILDER and RAW_UPLOAD pages can be freely mixed within the same site — each page's own sourceType is independent of its siblings). No separate publish step is needed for sub-pages: they automatically become reachable once the site's home page has a domain linked (see publish_existing_landing_page). Requires an Ultra subscription plan — sub-pages are otherwise unlimited per site once enabled.
351
436
 
352
- The new page starts blank — use update_template with the returned templateId to add content.`,
437
+ Provide htmlContent for a raw-HTML sub-page (same unsanitized/AUP/1MB caveats as create_raw_landing_page). Omit it to create a blank BUILDER page — fill it in afterward with update_template using the returned templateId. Edit an existing sub-page's content afterward with update_template (BUILDER) or update_raw_landing_page (RAW_UPLOAD) depending on which mode it was created with.`,
353
438
  inputSchema: {
354
439
  type: "object",
355
440
  properties: {
@@ -365,6 +450,14 @@ The new page starts blank — use update_template with the returned templateId t
365
450
  type: "string",
366
451
  description: "Optional URL segment (e.g. 'about-us'). Auto-generated from name if omitted; auto-suffixed (about-us-2, ...) if it collides with a sibling page under the same parent.",
367
452
  },
453
+ htmlContent: {
454
+ type: "string",
455
+ description: "Optional. A full, self-contained raw HTML document (up to 1MB) to serve as-is. Creates a RAW_UPLOAD sub-page instead of a blank BUILDER one.",
456
+ },
457
+ acceptAup: {
458
+ type: "boolean",
459
+ description: "Only relevant when htmlContent is given. Pass true ONLY after explicitly telling the user this publishes unsanitized HTML/CSS/JS as-is and getting their confirmation of the Acceptable Use Policy (/legal/acceptable-use). Not needed once the account has accepted it once before.",
460
+ },
368
461
  },
369
462
  required: ["parentTemplateId", "name"],
370
463
  },
@@ -415,6 +508,23 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
415
508
  required: ["templateId"],
416
509
  },
417
510
  },
511
+ {
512
+ name: "delete_template",
513
+ 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.",
514
+ inputSchema: {
515
+ type: "object",
516
+ properties: {
517
+ templateId: {
518
+ type: "string",
519
+ description: "ID of the template or page to delete (UUID, or exact/partial template name like 'buzm').",
520
+ },
521
+ name: {
522
+ type: "string",
523
+ description: "Optional template name or search phrase if templateId is unknown (e.g. 'buzm').",
524
+ },
525
+ },
526
+ },
527
+ },
418
528
  {
419
529
  name: "delete_landing_page_page",
420
530
  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.",
@@ -498,6 +608,8 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
498
608
  },
499
609
  },
500
610
  ...blogTools_js_1.BLOG_TOOL_DEFS,
611
+ ...commerceTools_js_1.COMMERCE_TOOL_DEFS,
612
+ ...lmsTools_js_1.LMS_TOOL_DEFS,
501
613
  ],
502
614
  };
503
615
  });
@@ -562,6 +674,39 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
562
674
  ],
563
675
  };
564
676
  }
677
+ case "create_raw_landing_page": {
678
+ const result = await client.createRawLandingPage(args);
679
+ return {
680
+ content: [
681
+ {
682
+ type: "text",
683
+ text: JSON.stringify(result, null, 2),
684
+ },
685
+ ],
686
+ };
687
+ }
688
+ case "publish_raw_landing_page": {
689
+ const result = await client.publishRawLandingPage(args);
690
+ return {
691
+ content: [
692
+ {
693
+ type: "text",
694
+ text: JSON.stringify(result, null, 2),
695
+ },
696
+ ],
697
+ };
698
+ }
699
+ case "update_raw_landing_page": {
700
+ const result = await client.updateRawLandingPage(args);
701
+ return {
702
+ content: [
703
+ {
704
+ type: "text",
705
+ text: JSON.stringify(result, null, 2),
706
+ },
707
+ ],
708
+ };
709
+ }
565
710
  case "publish_existing_landing_page": {
566
711
  const result = await client.publishExistingLandingPage(args);
567
712
  return {
@@ -606,8 +751,26 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
606
751
  ],
607
752
  };
608
753
  }
754
+ case "delete_template":
609
755
  case "delete_landing_page_page": {
610
- const result = await client.deleteLandingPagePage(args?.templateId);
756
+ const rawInput = (args?.templateId || args?.name || "").trim();
757
+ if (!rawInput)
758
+ throw new Error("templateId or template name is required.");
759
+ // Same UUID-or-name resolution as the server's own delete_template/
760
+ // delete_landing_page_page handler — try an exact id match first, then fall back to
761
+ // a case-insensitive name search across this account's templates.
762
+ let resolvedId = rawInput;
763
+ const { templates } = await client.listTemplates();
764
+ const byId = (templates || []).find((t) => t.id === rawInput);
765
+ if (!byId) {
766
+ const needle = rawInput.toLowerCase();
767
+ const matches = (templates || []).filter((t) => t.name?.toLowerCase().includes(needle));
768
+ matches.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
769
+ if (!matches[0])
770
+ throw new Error(`No template found matching "${rawInput}".`);
771
+ resolvedId = matches[0].id;
772
+ }
773
+ const result = await client.deleteLandingPagePage(resolvedId);
611
774
  return {
612
775
  content: [
613
776
  {
@@ -701,6 +864,28 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
701
864
  ],
702
865
  };
703
866
  }
867
+ if (commerceTools_js_1.COMMERCE_TOOL_NAMES.has(name)) {
868
+ const result = await (0, commerceTools_js_1.handleCommerceToolCall)(name, args || {}, client);
869
+ return {
870
+ content: [
871
+ {
872
+ type: "text",
873
+ text: JSON.stringify(result, null, 2),
874
+ },
875
+ ],
876
+ };
877
+ }
878
+ if (lmsTools_js_1.LMS_TOOL_NAMES.has(name)) {
879
+ const result = await (0, lmsTools_js_1.handleLmsToolCall)(name, args || {}, client);
880
+ return {
881
+ content: [
882
+ {
883
+ type: "text",
884
+ text: JSON.stringify(result, null, 2),
885
+ },
886
+ ],
887
+ };
888
+ }
704
889
  throw new Error(`Unknown tool: ${name}`);
705
890
  }
706
891
  }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charisol/plexo-mcp",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Model Context Protocol (MCP) Server for Plexo - Landing page generation, email templates, publishing, and analytics.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {