lotux-mcp 0.6.0 → 0.8.1

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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/index.mjs +215 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,6 +63,8 @@ Restart Claude, then try: *"list my LOTUX brands"*.
63
63
  | `list_landing_pages` / `create_landing_page` | A brand's landing pages + the agency pool; link them with `set_concept_fields` |
64
64
  | `iterate_concept` | Spawn a variation ({root}_Var{n}), the same as the app's Iterate button |
65
65
  | `list_kb_*` / `get_kb_doc` / `create_kb_doc` / `update_kb_doc` | Kennisbank pages |
66
+ | `content_options` / `create_flywheel` / `update_flywheel` / `list_flywheels` / `get_flywheel` | Content flywheels (Marketing → Content, agency-only): one long-form video. Start with `content_options`; give flywheels a `key` so re-runs reuse them |
67
+ | `push_assets` / `add_asset` / `get_asset` / `list_assets` / `update_asset` / `set_asset_status` / `add_comment` / `delete_asset` | Content assets, upserted on their `key`. Statuses: scripting → production → post_production → to_review → revisions → approved → live. Content you write lands in `to_review`; Claude can't set revisions, approve or mark live; deleting signed-off work needs `force`. Each asset carries `raw_footage_url` (raw files folder) and `final_asset_url` (the export — a change sends it to review) |
66
68
 
67
69
  Beats (`add_beat`, `update_beat`, `delete_beat`, `set_beat_translation`, `add_language`) were
68
70
  retired in Sep 2026 — scripts are documents now. Calling one returns a message pointing at the
package/index.mjs CHANGED
@@ -52,7 +52,7 @@ const ok = (result) => ({
52
52
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
53
53
  });
54
54
 
55
- const server = new McpServer({ name: "lotux", version: "0.6.0" });
55
+ const server = new McpServer({ name: "lotux", version: "0.8.1" });
56
56
 
57
57
  // ── Reads ─────────────────────────────────────────────────────────────────────
58
58
 
@@ -454,6 +454,220 @@ server.tool(
454
454
  async (args) => ok(await rpc("update_kb_doc", args))
455
455
  );
456
456
 
457
+ // ── Content flywheels (Marketing → Content, agency-only) ─────────────────────
458
+ // A flywheel = one long-form YouTube video + every asset made from it. LOTUX
459
+ // only TRACKS content: creating a flywheel creates no assets — your skills push
460
+ // them (push_assets / add_asset). Anything you write lands in "to_review"; a
461
+ // human approves in LOTUX. Built for re-runs: give flywheels and assets a stable
462
+ // `key` (flywheel = the video's content folder, asset = e.g. "clip-3") and every
463
+ // push updates instead of duplicating. account_id is optional everywhere
464
+ // (defaults to your agency). Call content_options first for the allowed values.
465
+
466
+ const PLATFORMS = ["youtube", "youtube_shorts", "instagram", "tiktok", "linkedin", "x", "email", "meta_ads", "opt_in_page", "google_doc", "internal"];
467
+ // Statuses follow production. You may set the first four; revisions,
468
+ // approved and live are a human's call.
469
+ const STATUSES = ["scripting", "production", "post_production", "to_review", "revisions", "approved", "live"];
470
+ const CLAUDE_STATUSES = ["scripting", "production", "post_production", "to_review"];
471
+ // video = "To long form".
472
+ const CTAS = ["none", "video", "lead_magnet", "dm_keyword", "booking", "follow"];
473
+ // long_form carries the YouTube pieces: title = YouTube title, body =
474
+ // description + chapters, cover_url = thumbnail. clip / creator_video: cover_url
475
+ // = reel cover.
476
+ const ASSET_TYPES = ["long_form", "clip", "creator_video", "story_sequence", "carousel", "post", "email", "vsl_ad", "follower_ad", "lead_magnet_ad", "lead_magnet_page", "other"];
477
+ const attachment = z.object({ url: z.string(), label: z.string().optional() });
478
+ const flywheelRef = {
479
+ flywheel_id: z.string().optional(),
480
+ flywheel_key: z.string().optional().describe("The key you gave the flywheel (instead of flywheel_id)"),
481
+ };
482
+ const assetRef = {
483
+ asset_id: z.string().optional(),
484
+ key: z.string().optional().describe("The asset's key — with flywheel_id or flywheel_key — instead of asset_id"),
485
+ ...flywheelRef,
486
+ };
487
+ // Every asset field. Omitted = left as is (on update) or the type's default (on create).
488
+ const assetFields = {
489
+ asset_type: z.enum(ASSET_TYPES).optional(),
490
+ title: z.string().optional().describe("Card title. long_form: the YouTube title"),
491
+ platform: z.enum(PLATFORMS).optional().describe("Defaults from the type"),
492
+ cross_post: z.array(z.enum(PLATFORMS)).optional().describe("Also posted on (defaults from the type)"),
493
+ account: z.enum(["personal", "company"]).optional().describe("personal = Niels, company = the agency"),
494
+ cta: z.enum(CTAS).optional(),
495
+ dm_keyword: z.string().optional(),
496
+ dm_reply: z.enum(["booking", "lead_magnet"]).optional().describe("For a dm_keyword CTA: what the DM reply sends"),
497
+ destination_url: z.string().optional().describe("Override the flywheel URL this CTA would use"),
498
+ posting_date: z.string().optional().describe("YYYY-MM-DD"),
499
+ deadline: z.string().optional().describe("YYYY-MM-DD; defaults to posting date − the agency's lead days"),
500
+ owner: z.string().optional().describe('"claude", "none" or a profile id (see content_options.owners)'),
501
+ status: z.enum(CLAUDE_STATUSES).optional(),
502
+ must_post: z.boolean().optional(),
503
+ body: z.string().optional().describe("Markdown content. long_form: YouTube description + chapters"),
504
+ attachments: z.array(attachment).optional().describe("Replaces all links (Figma, Drive, Dropbox…)"),
505
+ cover_url: z.string().optional().describe("long_form: thumbnail · clip / creator_video: reel cover"),
506
+ raw_footage_url: z.string().optional().describe("Folder with the raw footage this piece is cut from (Dropbox / Drive). Several assets may share one"),
507
+ final_asset_url: z.string().optional().describe("The exported deliverable (file or folder link). Signed off with the asset"),
508
+ published_url: z.string().optional(),
509
+ notes: z.string().optional(),
510
+ };
511
+
512
+ server.tool(
513
+ "content_options",
514
+ "Allowed values for content: asset types (with their default platform / cross-posts / CTA and what body + cover_url mean), platforms, CTAs, DM reply targets, accounts, statuses (and which you may set), owners (team members + claude), the default deadline lead and the rules. Call this first.",
515
+ { account_id: z.string().optional() },
516
+ async (args) => ok(await rpc("content_options", args))
517
+ );
518
+
519
+ server.tool(
520
+ "create_flywheel",
521
+ "Create a content flywheel for a long-form YouTube video. With a key that already exists you get that flywheel back (created: false) — safe to re-run. Creates no assets. Returns { id, created }.",
522
+ {
523
+ key: z.string().optional().describe("Your stable id for this video, e.g. the content folder '2026-10 Claude Code'"),
524
+ title: z.string().describe("Video title"),
525
+ d0: z.string().describe("Long-form publish date, YYYY-MM-DD"),
526
+ topic: z.string().optional(),
527
+ youtube_url: z.string().optional(),
528
+ lead_magnet_url: z.string().optional().describe("Lead magnet page: email form that redirects to the video"),
529
+ booking_url: z.string().optional(),
530
+ transcript: z.string().optional().describe("Full transcript text"),
531
+ notes: z.string().optional(),
532
+ account_id: z.string().optional(),
533
+ },
534
+ async (args) => ok(await rpc("create_flywheel", args))
535
+ );
536
+
537
+ server.tool(
538
+ "update_flywheel",
539
+ "Update a flywheel: title, topic, status, URLs, transcript, notes. Changing d0 moves every asset that isn't live by the same number of days. Omitted fields are left unchanged.",
540
+ {
541
+ ...flywheelRef,
542
+ title: z.string().optional(),
543
+ topic: z.string().optional(),
544
+ d0: z.string().optional().describe("YYYY-MM-DD — shifts the assets with it"),
545
+ status: z.enum(["planning", "production", "live", "done"]).optional(),
546
+ youtube_url: z.string().optional(),
547
+ lead_magnet_url: z.string().optional(),
548
+ booking_url: z.string().optional(),
549
+ transcript: z.string().optional(),
550
+ notes: z.string().optional(),
551
+ new_key: z.string().optional().describe("Give the flywheel a (new) key"),
552
+ account_id: z.string().optional(),
553
+ },
554
+ async (args) => ok(await rpc("update_flywheel", args))
555
+ );
556
+
557
+ server.tool(
558
+ "list_flywheels",
559
+ "List content flywheels with progress (approved / total), next deadline and overdue count. Filter by status, a title/key search, or an exact key.",
560
+ {
561
+ status: z.enum(["planning", "production", "live", "done"]).optional(),
562
+ query: z.string().optional().describe("Matches the title or key"),
563
+ key: z.string().optional(),
564
+ account_id: z.string().optional(),
565
+ },
566
+ async (args) => ok(await rpc("list_flywheels", args))
567
+ );
568
+
569
+ server.tool(
570
+ "get_flywheel",
571
+ "One flywheel and all its assets (each with `link` = where its CTA / DM reply points, and `warnings`). Bodies and the transcript only when asked.",
572
+ {
573
+ ...flywheelRef,
574
+ include_bodies: z.boolean().optional(),
575
+ include_transcript: z.boolean().optional(),
576
+ account_id: z.string().optional(),
577
+ },
578
+ async (args) => ok(await rpc("get_flywheel", args))
579
+ );
580
+
581
+ server.tool(
582
+ "push_assets",
583
+ "Push many assets into a flywheel in one call (max 100). Each item is upserted on its `key`: new keys are created (asset_type + title required), existing keys are updated with only the fields you send. Content lands in to_review. Returns counts + per-item { key, id, created, status, link, warnings } or { key, error }.",
584
+ {
585
+ ...flywheelRef,
586
+ assets: z.array(z.object({ key: z.string().optional(), ...assetFields })).max(100),
587
+ account_id: z.string().optional(),
588
+ },
589
+ async (args) => ok(await rpc("push_assets", args))
590
+ );
591
+
592
+ server.tool(
593
+ "add_asset",
594
+ "Push one asset. With a key that exists on the flywheel it updates that asset instead. The type fills platform, cross-posts and CTA when you leave them out. With content it lands in to_review, owned by Claude; without, as an unassigned slot in scripting.",
595
+ {
596
+ ...flywheelRef,
597
+ key: z.string().optional(),
598
+ ...assetFields,
599
+ account_id: z.string().optional(),
600
+ },
601
+ async (args) => ok(await rpc("add_asset", args))
602
+ );
603
+
604
+ server.tool(
605
+ "get_asset",
606
+ "One asset in full — body, attachments, cover/thumbnail, raw footage + final asset links, resolved link, warnings — plus its activity (status changes and comments, newest first). Use it to read what a human asked to change.",
607
+ { ...assetRef, account_id: z.string().optional() },
608
+ async (args) => ok(await rpc("get_asset", args))
609
+ );
610
+
611
+ server.tool(
612
+ "list_assets",
613
+ "Assets across flywheels with filters. Find work: status revisions (a human asked for changes), scripting + owner claude, posting_to next week, overdue.",
614
+ {
615
+ ...flywheelRef,
616
+ key: z.string().optional(),
617
+ asset_type: z.array(z.enum(ASSET_TYPES)).optional(),
618
+ status: z.array(z.enum(STATUSES)).optional(),
619
+ platform: z.array(z.enum(PLATFORMS)).optional().describe("Matches the main platform or a cross-post"),
620
+ account: z.enum(["personal", "company"]).optional(),
621
+ owner: z.string().optional().describe('"claude", "none" (unassigned) or a profile id'),
622
+ overdue: z.boolean().optional(),
623
+ posting_from: z.string().optional().describe("YYYY-MM-DD"),
624
+ posting_to: z.string().optional().describe("YYYY-MM-DD"),
625
+ include_bodies: z.boolean().optional(),
626
+ limit: z.number().int().min(1).max(500).optional(),
627
+ account_id: z.string().optional(),
628
+ },
629
+ async (args) => ok(await rpc("list_assets", args))
630
+ );
631
+
632
+ server.tool(
633
+ "update_asset",
634
+ "Update an asset by asset_id (or flywheel + key). Changing body, attachments, cover_url or final_asset_url moves it to to_review (even if it was approved) so a human signs it off. Omitted fields are left unchanged.",
635
+ {
636
+ ...assetRef,
637
+ ...assetFields,
638
+ add_attachments: z.array(attachment).optional().describe("Appends links"),
639
+ new_key: z.string().optional().describe("Give the asset a (new) key"),
640
+ account_id: z.string().optional(),
641
+ },
642
+ async (args) => ok(await rpc("update_asset", args))
643
+ );
644
+
645
+ server.tool(
646
+ "set_asset_status",
647
+ "Move an asset through scripting, production, post_production or to_review (to_review hands it to a human). Revisions, approved and live are a human's call in LOTUX.",
648
+ {
649
+ ...assetRef,
650
+ status: z.enum(CLAUDE_STATUSES),
651
+ comment: z.string().optional().describe("Shown in the asset's activity"),
652
+ account_id: z.string().optional(),
653
+ },
654
+ async (args) => ok(await rpc("set_asset_status", args))
655
+ );
656
+
657
+ server.tool(
658
+ "add_comment",
659
+ "Leave a comment on an asset (shown in its activity in LOTUX).",
660
+ { ...assetRef, comment: z.string(), account_id: z.string().optional() },
661
+ async (args) => ok(await rpc("add_comment", args))
662
+ );
663
+
664
+ server.tool(
665
+ "delete_asset",
666
+ "Delete an asset. An approved or live one needs force: true.",
667
+ { ...assetRef, force: z.boolean().optional(), account_id: z.string().optional() },
668
+ async (args) => ok(await rpc("delete_asset", args))
669
+ );
670
+
457
671
  async function main() {
458
672
  const transport = new StdioServerTransport();
459
673
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lotux-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "description": "Local MCP server that lets Claude create LOTUX concepts (fetch brand-DB entities, create concepts, fill scripts + copy). Talks to the LOTUX app with a personal token; no business logic or secrets live here.",
6
6
  "bin": {