lotux-mcp 0.1.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 +47 -0
- package/index.mjs +225 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# lotux-mcp
|
|
2
|
+
|
|
3
|
+
Local MCP server that lets Claude (Desktop or Code) create and edit **LOTUX**
|
|
4
|
+
concepts: fetch a brand's products / personas / angles / use-cases, create
|
|
5
|
+
concepts, fill scripts, draft + link ad copy, and assign team members.
|
|
6
|
+
|
|
7
|
+
It is a thin connector — all logic and data live in the LOTUX app. This package
|
|
8
|
+
just forwards your tool calls there, authenticated with your personal token.
|
|
9
|
+
RLS in LOTUX scopes everything to the brands your account can access.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
- **Node.js 18+** installed (this is what runs the connector).
|
|
14
|
+
- A **LOTUX MCP token** — generate one in LOTUX → Settings → Connections →
|
|
15
|
+
**MCP server** (shown once; revocable any time).
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
Add this to your Claude config (Claude Desktop → Settings → Developer → Edit
|
|
20
|
+
Config, or Claude Code → `~/.claude.json`):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"lotux": {
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["-y", "lotux-mcp"],
|
|
28
|
+
"env": {
|
|
29
|
+
"LOTUX_MCP_TOKEN": "lxmcp_…your token…",
|
|
30
|
+
"LOTUX_API_URL": "https://app.lotuxagency.com"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Windows:** use `"command": "cmd"` and
|
|
38
|
+
`"args": ["/c", "npx", "-y", "lotux-mcp"]`.
|
|
39
|
+
|
|
40
|
+
Restart Claude, then try: *"list my LOTUX brands"*.
|
|
41
|
+
|
|
42
|
+
## Env
|
|
43
|
+
|
|
44
|
+
| Var | Value |
|
|
45
|
+
|-----|-------|
|
|
46
|
+
| `LOTUX_MCP_TOKEN` | your personal token from Settings → Connections |
|
|
47
|
+
| `LOTUX_API_URL` | the LOTUX app URL (e.g. `https://app.lotuxagency.com`) |
|
package/index.mjs
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// LOTUX local MCP server. Runs on the user's machine (stdio) and turns Claude
|
|
3
|
+
// tool calls into authenticated RPC calls against the LOTUX app. Every call
|
|
4
|
+
// carries the user's personal token; the app resolves it to a user-scoped
|
|
5
|
+
// Supabase session, so writes are RLS-scoped to the brands that user can access.
|
|
6
|
+
//
|
|
7
|
+
// Env:
|
|
8
|
+
// LOTUX_MCP_TOKEN — personal token from LOTUX → Settings → Connections
|
|
9
|
+
// LOTUX_API_URL — app base URL, e.g. https://app.lotuxagency.com
|
|
10
|
+
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
const API = (process.env.LOTUX_API_URL || "").replace(/\/$/, "");
|
|
16
|
+
const TOKEN = process.env.LOTUX_MCP_TOKEN || "";
|
|
17
|
+
|
|
18
|
+
if (!API || !TOKEN) {
|
|
19
|
+
console.error(
|
|
20
|
+
"LOTUX MCP: set LOTUX_API_URL and LOTUX_MCP_TOKEN in the MCP server env."
|
|
21
|
+
);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Call the app's RPC endpoint. Drops undefined params so optional fields don't
|
|
26
|
+
// overwrite existing values with null on the server.
|
|
27
|
+
async function rpc(method, params = {}) {
|
|
28
|
+
const clean = Object.fromEntries(
|
|
29
|
+
Object.entries(params).filter(([, v]) => v !== undefined)
|
|
30
|
+
);
|
|
31
|
+
const res = await fetch(`${API}/api/mcp/rpc`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
"content-type": "application/json",
|
|
35
|
+
authorization: `Bearer ${TOKEN}`,
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({ method, params: clean }),
|
|
38
|
+
});
|
|
39
|
+
let data;
|
|
40
|
+
try {
|
|
41
|
+
data = await res.json();
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(`RPC ${method}: non-JSON response (HTTP ${res.status})`);
|
|
44
|
+
}
|
|
45
|
+
if (!res.ok || !data.ok) {
|
|
46
|
+
throw new Error(data?.error || `RPC ${method} failed (HTTP ${res.status})`);
|
|
47
|
+
}
|
|
48
|
+
return data.result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ok = (result) => ({
|
|
52
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const server = new McpServer({ name: "lotux", version: "0.1.0" });
|
|
56
|
+
|
|
57
|
+
// ── Reads ─────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
server.tool(
|
|
60
|
+
"list_brands",
|
|
61
|
+
"List the LOTUX brands (and the agency) you can access. Use the returned id as account_id for other tools.",
|
|
62
|
+
{},
|
|
63
|
+
async () => ok(await rpc("list_brands"))
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
server.tool(
|
|
67
|
+
"list_entities",
|
|
68
|
+
"List a brand's products, personas, angles, and use-cases (the building blocks for a concept). Pass account_id from list_brands.",
|
|
69
|
+
{ account_id: z.string().describe("Brand (or agency) id from list_brands") },
|
|
70
|
+
async ({ account_id }) => ok(await rpc("list_entities", { account_id }))
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
server.tool(
|
|
74
|
+
"list_concepts",
|
|
75
|
+
"List a brand's existing concepts (id, code like '109', title, stage). Use this to find the id of a concept the user names before editing it.",
|
|
76
|
+
{ account_id: z.string().describe("Brand id from list_brands") },
|
|
77
|
+
async ({ account_id }) => ok(await rpc("list_concepts", { account_id }))
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
server.tool(
|
|
81
|
+
"get_concept",
|
|
82
|
+
"Read the full state of one concept by id: fields, linked products/personas/angles/use-cases, script beats (with their text), and linked copy. Use to confirm what was written.",
|
|
83
|
+
{ concept_id: z.string() },
|
|
84
|
+
async ({ concept_id }) => ok(await rpc("get_concept", { concept_id }))
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
server.tool(
|
|
88
|
+
"list_options",
|
|
89
|
+
"Valid values for a concept's fields: buckets, sub_buckets, funnel_stages, messaging_frameworks, variants, formats (per-brand), countries, languages. Call before set_concept_fields so you use real keys (e.g. funnel 'tof', not 'top of funnel').",
|
|
90
|
+
{ account_id: z.string().describe("Brand id from list_brands") },
|
|
91
|
+
async ({ account_id }) => ok(await rpc("list_options", { account_id }))
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
server.tool(
|
|
95
|
+
"list_copy",
|
|
96
|
+
"List a brand's existing copy items (returns ids for link_copy). Optionally filter by type (adcopy/headline/description) or a text query.",
|
|
97
|
+
{
|
|
98
|
+
account_id: z.string().describe("Brand id from list_brands"),
|
|
99
|
+
type: z.enum(["adcopy", "headline", "description"]).optional(),
|
|
100
|
+
query: z.string().optional(),
|
|
101
|
+
},
|
|
102
|
+
async (args) => ok(await rpc("list_copy", args))
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
server.tool(
|
|
106
|
+
"list_members",
|
|
107
|
+
"List the agency team members (id + name) assignable to a concept's roles. Use the ids with set_concept_fields (strategist_id, copy_checker_id, editor_id, media_buyer_id, final_check_id).",
|
|
108
|
+
{ account_id: z.string().describe("Brand id from list_brands") },
|
|
109
|
+
async ({ account_id }) => ok(await rpc("list_members", { account_id }))
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ── Concept ────────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
server.tool(
|
|
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.",
|
|
117
|
+
{
|
|
118
|
+
account_id: z.string().describe("Brand id from list_brands"),
|
|
119
|
+
title: z.string(),
|
|
120
|
+
bucket: z
|
|
121
|
+
.string()
|
|
122
|
+
.describe("Main bucket, e.g. video / static / carousel (the brand's bucket key)"),
|
|
123
|
+
funnel_stage: z.string().optional(),
|
|
124
|
+
format: z.string().optional(),
|
|
125
|
+
product_ids: z.array(z.string()).optional(),
|
|
126
|
+
persona_ids: z.array(z.string()).optional(),
|
|
127
|
+
angle_ids: z.array(z.string()).optional(),
|
|
128
|
+
use_case_ids: z.array(z.string()).optional(),
|
|
129
|
+
},
|
|
130
|
+
async (args) => ok(await rpc("create_concept", args))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
server.tool(
|
|
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.",
|
|
136
|
+
{
|
|
137
|
+
concept_id: z.string(),
|
|
138
|
+
funnel_stage: z.string().optional(),
|
|
139
|
+
format: z.string().optional(),
|
|
140
|
+
messaging_framework: z.string().optional(),
|
|
141
|
+
variant: z.string().optional(),
|
|
142
|
+
country: z.string().optional(),
|
|
143
|
+
promotion: z.string().optional(),
|
|
144
|
+
visual_direction: z.string().optional(),
|
|
145
|
+
description: z.string().optional(),
|
|
146
|
+
base_language: z.string().optional(),
|
|
147
|
+
sub_buckets: z.array(z.string()).optional(),
|
|
148
|
+
product_ids: z.array(z.string()).optional(),
|
|
149
|
+
persona_ids: z.array(z.string()).optional(),
|
|
150
|
+
angle_ids: z.array(z.string()).optional(),
|
|
151
|
+
use_case_ids: z.array(z.string()).optional(),
|
|
152
|
+
},
|
|
153
|
+
async ({ concept_id, ...patch }) =>
|
|
154
|
+
ok(await rpc("set_concept_fields", { concept_id, patch }))
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// ── Script beats ────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
server.tool(
|
|
160
|
+
"add_beat",
|
|
161
|
+
"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.",
|
|
162
|
+
{
|
|
163
|
+
concept_id: z.string(),
|
|
164
|
+
beat_type: z.enum(["hook", "body", "cta"]).optional(),
|
|
165
|
+
text: z.string().optional().describe("The script line / what's said (the visible beat text)"),
|
|
166
|
+
visual_note: z.string().optional().describe("Optional on-screen visual note"),
|
|
167
|
+
},
|
|
168
|
+
async (args) => ok(await rpc("add_beat", args))
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
server.tool(
|
|
172
|
+
"update_beat",
|
|
173
|
+
"Update an existing script beat's script text, type, or visual note.",
|
|
174
|
+
{
|
|
175
|
+
beat_id: z.string(),
|
|
176
|
+
beat_type: z.enum(["hook", "body", "cta"]).optional(),
|
|
177
|
+
delivery_text: z.string().optional().describe("The script line / what's said"),
|
|
178
|
+
visual_note: z.string().optional(),
|
|
179
|
+
},
|
|
180
|
+
async ({ beat_id, ...patch }) => ok(await rpc("update_beat", { beat_id, patch }))
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
server.tool(
|
|
184
|
+
"set_base_language",
|
|
185
|
+
"Set the concept's base script language (the 'Original' tab), e.g. English.",
|
|
186
|
+
{ concept_id: z.string(), language: z.string() },
|
|
187
|
+
async (args) => ok(await rpc("set_base_language", args))
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// ── Copy ──────────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
server.tool(
|
|
193
|
+
"create_copy",
|
|
194
|
+
"Create a brand-scoped ad copy item (adcopy / headline / description) and link it to a concept. Returns the new copy id.",
|
|
195
|
+
{
|
|
196
|
+
account_id: z.string().describe("Brand id from list_brands"),
|
|
197
|
+
concept_id: z.string(),
|
|
198
|
+
type: z.enum(["adcopy", "headline", "description"]),
|
|
199
|
+
text: z.string(),
|
|
200
|
+
product_ids: z.array(z.string()).optional(),
|
|
201
|
+
promotion: z.string().optional(),
|
|
202
|
+
country: z.string().optional(),
|
|
203
|
+
language: z.string().optional(),
|
|
204
|
+
funnel_stage: z.string().optional(),
|
|
205
|
+
},
|
|
206
|
+
async (args) => ok(await rpc("create_copy", args))
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
server.tool(
|
|
210
|
+
"link_copy",
|
|
211
|
+
"Link an existing copy item to a concept.",
|
|
212
|
+
{ concept_id: z.string(), copy_item_id: z.string() },
|
|
213
|
+
async (args) => ok(await rpc("link_copy", args))
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
async function main() {
|
|
217
|
+
const transport = new StdioServerTransport();
|
|
218
|
+
await server.connect(transport);
|
|
219
|
+
console.error("LOTUX MCP server running on stdio");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main().catch((e) => {
|
|
223
|
+
console.error("Fatal:", e);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lotux-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
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
|
+
"bin": {
|
|
7
|
+
"lotux-mcp": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "index.mjs",
|
|
10
|
+
"files": [
|
|
11
|
+
"index.mjs",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"lotux",
|
|
21
|
+
"claude",
|
|
22
|
+
"model-context-protocol"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
26
|
+
"zod": "^3.23.8"
|
|
27
|
+
}
|
|
28
|
+
}
|