lotux-mcp 0.1.2 → 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.
- package/index.mjs +158 -5
- 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.
|
|
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
|
|
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,97 @@ 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
|
+
|
|
349
|
+
// ── Kennisbank (knowledge base) ─────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
server.tool(
|
|
352
|
+
"list_kb_workspaces",
|
|
353
|
+
"List the Kennisbank (knowledge base) workspaces you can access — markdown docs the team and AI share. Returns id, name, visibility. Use a workspace id with list_kb_docs / create_kb_doc.",
|
|
354
|
+
{},
|
|
355
|
+
async () => ok(await rpc("list_kb_workspaces"))
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
server.tool(
|
|
359
|
+
"list_kb_docs",
|
|
360
|
+
"List the pages in a Kennisbank workspace (id, title, folder path). Use a doc id with get_kb_doc.",
|
|
361
|
+
{ workspace_id: z.string().describe("Workspace id from list_kb_workspaces") },
|
|
362
|
+
async ({ workspace_id }) => ok(await rpc("list_kb_docs", { workspace_id }))
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
server.tool(
|
|
366
|
+
"get_kb_doc",
|
|
367
|
+
"Read one Kennisbank page's full markdown content by id.",
|
|
368
|
+
{ doc_id: z.string() },
|
|
369
|
+
async ({ doc_id }) => ok(await rpc("get_kb_doc", { doc_id }))
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
server.tool(
|
|
373
|
+
"create_kb_doc",
|
|
374
|
+
"Create a new markdown page in a Kennisbank workspace. Returns the new doc id. content is markdown; folder_id is optional (root if omitted).",
|
|
375
|
+
{
|
|
376
|
+
workspace_id: z.string().describe("Workspace id from list_kb_workspaces"),
|
|
377
|
+
title: z.string(),
|
|
378
|
+
content: z.string().optional().describe("Markdown body"),
|
|
379
|
+
folder_id: z.string().optional(),
|
|
380
|
+
},
|
|
381
|
+
async (args) => ok(await rpc("create_kb_doc", args))
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
server.tool(
|
|
385
|
+
"update_kb_doc",
|
|
386
|
+
"Update a Kennisbank page's title and/or markdown content. Omitted fields are left unchanged.",
|
|
387
|
+
{
|
|
388
|
+
doc_id: z.string(),
|
|
389
|
+
title: z.string().optional(),
|
|
390
|
+
content: z.string().optional().describe("Markdown body (replaces the page content)"),
|
|
391
|
+
},
|
|
392
|
+
async (args) => ok(await rpc("update_kb_doc", args))
|
|
393
|
+
);
|
|
394
|
+
|
|
242
395
|
async function main() {
|
|
243
396
|
const transport = new StdioServerTransport();
|
|
244
397
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lotux-mcp",
|
|
3
|
-
"version": "0.
|
|
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": {
|