lotux-mcp 0.5.1 → 0.6.0

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 +3 -1
  2. package/index.mjs +90 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -59,7 +59,9 @@ Restart Claude, then try: *"list my LOTUX brands"*.
59
59
  | `save_script` | Write a script: markdown body for one language (`- **Hook:** …`, `- **1:** …`, `- **CTA:** …`), the shotlist, a rename. Pass `language` for a translation |
60
60
  | `add_script` / `delete_script` / `remove_script_language` / `set_base_language` | More scripts per concept; language housekeeping |
61
61
  | `set_beats` / `set_beat_translations` | **Compatibility** for older prompts: a beat list is stored as the script document. Prefer `save_script` |
62
- | `list_copy` / `create_copy` / `link_copy` / `unlink_copy` / `add_copy_translation` | Ad copy library + concept links |
62
+ | `list_copy` / `create_copy` / `update_copy` / `delete_copy` / `link_copy` / `unlink_copy` / `add_copy_translation` | Ad copy library + concept links. `update_copy` edits in place; `delete_copy` removes it everywhere (status stays app-only) |
63
+ | `list_landing_pages` / `create_landing_page` | A brand's landing pages + the agency pool; link them with `set_concept_fields` |
64
+ | `iterate_concept` | Spawn a variation ({root}_Var{n}), the same as the app's Iterate button |
63
65
  | `list_kb_*` / `get_kb_doc` / `create_kb_doc` / `update_kb_doc` | Kennisbank pages |
64
66
 
65
67
  Beats (`add_beat`, `update_beat`, `delete_beat`, `set_beat_translation`, `add_language`) were
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.5.1" });
55
+ const server = new McpServer({ name: "lotux", version: "0.6.0" });
56
56
 
57
57
  // ── Reads ─────────────────────────────────────────────────────────────────────
58
58
 
@@ -157,6 +157,14 @@ server.tool(
157
157
  angle_ids: z.array(z.string()).optional(),
158
158
  use_case_ids: z.array(z.string()).optional(),
159
159
  offer_ids: z.array(z.string()).optional(),
160
+ landing_page_ids: z
161
+ .array(z.string())
162
+ .optional()
163
+ .describe("Landing page ids from list_landing_pages; replaces the concept's set"),
164
+ landing_page_id: z
165
+ .string()
166
+ .optional()
167
+ .describe("Convenience for a single landing page — ignored if landing_page_ids is given"),
160
168
  // Team roles — pass a member id from list_members.
161
169
  strategist_id: z.string().optional().describe("Member id from list_members"),
162
170
  copy_checker_id: z.string().optional().describe("Member id from list_members"),
@@ -164,8 +172,14 @@ server.tool(
164
172
  media_buyer_id: z.string().optional().describe("Member id from list_members"),
165
173
  final_check_id: z.string().optional().describe("Member id from list_members"),
166
174
  },
167
- async ({ concept_id, ...patch }) =>
168
- ok(await rpc("set_concept_fields", { concept_id, patch }))
175
+ // A concept can run several landing pages (one per market), so the column is
176
+ // a list. Fold the single-id convenience into it; an explicit list wins.
177
+ async ({ concept_id, landing_page_id, ...patch }) => {
178
+ if (landing_page_id && patch.landing_page_ids === undefined) {
179
+ patch.landing_page_ids = [landing_page_id];
180
+ }
181
+ return ok(await rpc("set_concept_fields", { concept_id, patch }));
182
+ }
169
183
  );
170
184
 
171
185
  // ── Scripts (document model) ────────────────────────────────────────────────
@@ -257,6 +271,23 @@ server.tool(
257
271
  async (args) => ok(await rpc("set_base_language", args))
258
272
  );
259
273
 
274
+ server.tool(
275
+ "create_copy",
276
+ "Create a brand-scoped ad copy item (adcopy / headline / description) and link it to a concept. Returns the new copy id. To add another language to an existing copy use add_copy_translation instead.",
277
+ {
278
+ account_id: z.string().describe("Brand id from list_brands"),
279
+ concept_id: z.string(),
280
+ type: z.enum(["adcopy", "headline", "description"]),
281
+ text: z.string(),
282
+ product_ids: z.array(z.string()).optional(),
283
+ promotion: z.string().optional(),
284
+ country: z.string().optional(),
285
+ language: z.string().optional().describe("Full-written name, e.g. English"),
286
+ funnel_stage: z.string().optional(),
287
+ },
288
+ async (args) => ok(await rpc("create_copy", args))
289
+ );
290
+
260
291
  server.tool(
261
292
  "link_copy",
262
293
  "Link an existing copy item to a concept.",
@@ -271,6 +302,29 @@ server.tool(
271
302
  async (args) => ok(await rpc("unlink_copy", args))
272
303
  );
273
304
 
305
+ server.tool(
306
+ "update_copy",
307
+ "Edit an existing copy item in place (fix a typo, correct the language or the product). Only the fields you pass change. Status is deliberately not settable here — approving or rejecting stays in the app.",
308
+ {
309
+ copy_item_id: z.string().describe("From list_copy or get_concept"),
310
+ text: z.string().optional(),
311
+ type: z.enum(["adcopy", "headline", "description"]).optional(),
312
+ product_ids: z.array(z.string()).optional(),
313
+ promotion: z.string().optional(),
314
+ country: z.string().optional(),
315
+ language: z.string().optional().describe("Full-written name, e.g. English"),
316
+ funnel_stage: z.string().optional(),
317
+ },
318
+ async (args) => ok(await rpc("update_copy", args))
319
+ );
320
+
321
+ server.tool(
322
+ "delete_copy",
323
+ "Delete a copy item from the brand library. It disappears from every concept it was linked to. To keep the copy and remove it from one concept, use unlink_copy instead.",
324
+ { copy_item_id: z.string() },
325
+ async (args) => ok(await rpc("delete_copy", args))
326
+ );
327
+
274
328
  server.tool(
275
329
  "add_copy_translation",
276
330
  "Add a language variant to an existing copy ('primary text') so it reads as one copy with a language switcher in the app. Clones the base copy's products/promotion/country/funnel with a new language + text and groups them (translation_group_id). base_copy_id comes from list_copy or get_concept; language is a full-written name (e.g. Dutch). Optionally pass concept_id to also link the new variant to that concept. Returns the new copy id.",
@@ -283,6 +337,39 @@ server.tool(
283
337
  async (args) => ok(await rpc("add_copy_translation", args))
284
338
  );
285
339
 
340
+ // ── Landing pages ────────────────────────────────────────────────────────
341
+
342
+ server.tool(
343
+ "list_landing_pages",
344
+ "A brand's landing pages, plus the agency-wide pool. Use it to get an id for set_concept_fields (landing_page_id / landing_page_ids) or for an offer's landing_page_id.",
345
+ { account_id: z.string().describe("Brand id from list_brands") },
346
+ async (args) => ok(await rpc("list_landing_pages", args))
347
+ );
348
+
349
+ server.tool(
350
+ "create_landing_page",
351
+ "Create a landing page for a brand. `type` is required and feeds the ad-name token (pdp = product page, hp = homepage, cp = category page, li = listicle, adv = advertorial, lp = landing page). `id_code` is the short suffix ad names use (pdpa1). Tag `country` when the brand runs several markets — that is how a concept picks the right page.",
352
+ {
353
+ account_id: z.string().describe("Brand id from list_brands"),
354
+ title: z.string().describe("The page's name in LOTUX"),
355
+ url: z.string(),
356
+ type: z.enum(["pdp", "hp", "cp", "li", "adv", "lp"]),
357
+ id_code: z.string().optional(),
358
+ country: z.string().optional().describe("2-letter code, e.g. NL"),
359
+ language: z.string().optional().describe("Full-written name, e.g. Dutch"),
360
+ },
361
+ async (args) => ok(await rpc("create_landing_page", args))
362
+ );
363
+
364
+ // ── Iterate ──────────────────────────────────────────────────────────────
365
+
366
+ server.tool(
367
+ "iterate_concept",
368
+ "Spawn a variation of a concept, the same as the app's Iterate button. The new concept is coded {root}_Var{n} (89 becomes 89_Var1), lands in Idea, and inherits the products / personas / angles / offers, team, brief, scripts, copy and references. Variations are flat: iterating 89_Var2 makes 89_Var3 off 89. Timeline, media buying and landing pages are reset, and base_language goes back to English even though the copied script texts keep their own languages. Returns { id, concept_code, script_id }; script_id is null when the source had no script.",
369
+ { concept_id: z.string() },
370
+ async (args) => ok(await rpc("iterate_concept", args))
371
+ );
372
+
286
373
  // ── Brand-DB entities (products / personas / angles / use-cases / offers) ──────
287
374
 
288
375
  server.tool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lotux-mcp",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
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": {