lotux-mcp 0.1.3 → 0.3.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 (2) hide show
  1. package/index.mjs +112 -5
  2. package/package.json +1 -1
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.1.3" });
55
+ const server = new McpServer({ name: "lotux", version: "0.3.0" });
56
56
 
57
57
  // ── Reads ─────────────────────────────────────────────────────────────────────
58
58
 
@@ -113,7 +113,7 @@ 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 the new concept id. A human moves it through the copy-check gate later in the app.",
116
+ "Create a new concept in the brand's first (Idea) stage, optionally linking products/personas/angles/use-cases. Returns { id, beat_ids } — the concept id plus the ids of 3 pre-seeded empty Hook beats. Fill those 3 with update_beat; do NOT add_beat for the first three hooks or you get duplicates. 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(),
@@ -132,9 +132,10 @@ server.tool(
132
132
 
133
133
  server.tool(
134
134
  "set_concept_fields",
135
- "Update fields on an existing concept. Any omitted field is left unchanged; the id arrays, when given, replace the full set of links. Assign team roles by passing a person id from list_members into strategist_id / copy_checker_id / editor_id / media_buyer_id / final_check_id.",
135
+ "Update fields on an existing concept — including renaming it via title. Any omitted field is left unchanged; the id arrays, when given, replace the full set of links. Assign team roles by passing a person id from list_members into strategist_id / copy_checker_id / editor_id / media_buyer_id / final_check_id.",
136
136
  {
137
137
  concept_id: z.string(),
138
+ title: z.string().optional().describe("Rename the concept (blank is ignored — title is required)"),
138
139
  funnel_stage: z.string().optional(),
139
140
  format: z.string().optional(),
140
141
  messaging_framework: z.string().optional(),
@@ -164,28 +165,89 @@ server.tool(
164
165
 
165
166
  server.tool(
166
167
  "add_beat",
167
- "Add a script beat (Hook / Body / CTA) to a concept. `text` is the spoken/script line shown in the Script tab's VO/TH/TEXT field. Returns the new beat id.",
168
+ "Add a script beat (Hook / Body / CTA) to a concept. `text` is the spoken/script line shown in the Script tab's VO/TH/TEXT field. Returns the new beat id. NOTE: new concepts already come with 3 empty Hook beats (see create_concept) — fill those with update_beat first; only add_beat for extra beats beyond them.",
168
169
  {
169
170
  concept_id: z.string(),
170
171
  beat_type: z.enum(["hook", "body", "cta"]).optional(),
171
172
  text: z.string().optional().describe("The script line / what's said (the visible beat text)"),
172
173
  visual_note: z.string().optional().describe("Optional on-screen visual note"),
174
+ delivery_kind: z
175
+ .enum(["voiceover", "talking_head", "text_on_screen"])
176
+ .optional()
177
+ .describe("Per-beat delivery type dropdown"),
173
178
  },
174
179
  async (args) => ok(await rpc("add_beat", args))
175
180
  );
176
181
 
177
182
  server.tool(
178
183
  "update_beat",
179
- "Update an existing script beat's script text, type, or visual note.",
184
+ "Update an existing script beat's script text, type, delivery kind, or visual note. Use this (not add_beat) to edit a beat — the beat_id comes from create_concept or get_concept.",
180
185
  {
181
186
  beat_id: z.string(),
182
187
  beat_type: z.enum(["hook", "body", "cta"]).optional(),
183
188
  delivery_text: z.string().optional().describe("The script line / what's said"),
184
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"),
185
194
  },
186
195
  async ({ beat_id, ...patch }) => ok(await rpc("update_beat", { beat_id, patch }))
187
196
  );
188
197
 
198
+ server.tool(
199
+ "delete_beat",
200
+ "Delete a script beat by id (its translations are removed with it). Use to remove a wrong or leftover beat instead of blanking its text.",
201
+ { beat_id: z.string() },
202
+ async ({ beat_id }) => ok(await rpc("delete_beat", { beat_id }))
203
+ );
204
+
205
+ server.tool(
206
+ "set_beats",
207
+ "Set a concept's ENTIRE script in one call (the efficient way to build or restructure beats). AUTHORITATIVE: the beats array becomes the full ordered script — order = array order, beats with an id are updated in place, id-less beats are inserted, and any existing beat you omit is deleted. Preserves translations on kept beats. Returns { beat_ids } in final order — feed those to set_beat_translation for per-language text. Prefer this over repeated add_beat/update_beat/delete_beat. Read the current beats with get_concept first so you resend the ones you want to keep.",
208
+ {
209
+ concept_id: z.string(),
210
+ beats: z
211
+ .array(
212
+ z.object({
213
+ id: z.string().optional().describe("Existing beat id to update; omit to insert a new beat"),
214
+ beat_type: z.enum(["hook", "body", "cta"]).optional(),
215
+ text: z.string().optional().describe("The script line / what's said"),
216
+ visual_note: z.string().optional(),
217
+ delivery_kind: z.enum(["voiceover", "talking_head", "text_on_screen"]).optional(),
218
+ })
219
+ )
220
+ .describe("The full ordered beat list. Anything not included is deleted."),
221
+ },
222
+ async (args) => ok(await rpc("set_beats", args))
223
+ );
224
+
225
+ server.tool(
226
+ "set_beat_translations",
227
+ "Write one language's ENTIRE translated script in a single call (the efficient partner to set_beats). Registers the language (if new) and upserts each beat's translated line. Prefer this over repeated add_language + set_beat_translation. beat_id values come from set_beats or get_concept; language is a full-written name (e.g. Dutch).",
228
+ {
229
+ concept_id: z.string(),
230
+ language: z.string().describe("Full-written name, e.g. Dutch"),
231
+ items: z
232
+ .array(
233
+ z.object({
234
+ beat_id: z.string(),
235
+ text: z.string().optional().describe("The translated script line"),
236
+ visual_note: z.string().optional(),
237
+ })
238
+ )
239
+ .describe("One entry per beat to translate for this language"),
240
+ },
241
+ async (args) => ok(await rpc("set_beat_translations", args))
242
+ );
243
+
244
+ server.tool(
245
+ "delete_concept",
246
+ "Delete an entire concept by id. Removes its beats, translations, entity links and copy links. Irreversible — confirm the id via list_concepts / get_concept first.",
247
+ { concept_id: z.string() },
248
+ async ({ concept_id }) => ok(await rpc("delete_concept", { concept_id }))
249
+ );
250
+
189
251
  server.tool(
190
252
  "set_base_language",
191
253
  "Set the concept's base script language (the 'Original' tab), e.g. English.",
@@ -239,6 +301,51 @@ server.tool(
239
301
  async (args) => ok(await rpc("link_copy", args))
240
302
  );
241
303
 
304
+ server.tool(
305
+ "unlink_copy",
306
+ "Unlink a copy item from a concept. The copy stays in the brand library; only the link is removed.",
307
+ { concept_id: z.string(), copy_item_id: z.string() },
308
+ async (args) => ok(await rpc("unlink_copy", args))
309
+ );
310
+
311
+ // ── Brand-DB entities (products / personas / angles / use-cases) ────────────────
312
+
313
+ server.tool(
314
+ "create_entity",
315
+ "Create a brand-DB entity (persona / product / use_case / angle). name is required; description optional. `extras` carries the typed fields (e.g. persona awareness_stage/hypothesis, angle core idea via description + belief_shift, product average_price/margin_pct) — omit if unknown. Personas/use_cases/angles get an auto id_code (p1/u1/a1), returned in the result. Link it to a concept afterwards with set_concept_fields.",
316
+ {
317
+ account_id: z.string().describe("Brand id from list_brands"),
318
+ kind: z.enum(["products", "personas", "angles", "use_cases"]),
319
+ name: z.string(),
320
+ description: z.string().optional(),
321
+ extras: z.record(z.any()).optional().describe("Typed fields keyed by column name (see list_entities output shape)"),
322
+ },
323
+ async (args) => ok(await rpc("create_entity", args))
324
+ );
325
+
326
+ server.tool(
327
+ "update_entity",
328
+ "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.",
329
+ {
330
+ entity_id: z.string(),
331
+ kind: z.enum(["products", "personas", "angles", "use_cases"]),
332
+ name: z.string().optional(),
333
+ description: z.string().optional(),
334
+ extras: z.record(z.any()).optional(),
335
+ },
336
+ async (args) => ok(await rpc("update_entity", args))
337
+ );
338
+
339
+ server.tool(
340
+ "delete_entity",
341
+ "Delete a brand-DB entity by id. Its links to concepts are removed with it. Irreversible — confirm via list_entities first.",
342
+ {
343
+ entity_id: z.string(),
344
+ kind: z.enum(["products", "personas", "angles", "use_cases"]),
345
+ },
346
+ async (args) => ok(await rpc("delete_entity", args))
347
+ );
348
+
242
349
  // ── Kennisbank (knowledge base) ─────────────────────────────────────────────────
243
350
 
244
351
  server.tool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lotux-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.3.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": {