@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/client/plexoClient.js +266 -3
- package/dist/commerceTools.js +500 -0
- package/dist/index.js +188 -3
- package/dist/lmsTools.js +254 -0
- package/package.json +1 -1
- package/src/client/plexoClient.ts +313 -3
- package/src/commerceTools.ts +516 -0
- package/src/index.ts +191 -3
- package/src/lmsTools.ts +262 -0
package/src/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ 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";
|
|
11
13
|
|
|
12
14
|
const server = new Server(
|
|
13
15
|
{
|
|
@@ -285,6 +287,8 @@ Map the returned tokens into designJson style fields: color-type tokens into bac
|
|
|
285
287
|
name: "update_template",
|
|
286
288
|
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).
|
|
287
289
|
|
|
290
|
+
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.
|
|
291
|
+
|
|
288
292
|
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.`,
|
|
289
293
|
inputSchema: {
|
|
290
294
|
type: "object",
|
|
@@ -309,6 +313,30 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page/create_email_
|
|
|
309
313
|
required: ["templateId", "designJson"],
|
|
310
314
|
},
|
|
311
315
|
},
|
|
316
|
+
{
|
|
317
|
+
name: "update_raw_landing_page",
|
|
318
|
+
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.
|
|
319
|
+
|
|
320
|
+
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.`,
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: {
|
|
324
|
+
templateId: {
|
|
325
|
+
type: "string",
|
|
326
|
+
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.",
|
|
327
|
+
},
|
|
328
|
+
htmlContent: {
|
|
329
|
+
type: "string",
|
|
330
|
+
description: "The FULL replacement raw HTML document (up to 1MB) — not a partial patch.",
|
|
331
|
+
},
|
|
332
|
+
name: {
|
|
333
|
+
type: "string",
|
|
334
|
+
description: "Optional new title for the page. Omit to keep the existing name.",
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
required: ["templateId", "htmlContent"],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
312
340
|
{
|
|
313
341
|
name: "create_landing_page",
|
|
314
342
|
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.
|
|
@@ -329,6 +357,63 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page — see its d
|
|
|
329
357
|
required: ["designJson"],
|
|
330
358
|
},
|
|
331
359
|
},
|
|
360
|
+
{
|
|
361
|
+
name: "create_raw_landing_page",
|
|
362
|
+
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.
|
|
363
|
+
|
|
364
|
+
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).
|
|
365
|
+
|
|
366
|
+
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).`,
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {
|
|
370
|
+
name: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description: "Title of the landing page (e.g. 'Kicks Store').",
|
|
373
|
+
},
|
|
374
|
+
htmlContent: {
|
|
375
|
+
type: "string",
|
|
376
|
+
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.",
|
|
377
|
+
},
|
|
378
|
+
acceptAup: {
|
|
379
|
+
type: "boolean",
|
|
380
|
+
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.",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
required: ["htmlContent"],
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: "publish_raw_landing_page",
|
|
388
|
+
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.`,
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: "object",
|
|
391
|
+
properties: {
|
|
392
|
+
name: {
|
|
393
|
+
type: "string",
|
|
394
|
+
description: "Title of the landing page (e.g. 'Kicks Store').",
|
|
395
|
+
},
|
|
396
|
+
htmlContent: {
|
|
397
|
+
type: "string",
|
|
398
|
+
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.",
|
|
399
|
+
},
|
|
400
|
+
acceptAup: {
|
|
401
|
+
type: "boolean",
|
|
402
|
+
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.",
|
|
403
|
+
},
|
|
404
|
+
domain: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "Subdomain slug (e.g. 'kicks') or custom domain ('kicks.com').",
|
|
407
|
+
},
|
|
408
|
+
type: {
|
|
409
|
+
type: "string",
|
|
410
|
+
enum: ["SUBDOMAIN", "CUSTOM"],
|
|
411
|
+
description: "Domain routing type. Inferred from the domain string if omitted.",
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
required: ["htmlContent", "domain"],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
332
417
|
{
|
|
333
418
|
name: "publish_existing_landing_page",
|
|
334
419
|
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.`,
|
|
@@ -354,9 +439,9 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page — see its d
|
|
|
354
439
|
},
|
|
355
440
|
{
|
|
356
441
|
name: "create_landing_page_subpage",
|
|
357
|
-
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.
|
|
442
|
+
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.
|
|
358
443
|
|
|
359
|
-
|
|
444
|
+
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.`,
|
|
360
445
|
inputSchema: {
|
|
361
446
|
type: "object",
|
|
362
447
|
properties: {
|
|
@@ -372,6 +457,14 @@ The new page starts blank — use update_template with the returned templateId t
|
|
|
372
457
|
type: "string",
|
|
373
458
|
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.",
|
|
374
459
|
},
|
|
460
|
+
htmlContent: {
|
|
461
|
+
type: "string",
|
|
462
|
+
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.",
|
|
463
|
+
},
|
|
464
|
+
acceptAup: {
|
|
465
|
+
type: "boolean",
|
|
466
|
+
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.",
|
|
467
|
+
},
|
|
375
468
|
},
|
|
376
469
|
required: ["parentTemplateId", "name"],
|
|
377
470
|
},
|
|
@@ -422,6 +515,23 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
|
|
|
422
515
|
required: ["templateId"],
|
|
423
516
|
},
|
|
424
517
|
},
|
|
518
|
+
{
|
|
519
|
+
name: "delete_template",
|
|
520
|
+
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.",
|
|
521
|
+
inputSchema: {
|
|
522
|
+
type: "object",
|
|
523
|
+
properties: {
|
|
524
|
+
templateId: {
|
|
525
|
+
type: "string",
|
|
526
|
+
description: "ID of the template or page to delete (UUID, or exact/partial template name like 'buzm').",
|
|
527
|
+
},
|
|
528
|
+
name: {
|
|
529
|
+
type: "string",
|
|
530
|
+
description: "Optional template name or search phrase if templateId is unknown (e.g. 'buzm').",
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
},
|
|
425
535
|
{
|
|
426
536
|
name: "delete_landing_page_page",
|
|
427
537
|
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.",
|
|
@@ -505,6 +615,8 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
|
|
|
505
615
|
},
|
|
506
616
|
},
|
|
507
617
|
...BLOG_TOOL_DEFS,
|
|
618
|
+
...COMMERCE_TOOL_DEFS,
|
|
619
|
+
...LMS_TOOL_DEFS,
|
|
508
620
|
],
|
|
509
621
|
};
|
|
510
622
|
});
|
|
@@ -576,6 +688,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
576
688
|
};
|
|
577
689
|
}
|
|
578
690
|
|
|
691
|
+
case "create_raw_landing_page": {
|
|
692
|
+
const result = await client.createRawLandingPage(args as any);
|
|
693
|
+
return {
|
|
694
|
+
content: [
|
|
695
|
+
{
|
|
696
|
+
type: "text",
|
|
697
|
+
text: JSON.stringify(result, null, 2),
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
case "publish_raw_landing_page": {
|
|
704
|
+
const result = await client.publishRawLandingPage(args as any);
|
|
705
|
+
return {
|
|
706
|
+
content: [
|
|
707
|
+
{
|
|
708
|
+
type: "text",
|
|
709
|
+
text: JSON.stringify(result, null, 2),
|
|
710
|
+
},
|
|
711
|
+
],
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
case "update_raw_landing_page": {
|
|
716
|
+
const result = await client.updateRawLandingPage(args as any);
|
|
717
|
+
return {
|
|
718
|
+
content: [
|
|
719
|
+
{
|
|
720
|
+
type: "text",
|
|
721
|
+
text: JSON.stringify(result, null, 2),
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
579
727
|
case "publish_existing_landing_page": {
|
|
580
728
|
const result = await client.publishExistingLandingPage(args as any);
|
|
581
729
|
return {
|
|
@@ -624,8 +772,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
624
772
|
};
|
|
625
773
|
}
|
|
626
774
|
|
|
775
|
+
case "delete_template":
|
|
627
776
|
case "delete_landing_page_page": {
|
|
628
|
-
const
|
|
777
|
+
const rawInput = ((args as any)?.templateId || (args as any)?.name || "").trim();
|
|
778
|
+
if (!rawInput) throw new Error("templateId or template name is required.");
|
|
779
|
+
|
|
780
|
+
// Same UUID-or-name resolution as the server's own delete_template/
|
|
781
|
+
// delete_landing_page_page handler — try an exact id match first, then fall back to
|
|
782
|
+
// a case-insensitive name search across this account's templates.
|
|
783
|
+
let resolvedId = rawInput;
|
|
784
|
+
const { templates } = await client.listTemplates();
|
|
785
|
+
const byId = (templates || []).find((t: any) => t.id === rawInput);
|
|
786
|
+
if (!byId) {
|
|
787
|
+
const needle = rawInput.toLowerCase();
|
|
788
|
+
const matches = (templates || []).filter((t: any) => t.name?.toLowerCase().includes(needle));
|
|
789
|
+
matches.sort((a: any, b: any) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
790
|
+
if (!matches[0]) throw new Error(`No template found matching "${rawInput}".`);
|
|
791
|
+
resolvedId = matches[0].id;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const result = await client.deleteLandingPagePage(resolvedId);
|
|
629
795
|
return {
|
|
630
796
|
content: [
|
|
631
797
|
{
|
|
@@ -734,6 +900,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
734
900
|
],
|
|
735
901
|
};
|
|
736
902
|
}
|
|
903
|
+
if (COMMERCE_TOOL_NAMES.has(name)) {
|
|
904
|
+
const result = await handleCommerceToolCall(name, (args as any) || {}, client);
|
|
905
|
+
return {
|
|
906
|
+
content: [
|
|
907
|
+
{
|
|
908
|
+
type: "text",
|
|
909
|
+
text: JSON.stringify(result, null, 2),
|
|
910
|
+
},
|
|
911
|
+
],
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (LMS_TOOL_NAMES.has(name)) {
|
|
915
|
+
const result = await handleLmsToolCall(name, (args as any) || {}, client);
|
|
916
|
+
return {
|
|
917
|
+
content: [
|
|
918
|
+
{
|
|
919
|
+
type: "text",
|
|
920
|
+
text: JSON.stringify(result, null, 2),
|
|
921
|
+
},
|
|
922
|
+
],
|
|
923
|
+
};
|
|
924
|
+
}
|
|
737
925
|
throw new Error(`Unknown tool: ${name}`);
|
|
738
926
|
}
|
|
739
927
|
}
|
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
|
+
}
|