lotux-mcp 0.4.0 → 0.5.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.
- package/README.md +27 -0
- package/index.mjs +67 -104
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,3 +45,30 @@ Restart Claude, then try: *"list my LOTUX brands"*.
|
|
|
45
45
|
|-----|-------|
|
|
46
46
|
| `LOTUX_MCP_TOKEN` | your personal token from Settings → Connections |
|
|
47
47
|
| `LOTUX_API_URL` | the LOTUX app URL (e.g. `https://app.lotuxagency.com`) |
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
|
|
51
|
+
| Tool | What it does |
|
|
52
|
+
|------|--------------|
|
|
53
|
+
| `list_brands` | Brands (+ agency) you can access |
|
|
54
|
+
| `list_entities` / `create_entity` / `update_entity` / `delete_entity` | A brand's products / personas / angles / use-cases |
|
|
55
|
+
| `list_concepts` / `get_concept` | Find a concept; read its full state (fields, links, **scripts** = markdown per language + shotlist, copy) |
|
|
56
|
+
| `list_options` / `list_members` | Valid field values; assignable team members |
|
|
57
|
+
| `create_concept` | New concept in Idea (+ optional links). Returns `{ id, script_id }` — write the script with `save_script` |
|
|
58
|
+
| `set_concept_fields` | Update fields / links / team roles |
|
|
59
|
+
| `save_script` | Write a script: markdown body for one language (`- **Hook:** …`, `- **1:** …`, `- **CTA:** …`), the shotlist, a rename. Pass `language` for a translation |
|
|
60
|
+
| `add_script` / `delete_script` / `remove_script_language` / `set_base_language` | More scripts per concept; language housekeeping |
|
|
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 |
|
|
63
|
+
| `list_kb_*` / `get_kb_doc` / `create_kb_doc` / `update_kb_doc` | Kennisbank pages |
|
|
64
|
+
|
|
65
|
+
Beats (`add_beat`, `update_beat`, `delete_beat`, `set_beat_translation`, `add_language`) were
|
|
66
|
+
retired in Sep 2026 — scripts are documents now. Calling one returns a message pointing at the
|
|
67
|
+
replacement.
|
|
68
|
+
|
|
69
|
+
## Updating
|
|
70
|
+
|
|
71
|
+
A running connector keeps the tool list it started with. To pick up new tools:
|
|
72
|
+
|
|
73
|
+
1. Use `"args": ["-y", "lotux-mcp@latest"]` in the config (so `npx` re-resolves the version).
|
|
74
|
+
2. Restart Claude (Desktop: quit + reopen; Code: `/mcp` → reconnect, or restart the session).
|
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.
|
|
55
|
+
const server = new McpServer({ name: "lotux", version: "0.5.0" });
|
|
56
56
|
|
|
57
57
|
// ── Reads ─────────────────────────────────────────────────────────────────────
|
|
58
58
|
|
|
@@ -79,7 +79,7 @@ server.tool(
|
|
|
79
79
|
|
|
80
80
|
server.tool(
|
|
81
81
|
"get_concept",
|
|
82
|
-
"Read the full state of one concept by id: fields, linked products/personas/angles/use-cases,
|
|
82
|
+
"Read the full state of one concept by id: fields, linked products/personas/angles/use-cases, scripts (markdown per language + shotlist), and linked copy. Use to confirm what was written.",
|
|
83
83
|
{ concept_id: z.string() },
|
|
84
84
|
async ({ concept_id }) => ok(await rpc("get_concept", { concept_id }))
|
|
85
85
|
);
|
|
@@ -113,19 +113,21 @@ server.tool(
|
|
|
113
113
|
|
|
114
114
|
server.tool(
|
|
115
115
|
"create_concept",
|
|
116
|
-
"Create a new concept in the brand's first (Idea) stage, optionally linking products/personas/angles/use-cases. Returns { id,
|
|
116
|
+
"Create a new concept in the brand's first (Idea) stage, optionally linking products/personas/angles/offers/use-cases (a concept needs ≥1 product, persona, angle and offer before it can enter Copy check). Returns { id, script_id } — the concept id plus the id of its pre-seeded script (starter template). Write the script with save_script(script_id, body); do NOT add_script for the first script. A human moves the concept through the copy-check gate later in the app.",
|
|
117
117
|
{
|
|
118
118
|
account_id: z.string().describe("Brand id from list_brands"),
|
|
119
119
|
title: z.string(),
|
|
120
120
|
bucket: z
|
|
121
121
|
.string()
|
|
122
|
-
.
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Main bucket, e.g. video / static / carousel (the brand's bucket key). Defaults to video."),
|
|
123
124
|
funnel_stage: z.string().optional(),
|
|
124
125
|
format: z.string().optional(),
|
|
125
126
|
product_ids: z.array(z.string()).optional(),
|
|
126
127
|
persona_ids: z.array(z.string()).optional(),
|
|
127
128
|
angle_ids: z.array(z.string()).optional(),
|
|
128
129
|
use_case_ids: z.array(z.string()).optional(),
|
|
130
|
+
offer_ids: z.array(z.string()).optional().describe("Offer ids from list_entities (offers)"),
|
|
129
131
|
},
|
|
130
132
|
async (args) => ok(await rpc("create_concept", args))
|
|
131
133
|
);
|
|
@@ -142,14 +144,19 @@ server.tool(
|
|
|
142
144
|
variant: z.string().optional(),
|
|
143
145
|
country: z.string().optional(),
|
|
144
146
|
promotion: z.string().optional(),
|
|
145
|
-
|
|
146
|
-
description: z.string().optional(),
|
|
147
|
+
brief: z.string().optional().describe("Concept brief (markdown)"),
|
|
147
148
|
base_language: z.string().optional(),
|
|
148
149
|
sub_buckets: z.array(z.string()).optional(),
|
|
150
|
+
bucket: z.string().optional().describe("Main bucket option key (see list_options.fields)"),
|
|
151
|
+
fields: z
|
|
152
|
+
.record(z.union([z.string(), z.array(z.string()), z.null()]))
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Brand-defined concept fields by key → option key(s); see list_options.fields"),
|
|
149
155
|
product_ids: z.array(z.string()).optional(),
|
|
150
156
|
persona_ids: z.array(z.string()).optional(),
|
|
151
157
|
angle_ids: z.array(z.string()).optional(),
|
|
152
158
|
use_case_ids: z.array(z.string()).optional(),
|
|
159
|
+
offer_ids: z.array(z.string()).optional(),
|
|
153
160
|
// Team roles — pass a member id from list_members.
|
|
154
161
|
strategist_id: z.string().optional().describe("Member id from list_members"),
|
|
155
162
|
copy_checker_id: z.string().optional().describe("Member id from list_members"),
|
|
@@ -161,139 +168,95 @@ server.tool(
|
|
|
161
168
|
ok(await rpc("set_concept_fields", { concept_id, patch }))
|
|
162
169
|
);
|
|
163
170
|
|
|
164
|
-
// ──
|
|
171
|
+
// ── Scripts (document model) ────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
const SCRIPT_BODY_DOC =
|
|
174
|
+
"Markdown. Use labelled lines: `- **Hook:** …` (or `- **Hook 1:** …`, `- **Hook 2:** …` for several hooks), `- **1:** …`, `- **2:** …` for body lines, `- **CTA:** …` for the close. Hook ad-names are derived from the Hook lines. Free text (headings, paragraphs) is fine around them.";
|
|
165
175
|
|
|
166
176
|
server.tool(
|
|
167
|
-
"
|
|
168
|
-
"
|
|
177
|
+
"save_script",
|
|
178
|
+
"Write a concept script: the markdown body for one language and/or the shotlist and/or a rename. Any omitted part is left unchanged. script_id comes from create_concept (script_id) or get_concept (scripts[].id). language defaults to the concept's base language; pass a translation language (full-written, e.g. Dutch) to write that language's version — a new language is added automatically. Returns the full script.",
|
|
169
179
|
{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.enum(["voiceover", "talking_head", "text_on_screen"])
|
|
176
|
-
.optional()
|
|
177
|
-
.describe("Per-beat delivery type dropdown"),
|
|
180
|
+
script_id: z.string(),
|
|
181
|
+
language: z.string().optional().describe("Full-written language name, e.g. English / Dutch. Defaults to the base language."),
|
|
182
|
+
body: z.string().optional().describe(SCRIPT_BODY_DOC),
|
|
183
|
+
shotlist: z.array(z.string()).optional().describe("Ordered list of shots / b-roll (one line each). Replaces the whole list."),
|
|
184
|
+
name: z.string().optional().describe("Rename the script"),
|
|
178
185
|
},
|
|
179
|
-
async (args) => ok(await rpc("
|
|
186
|
+
async (args) => ok(await rpc("save_script", args))
|
|
180
187
|
);
|
|
181
188
|
|
|
182
189
|
server.tool(
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
beat_type: z.enum(["hook", "body", "cta"]).optional(),
|
|
188
|
-
delivery_text: z.string().optional().describe("The script line / what's said"),
|
|
189
|
-
visual_note: z.string().optional(),
|
|
190
|
-
delivery_kind: z
|
|
191
|
-
.enum(["voiceover", "talking_head", "text_on_screen"])
|
|
192
|
-
.optional()
|
|
193
|
-
.describe("Per-beat delivery type dropdown"),
|
|
194
|
-
},
|
|
195
|
-
async ({ beat_id, ...patch }) => ok(await rpc("update_beat", { beat_id, patch }))
|
|
190
|
+
"add_script",
|
|
191
|
+
"Add another script to a concept (every concept already has one — use save_script on it first). Returns the new script (starter template in the base language).",
|
|
192
|
+
{ concept_id: z.string(), name: z.string().optional() },
|
|
193
|
+
async (args) => ok(await rpc("add_script", args))
|
|
196
194
|
);
|
|
197
195
|
|
|
198
196
|
server.tool(
|
|
199
|
-
"
|
|
200
|
-
"Delete a script
|
|
201
|
-
{
|
|
202
|
-
async ({
|
|
197
|
+
"delete_script",
|
|
198
|
+
"Delete a script (all its language versions + shotlist). Irreversible.",
|
|
199
|
+
{ script_id: z.string() },
|
|
200
|
+
async ({ script_id }) => ok(await rpc("delete_script", { script_id }))
|
|
203
201
|
);
|
|
204
202
|
|
|
203
|
+
server.tool(
|
|
204
|
+
"remove_script_language",
|
|
205
|
+
"Remove one translation language from a script. The base language cannot be removed.",
|
|
206
|
+
{ script_id: z.string(), language: z.string() },
|
|
207
|
+
async (args) => ok(await rpc("remove_script_language", args))
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// ── Beat compat (older prompts / scripts) ───────────────────────────────────
|
|
211
|
+
|
|
205
212
|
server.tool(
|
|
206
213
|
"set_beats",
|
|
207
|
-
"
|
|
214
|
+
"Compatibility — prefer save_script. Sends an ordered beat list (Hook / Body / CTA lines + optional visual notes) and stores it as the concept's script document: lines become `- **Hook:**` / `- **1:**` / `- **CTA:**` markdown, visual notes become the shotlist. Replaces the whole script text for that language (default: the base language). Returns script_id + synthetic beat_ids ('<script_id>#<index>') for set_beat_translations.",
|
|
208
215
|
{
|
|
209
216
|
concept_id: z.string(),
|
|
210
|
-
beats: z
|
|
211
|
-
.
|
|
212
|
-
z.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
})
|
|
219
|
-
)
|
|
220
|
-
.describe("The full ordered beat list. Anything not included is deleted."),
|
|
217
|
+
beats: z.array(
|
|
218
|
+
z.object({
|
|
219
|
+
beat_type: z.enum(["hook", "body", "cta"]).optional(),
|
|
220
|
+
text: z.string().optional().describe("Spoken line"),
|
|
221
|
+
visual_note: z.string().optional().describe("What to show — lands in the shotlist"),
|
|
222
|
+
})
|
|
223
|
+
),
|
|
224
|
+
language: z.string().optional().describe("Full-written language, defaults to the concept's base language"),
|
|
221
225
|
},
|
|
222
226
|
async (args) => ok(await rpc("set_beats", args))
|
|
223
227
|
);
|
|
224
228
|
|
|
225
229
|
server.tool(
|
|
226
230
|
"set_beat_translations",
|
|
227
|
-
"
|
|
231
|
+
"Compatibility — prefer save_script with a language. Writes one language's translated lines onto the concept's script: items are matched to the base script's lines by beat_id ('<script_id>#<index>' from set_beats) or by array order; a missing/empty text keeps the base line. Labels (Hook / 1 / CTA) are preserved.",
|
|
228
232
|
{
|
|
229
233
|
concept_id: z.string(),
|
|
230
|
-
language: z.string().describe("Full-written
|
|
231
|
-
items: z
|
|
232
|
-
.
|
|
233
|
-
z.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
239
|
-
.describe("One entry per beat to translate for this language"),
|
|
234
|
+
language: z.string().describe("Full-written language, e.g. Dutch"),
|
|
235
|
+
items: z.array(
|
|
236
|
+
z.object({
|
|
237
|
+
beat_id: z.string().optional(),
|
|
238
|
+
text: z.string().optional(),
|
|
239
|
+
visual_note: z.string().optional().describe("Ignored — shotlists are language-independent"),
|
|
240
|
+
})
|
|
241
|
+
),
|
|
240
242
|
},
|
|
241
243
|
async (args) => ok(await rpc("set_beat_translations", args))
|
|
242
244
|
);
|
|
243
245
|
|
|
244
246
|
server.tool(
|
|
245
247
|
"delete_concept",
|
|
246
|
-
"Delete an entire concept by id. Removes its
|
|
248
|
+
"Delete an entire concept by id. Removes its scripts, entity links and copy links. Irreversible — confirm the id via list_concepts / get_concept first.",
|
|
247
249
|
{ concept_id: z.string() },
|
|
248
250
|
async ({ concept_id }) => ok(await rpc("delete_concept", { concept_id }))
|
|
249
251
|
);
|
|
250
252
|
|
|
251
253
|
server.tool(
|
|
252
254
|
"set_base_language",
|
|
253
|
-
"Set the concept's base script language
|
|
255
|
+
"Set the concept's base script language, e.g. English. Existing base-language script texts are re-labelled to the new language.",
|
|
254
256
|
{ concept_id: z.string(), language: z.string() },
|
|
255
257
|
async (args) => ok(await rpc("set_base_language", args))
|
|
256
258
|
);
|
|
257
259
|
|
|
258
|
-
server.tool(
|
|
259
|
-
"add_language",
|
|
260
|
-
"Add a translation language (tab) to a concept's Script, e.g. Dutch. Use a full-written name from list_options. Then write each beat's translated line with set_beat_translation.",
|
|
261
|
-
{ concept_id: z.string(), language: z.string().describe("Full-written name, e.g. Dutch") },
|
|
262
|
-
async (args) => ok(await rpc("add_language", args))
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
server.tool(
|
|
266
|
-
"set_beat_translation",
|
|
267
|
-
"Write one beat's translated script line for a language. beat_id comes from get_concept; language must match one added via add_language.",
|
|
268
|
-
{
|
|
269
|
-
beat_id: z.string(),
|
|
270
|
-
language: z.string().describe("Full-written name, e.g. Dutch"),
|
|
271
|
-
delivery_text: z.string().optional().describe("The translated script line / what's said"),
|
|
272
|
-
visual_note: z.string().optional(),
|
|
273
|
-
},
|
|
274
|
-
async ({ beat_id, language, ...patch }) =>
|
|
275
|
-
ok(await rpc("set_beat_translation", { beat_id, language, ...patch }))
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
// ── Copy ──────────────────────────────────────────────────────────────────────
|
|
279
|
-
|
|
280
|
-
server.tool(
|
|
281
|
-
"create_copy",
|
|
282
|
-
"Create a brand-scoped ad copy item (adcopy / headline / description) and link it to a concept. Returns the new copy id.",
|
|
283
|
-
{
|
|
284
|
-
account_id: z.string().describe("Brand id from list_brands"),
|
|
285
|
-
concept_id: z.string(),
|
|
286
|
-
type: z.enum(["adcopy", "headline", "description"]),
|
|
287
|
-
text: z.string(),
|
|
288
|
-
product_ids: z.array(z.string()).optional(),
|
|
289
|
-
promotion: z.string().optional(),
|
|
290
|
-
country: z.string().optional(),
|
|
291
|
-
language: z.string().optional(),
|
|
292
|
-
funnel_stage: z.string().optional(),
|
|
293
|
-
},
|
|
294
|
-
async (args) => ok(await rpc("create_copy", args))
|
|
295
|
-
);
|
|
296
|
-
|
|
297
260
|
server.tool(
|
|
298
261
|
"link_copy",
|
|
299
262
|
"Link an existing copy item to a concept.",
|
|
@@ -324,10 +287,10 @@ server.tool(
|
|
|
324
287
|
|
|
325
288
|
server.tool(
|
|
326
289
|
"create_entity",
|
|
327
|
-
"Create a brand-DB entity
|
|
290
|
+
"Create a NEW brand-DB entity — a product, persona, use_case or angle that doesn't exist yet (to attach an EXISTING one to a concept, pass its id to set_concept_fields instead). name is required; description optional. `extras` carries the typed fields: products take average_price / margin_pct / cm_target (target contribution-margin %) / ltv_multiplier / be_ncroas / product_link / type / status; personas take awareness_stage / hypothesis; angles take belief_shift (core idea goes in description). Omit unknown fields. Personas/use_cases/angles get an auto id_code (p1/u1/a1), returned in the result. Link the new entity to a concept afterwards with set_concept_fields.",
|
|
328
291
|
{
|
|
329
292
|
account_id: z.string().describe("Brand id from list_brands"),
|
|
330
|
-
kind: z.enum(["products", "personas", "angles", "use_cases"]),
|
|
293
|
+
kind: z.enum(["products", "personas", "angles", "use_cases", "offers"]),
|
|
331
294
|
name: z.string(),
|
|
332
295
|
description: z.string().optional(),
|
|
333
296
|
extras: z.record(z.any()).optional().describe("Typed fields keyed by column name (see list_entities output shape)"),
|
|
@@ -340,7 +303,7 @@ server.tool(
|
|
|
340
303
|
"Update a brand-DB entity by id. Omitted fields are left unchanged; a blank name is ignored (name is required). Use extras to patch typed fields.",
|
|
341
304
|
{
|
|
342
305
|
entity_id: z.string(),
|
|
343
|
-
kind: z.enum(["products", "personas", "angles", "use_cases"]),
|
|
306
|
+
kind: z.enum(["products", "personas", "angles", "use_cases", "offers"]),
|
|
344
307
|
name: z.string().optional(),
|
|
345
308
|
description: z.string().optional(),
|
|
346
309
|
extras: z.record(z.any()).optional(),
|
|
@@ -350,10 +313,10 @@ server.tool(
|
|
|
350
313
|
|
|
351
314
|
server.tool(
|
|
352
315
|
"delete_entity",
|
|
353
|
-
"Delete a brand-DB entity by id. Its links to concepts are removed with it. Irreversible — confirm via list_entities first.",
|
|
316
|
+
"Delete a brand-DB entity (product / persona / use_case / angle / offer) by id. Its links to concepts are removed with it. Irreversible — confirm the id via list_entities first.",
|
|
354
317
|
{
|
|
355
318
|
entity_id: z.string(),
|
|
356
|
-
kind: z.enum(["products", "personas", "angles", "use_cases"]),
|
|
319
|
+
kind: z.enum(["products", "personas", "angles", "use_cases", "offers"]),
|
|
357
320
|
},
|
|
358
321
|
async (args) => ok(await rpc("delete_entity", args))
|
|
359
322
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lotux-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|