neuron-mcp-server 1.0.0 → 1.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/.env +13 -0
- package/AI_PERSONALITY_DESIGN_RESEARCH.md +1020 -0
- package/Dockerfile +18 -0
- package/README.md +146 -131
- package/dist/client.d.ts +6 -0
- package/dist/client.js +50 -12
- package/dist/client.js.map +1 -1
- package/dist/index.js +51 -7
- package/dist/index.js.map +1 -1
- package/dist/tools/ai.js +9 -3
- package/dist/tools/ai.js.map +1 -1
- package/dist/tools/approvals.d.ts +2 -0
- package/dist/tools/approvals.js +181 -0
- package/dist/tools/approvals.js.map +1 -0
- package/dist/tools/audit.js +7 -4
- package/dist/tools/audit.js.map +1 -1
- package/dist/tools/auth.js +119 -19
- package/dist/tools/auth.js.map +1 -1
- package/dist/tools/billing.js +22 -6
- package/dist/tools/billing.js.map +1 -1
- package/dist/tools/blog.js +98 -34
- package/dist/tools/blog.js.map +1 -1
- package/dist/tools/bot-api-keys.js +27 -12
- package/dist/tools/bot-api-keys.js.map +1 -1
- package/dist/tools/bot-api.js +53 -27
- package/dist/tools/bot-api.js.map +1 -1
- package/dist/tools/bots.js +116 -68
- package/dist/tools/bots.js.map +1 -1
- package/dist/tools/broadcasts.js +78 -38
- package/dist/tools/broadcasts.js.map +1 -1
- package/dist/tools/builtin-tools.js +19 -12
- package/dist/tools/builtin-tools.js.map +1 -1
- package/dist/tools/campaigns.js +149 -61
- package/dist/tools/campaigns.js.map +1 -1
- package/dist/tools/channels.js +163 -53
- package/dist/tools/channels.js.map +1 -1
- package/dist/tools/contact-lists.js +111 -51
- package/dist/tools/contact-lists.js.map +1 -1
- package/dist/tools/contacts.js +117 -44
- package/dist/tools/contacts.js.map +1 -1
- package/dist/tools/conversations.js +95 -50
- package/dist/tools/conversations.js.map +1 -1
- package/dist/tools/flows.d.ts +2 -0
- package/dist/tools/flows.js +242 -0
- package/dist/tools/flows.js.map +1 -0
- package/dist/tools/group-management.d.ts +2 -0
- package/dist/tools/group-management.js +239 -0
- package/dist/tools/group-management.js.map +1 -0
- package/dist/tools/leads.d.ts +2 -0
- package/dist/tools/leads.js +100 -0
- package/dist/tools/leads.js.map +1 -0
- package/dist/tools/list-campaigns.d.ts +2 -0
- package/dist/tools/list-campaigns.js +152 -0
- package/dist/tools/list-campaigns.js.map +1 -0
- package/dist/tools/list-pool.js +250 -33
- package/dist/tools/list-pool.js.map +1 -1
- package/dist/tools/media.js +12 -7
- package/dist/tools/media.js.map +1 -1
- package/dist/tools/newsletters.js +106 -26
- package/dist/tools/newsletters.js.map +1 -1
- package/dist/tools/organizations.js +76 -35
- package/dist/tools/organizations.js.map +1 -1
- package/dist/tools/outbound-webhooks.js +32 -20
- package/dist/tools/outbound-webhooks.js.map +1 -1
- package/dist/tools/payouts.js +44 -17
- package/dist/tools/payouts.js.map +1 -1
- package/dist/tools/personas.d.ts +2 -0
- package/dist/tools/personas.js +141 -0
- package/dist/tools/personas.js.map +1 -0
- package/dist/tools/polls.d.ts +2 -0
- package/dist/tools/polls.js +30 -0
- package/dist/tools/polls.js.map +1 -0
- package/dist/tools/pool.js +101 -43
- package/dist/tools/pool.js.map +1 -1
- package/dist/tools/products.d.ts +2 -0
- package/dist/tools/products.js +277 -0
- package/dist/tools/products.js.map +1 -0
- package/dist/tools/profile-privacy.d.ts +2 -0
- package/dist/tools/profile-privacy.js +195 -0
- package/dist/tools/profile-privacy.js.map +1 -0
- package/dist/tools/reflections.js +25 -13
- package/dist/tools/reflections.js.map +1 -1
- package/dist/tools/scheduled-messages.d.ts +2 -0
- package/dist/tools/scheduled-messages.js +124 -0
- package/dist/tools/scheduled-messages.js.map +1 -0
- package/dist/tools/social-channels.d.ts +2 -0
- package/dist/tools/social-channels.js +318 -0
- package/dist/tools/social-channels.js.map +1 -0
- package/dist/tools/tasks.d.ts +2 -0
- package/dist/tools/tasks.js +155 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/tools.js +86 -40
- package/dist/tools/tools.js.map +1 -1
- package/dist/tools/wallets.js +34 -12
- package/dist/tools/wallets.js.map +1 -1
- package/dist/tools/webhooks.js +35 -20
- package/dist/tools/webhooks.js.map +1 -1
- package/dist/tools/whatsapp-actions.d.ts +2 -0
- package/dist/tools/whatsapp-actions.js +186 -0
- package/dist/tools/whatsapp-actions.js.map +1 -0
- package/package.json +4 -31
- package/scripts/setup-delivahere-bot.ts +804 -0
- package/scripts/test-delivahere-bot.ts +128 -0
- package/scripts/update-letschop-prompt.ts +194 -0
- package/server.json +26 -0
- package/smithery.yaml +19 -0
- package/src/client.ts +169 -0
- package/src/index.ts +256 -0
- package/src/tools/ai.ts +35 -0
- package/src/tools/approvals.ts +231 -0
- package/src/tools/audit.ts +49 -0
- package/src/tools/auth.ts +536 -0
- package/src/tools/billing.ts +79 -0
- package/src/tools/blog.ts +264 -0
- package/src/tools/bot-api-keys.ts +87 -0
- package/src/tools/bot-api.ts +193 -0
- package/src/tools/bots.ts +320 -0
- package/src/tools/broadcasts.ts +224 -0
- package/src/tools/builtin-tools.ts +108 -0
- package/src/tools/campaigns.ts +429 -0
- package/src/tools/channels.ts +520 -0
- package/src/tools/contact-lists.ts +320 -0
- package/src/tools/contacts.ts +395 -0
- package/src/tools/conversations.ts +349 -0
- package/src/tools/flows.ts +298 -0
- package/src/tools/group-management.ts +265 -0
- package/src/tools/knowledge-bases.ts +451 -0
- package/src/tools/leads.ts +104 -0
- package/src/tools/list-campaigns.ts +190 -0
- package/src/tools/list-pool.ts +389 -0
- package/src/tools/media.ts +50 -0
- package/src/tools/newsletters.ts +271 -0
- package/src/tools/organizations.ts +287 -0
- package/src/tools/outbound-webhooks.ts +181 -0
- package/src/tools/payouts.ts +133 -0
- package/src/tools/personas.ts +145 -0
- package/src/tools/polls.ts +32 -0
- package/src/tools/pool.ts +293 -0
- package/src/tools/products.ts +280 -0
- package/src/tools/profile-privacy.ts +215 -0
- package/src/tools/reflections.ts +124 -0
- package/src/tools/scheduled-messages.ts +144 -0
- package/src/tools/social-channels.ts +396 -0
- package/src/tools/tasks.ts +198 -0
- package/src/tools/tools.ts +255 -0
- package/src/tools/wallets.ts +107 -0
- package/src/tools/webhooks.ts +166 -0
- package/src/tools/whatsapp-actions.ts +203 -0
- package/tsconfig.json +20 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { api, jsonResult, errorResult } from "../client.js";
|
|
4
|
+
|
|
5
|
+
const messageSource = z.discriminatedUnion("kind", [
|
|
6
|
+
z.object({ kind: z.literal("static"), text: z.string().min(1).describe("The message body sent to each contact in the batch") }),
|
|
7
|
+
z.object({ kind: z.literal("rotation"), messages: z.array(z.string().min(1)).min(1).describe("Pool of messages"), mode: z.enum(["sequential", "random"]).optional().describe("Rotate in order (default) or pick randomly per run") }),
|
|
8
|
+
z.object({ kind: z.literal("content"), ref: z.string().min(1).describe("A Neuron Note ID; its content becomes the message, fetched live at each run") }),
|
|
9
|
+
z.object({ kind: z.literal("ai"), prompt: z.string().min(1).describe("Instruction for the message the AI should write"), botId: z.string().uuid().optional().describe("Bot whose system prompt supplies business context"), personaId: z.string().uuid().optional().describe("Persona whose voice the message is written in"), perContact: z.boolean().optional().describe("Generate a distinct message per recipient (slower, more tokens). Default false = one message for the whole batch") }).describe("AI-written message generated live at each run (BYOK-aware)"),
|
|
10
|
+
z.object({ kind: z.literal("webhook"), url: z.string().url().describe("POSTed { campaignId, listId, contact? }; returns { message } or a bare string"), perContact: z.boolean().optional().describe("Call once per recipient with the contact in the payload. Default false = one call per batch") }).describe("Message fetched from your webhook at each run"),
|
|
11
|
+
]).describe("How the message is resolved live at each batch run: 'static' (fixed), 'rotation' (cycle through several), 'ai' (AI-written), 'webhook' (from your service), or 'content' (a saved Neuron Note).");
|
|
12
|
+
|
|
13
|
+
const schedule = z.intersection(
|
|
14
|
+
z.discriminatedUnion("kind", [
|
|
15
|
+
z.object({ kind: z.literal("interval"), everyMs: z.number().int().min(60_000).describe("Milliseconds between runs, e.g. 86400000 for daily") }),
|
|
16
|
+
z.object({ kind: z.literal("daily"), hour: z.number().int().min(0).max(23).describe("UTC hour"), minute: z.number().int().min(0).max(59).optional() }),
|
|
17
|
+
z.object({ kind: z.literal("weekly"), days: z.array(z.number().int().min(0).max(6)).min(1).describe("Days of week, 0=Sun … 6=Sat"), hour: z.number().int().min(0).max(23).describe("UTC hour"), minute: z.number().int().min(0).max(59).optional() }),
|
|
18
|
+
z.object({ kind: z.literal("monthly"), dayOfMonth: z.number().int().min(1).max(31), hour: z.number().int().min(0).max(23).describe("UTC hour"), minute: z.number().int().min(0).max(59).optional() }),
|
|
19
|
+
]),
|
|
20
|
+
z.object({
|
|
21
|
+
timezone: z.string().optional().describe("IANA timezone the daily/weekly/monthly time is interpreted in, e.g. 'Africa/Lagos'. Defaults to UTC. Ignored for 'interval'. The schedule holds its local wall-clock time across DST."),
|
|
22
|
+
endAt: z.string().optional().describe("ISO date; the campaign completes after this"),
|
|
23
|
+
maxRuns: z.number().int().min(1).optional().describe("Stop after this many runs"),
|
|
24
|
+
}),
|
|
25
|
+
).describe("When each batch fires. e.g. daily at 9am in Africa/Lagos, or every weekend.");
|
|
26
|
+
|
|
27
|
+
export function registerListCampaignTools(server: McpServer): void {
|
|
28
|
+
server.registerTool(
|
|
29
|
+
"neuron_create_list_campaign",
|
|
30
|
+
{
|
|
31
|
+
title: "Create List Campaign",
|
|
32
|
+
description:
|
|
33
|
+
"Create a paced campaign that messages the next N un-reached members of a contact list on a schedule. The cursor follows the LIVE list — as members are added or removed, only current members get reached, and nobody is messaged twice per cycle. Multiple campaigns can run on the same or different lists concurrently. On exhaust, either stop or loop (start over after a cooldown).",
|
|
34
|
+
inputSchema: z.object({
|
|
35
|
+
listId: z.string().uuid().describe("The contact list to run the campaign over"),
|
|
36
|
+
name: z.string().max(255).optional().describe("Campaign name (default: 'Campaign')"),
|
|
37
|
+
sendMode: z.enum(["message", "bot"]).optional().describe("'message' sends a plain broadcast; 'bot' initiates the conversation through a bot (default: 'message')"),
|
|
38
|
+
botId: z.string().uuid().nullable().optional().describe("Bot to send through when sendMode='bot'; its channel is used"),
|
|
39
|
+
channelId: z.string().uuid().nullable().optional().describe("WhatsApp channel to send from; falls back to the org default / first connected channel"),
|
|
40
|
+
messageSource: messageSource,
|
|
41
|
+
batchSize: z.number().int().min(1).max(1000).optional().describe("How many contacts to message per run (default: 20)"),
|
|
42
|
+
schedule: schedule,
|
|
43
|
+
onExhaust: z.enum(["stop", "loop"]).optional().describe("When the whole list has been reached: 'stop' (default) completes the campaign; 'loop' starts a new cycle after loopCooldownMs"),
|
|
44
|
+
loopCooldownMs: z.number().int().min(0).nullable().optional().describe("Milliseconds to wait before looping (default: 24h)"),
|
|
45
|
+
startImmediately: z.boolean().optional().describe("Fire the first batch right away (default: true). Set false to wait for the first scheduled slot"),
|
|
46
|
+
}),
|
|
47
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
48
|
+
},
|
|
49
|
+
async (params) => {
|
|
50
|
+
try {
|
|
51
|
+
return jsonResult(await api("POST", "/list-campaigns", params));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return errorResult(error);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
server.registerTool(
|
|
59
|
+
"neuron_list_list_campaigns",
|
|
60
|
+
{
|
|
61
|
+
title: "List List Campaigns",
|
|
62
|
+
description: "List all paced list campaigns for the organization, optionally filtered to a single contact list.",
|
|
63
|
+
inputSchema: z.object({
|
|
64
|
+
listId: z.string().uuid().optional().describe("Filter to campaigns on this contact list"),
|
|
65
|
+
}),
|
|
66
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
67
|
+
},
|
|
68
|
+
async (params) => {
|
|
69
|
+
try {
|
|
70
|
+
return jsonResult(await api("GET", "/list-campaigns", undefined, { listId: params.listId }));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return errorResult(error);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
server.registerTool(
|
|
78
|
+
"neuron_get_list_campaign",
|
|
79
|
+
{
|
|
80
|
+
title: "Get List Campaign",
|
|
81
|
+
description: "Get a paced list campaign with live progress: how many members have been reached this cycle, the current list size, and how many remain.",
|
|
82
|
+
inputSchema: z.object({ id: z.string().uuid().describe("Campaign ID") }),
|
|
83
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
84
|
+
},
|
|
85
|
+
async (params) => {
|
|
86
|
+
try {
|
|
87
|
+
return jsonResult(await api("GET", `/list-campaigns/${params.id}`));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return errorResult(error);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
server.registerTool(
|
|
95
|
+
"neuron_update_list_campaign",
|
|
96
|
+
{
|
|
97
|
+
title: "Update List Campaign",
|
|
98
|
+
description: "Update a paced list campaign's name, message source, batch size, schedule, exhaust behavior, or send settings. Changing the schedule reschedules the next run.",
|
|
99
|
+
inputSchema: z.object({
|
|
100
|
+
id: z.string().uuid().describe("Campaign ID"),
|
|
101
|
+
name: z.string().max(255).optional(),
|
|
102
|
+
sendMode: z.enum(["message", "bot"]).optional(),
|
|
103
|
+
botId: z.string().uuid().nullable().optional(),
|
|
104
|
+
channelId: z.string().uuid().nullable().optional(),
|
|
105
|
+
messageSource: messageSource.optional(),
|
|
106
|
+
batchSize: z.number().int().min(1).max(1000).optional(),
|
|
107
|
+
schedule: schedule.optional(),
|
|
108
|
+
onExhaust: z.enum(["stop", "loop"]).optional(),
|
|
109
|
+
loopCooldownMs: z.number().int().min(0).nullable().optional(),
|
|
110
|
+
}),
|
|
111
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
112
|
+
},
|
|
113
|
+
async (params) => {
|
|
114
|
+
try {
|
|
115
|
+
const { id, ...body } = params;
|
|
116
|
+
return jsonResult(await api("PUT", `/list-campaigns/${id}`, body));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return errorResult(error);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
server.registerTool(
|
|
124
|
+
"neuron_pause_list_campaign",
|
|
125
|
+
{
|
|
126
|
+
title: "Pause List Campaign",
|
|
127
|
+
description: "Pause a paced list campaign. No further batches run until resumed. The cursor is preserved.",
|
|
128
|
+
inputSchema: z.object({ id: z.string().uuid().describe("Campaign ID") }),
|
|
129
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
130
|
+
},
|
|
131
|
+
async (params) => {
|
|
132
|
+
try {
|
|
133
|
+
return jsonResult(await api("POST", `/list-campaigns/${params.id}/pause`));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return errorResult(error);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
server.registerTool(
|
|
141
|
+
"neuron_resume_list_campaign",
|
|
142
|
+
{
|
|
143
|
+
title: "Resume List Campaign",
|
|
144
|
+
description: "Resume a paused paced list campaign. It picks up from where the cursor left off.",
|
|
145
|
+
inputSchema: z.object({ id: z.string().uuid().describe("Campaign ID") }),
|
|
146
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
147
|
+
},
|
|
148
|
+
async (params) => {
|
|
149
|
+
try {
|
|
150
|
+
return jsonResult(await api("POST", `/list-campaigns/${params.id}/resume`));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return errorResult(error);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
server.registerTool(
|
|
158
|
+
"neuron_run_list_campaign_now",
|
|
159
|
+
{
|
|
160
|
+
title: "Run List Campaign Now",
|
|
161
|
+
description: "Fire the next batch of a paced list campaign immediately (on the next worker tick, ~30s), regardless of its schedule. The schedule continues normally afterward.",
|
|
162
|
+
inputSchema: z.object({ id: z.string().uuid().describe("Campaign ID") }),
|
|
163
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
164
|
+
},
|
|
165
|
+
async (params) => {
|
|
166
|
+
try {
|
|
167
|
+
return jsonResult(await api("POST", `/list-campaigns/${params.id}/run-now`));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return errorResult(error);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
server.registerTool(
|
|
175
|
+
"neuron_delete_list_campaign",
|
|
176
|
+
{
|
|
177
|
+
title: "Delete List Campaign",
|
|
178
|
+
description: "Permanently delete a paced list campaign and its reached-set. Does not delete the contacts or the list.",
|
|
179
|
+
inputSchema: z.object({ id: z.string().uuid().describe("Campaign ID") }),
|
|
180
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
181
|
+
},
|
|
182
|
+
async (params) => {
|
|
183
|
+
try {
|
|
184
|
+
return jsonResult(await api("DELETE", `/list-campaigns/${params.id}`));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return errorResult(error);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { api, jsonResult, errorResult } from "../client.js";
|
|
4
|
+
|
|
5
|
+
export function registerListPoolTools(server: McpServer): void {
|
|
6
|
+
// Browse pool
|
|
7
|
+
server.registerTool(
|
|
8
|
+
"neuron_browse_list_pool",
|
|
9
|
+
{
|
|
10
|
+
title: "Browse List Pool",
|
|
11
|
+
description: "Browse verified contact lists available in the shared marketplace pool. Filter by category, region, or search term.",
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
category: z.string().optional().describe("Filter by audience category (e.g., 'delivery', 'tech', 'consumers')"),
|
|
14
|
+
region: z.string().optional().describe("Filter by geographic region (e.g., 'lagos', 'abuja')"),
|
|
15
|
+
search: z.string().optional().describe("Search term to filter by description or category"),
|
|
16
|
+
page: z.number().optional().describe("Page number for pagination (default: 1)"),
|
|
17
|
+
limit: z.number().optional().describe("Number of items per page (default: 20)"),
|
|
18
|
+
}),
|
|
19
|
+
annotations: {
|
|
20
|
+
readOnlyHint: true,
|
|
21
|
+
destructiveHint: false,
|
|
22
|
+
idempotentHint: true,
|
|
23
|
+
openWorldHint: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
async (params) => {
|
|
27
|
+
try {
|
|
28
|
+
const result = await api("GET", "/list-pool", undefined, {
|
|
29
|
+
category: params.category,
|
|
30
|
+
region: params.region,
|
|
31
|
+
search: params.search,
|
|
32
|
+
page: params.page,
|
|
33
|
+
limit: params.limit,
|
|
34
|
+
});
|
|
35
|
+
return jsonResult(result);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return errorResult(error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Submit to pool
|
|
43
|
+
server.registerTool(
|
|
44
|
+
"neuron_submit_to_list_pool",
|
|
45
|
+
{
|
|
46
|
+
title: "Submit List to Pool",
|
|
47
|
+
description: "Submit a contact list to the shared pool marketplace for other organizations to discover and use for ad distribution.",
|
|
48
|
+
inputSchema: z.object({
|
|
49
|
+
listId: z.string().describe("Unique identifier (UUID) of the contact list to submit"),
|
|
50
|
+
channelId: z.string().describe("Unique identifier (UUID) of the WhatsApp channel used for sending messages"),
|
|
51
|
+
category: z.string().describe("Audience category (e.g., 'delivery', 'tech', 'consumers', 'food')"),
|
|
52
|
+
description: z.string().optional().describe("Human-readable description of the audience and its characteristics"),
|
|
53
|
+
demographics: z.object({
|
|
54
|
+
regions: z.array(z.string()).optional().describe("Geographic regions covered (e.g., ['lagos', 'abuja'])"),
|
|
55
|
+
tags: z.array(z.string()).optional().describe("Demographic tags describing the audience (e.g., ['young-adults', 'tech-savvy'])"),
|
|
56
|
+
}).optional().describe("Demographic information about the contact list audience"),
|
|
57
|
+
pricePerDelivery: z.number().describe("Price per successful message delivery in kobo (e.g., 5000 = NGN 50)"),
|
|
58
|
+
}),
|
|
59
|
+
annotations: {
|
|
60
|
+
readOnlyHint: false,
|
|
61
|
+
destructiveHint: false,
|
|
62
|
+
idempotentHint: false,
|
|
63
|
+
openWorldHint: false,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
async (params) => {
|
|
67
|
+
try {
|
|
68
|
+
const result = await api("POST", "/list-pool", params);
|
|
69
|
+
return jsonResult(result);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return errorResult(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Create ad request
|
|
77
|
+
server.registerTool(
|
|
78
|
+
"neuron_create_ad_request",
|
|
79
|
+
{
|
|
80
|
+
title: "Create Ad Request",
|
|
81
|
+
description: "Create an ad distribution request to send a message to contacts in pool lists matching target demographics. Deducts from wallet upon approval.",
|
|
82
|
+
inputSchema: z.object({
|
|
83
|
+
message: z.string().describe("The ad message content to distribute to matched contacts"),
|
|
84
|
+
messageType: z.string().optional().describe("Message content type: 'text', 'image', 'video', or 'document' (default: 'text')"),
|
|
85
|
+
mediaUrl: z.string().optional().describe("Media URL for non-text messages (required when messageType is 'image', 'video', or 'document')"),
|
|
86
|
+
targetDemographics: z.object({
|
|
87
|
+
categories: z.array(z.string()).optional().describe("Target audience categories to match (e.g., ['delivery', 'tech'])"),
|
|
88
|
+
regions: z.array(z.string()).optional().describe("Target geographic regions to match (e.g., ['lagos', 'abuja'])"),
|
|
89
|
+
}).describe("Demographic criteria to match against pool list entries"),
|
|
90
|
+
targetGoal: z.number().describe("Target number of successful message deliveries"),
|
|
91
|
+
budget: z.number().describe("Total ad budget in kobo (e.g., 500000 = NGN 5,000)"),
|
|
92
|
+
}),
|
|
93
|
+
annotations: {
|
|
94
|
+
readOnlyHint: false,
|
|
95
|
+
destructiveHint: false,
|
|
96
|
+
idempotentHint: false,
|
|
97
|
+
openWorldHint: false,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
async (params) => {
|
|
101
|
+
try {
|
|
102
|
+
const result = await api("POST", "/ad-requests", params);
|
|
103
|
+
return jsonResult(result);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return errorResult(error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// List ad requests
|
|
111
|
+
server.registerTool(
|
|
112
|
+
"neuron_list_ad_requests",
|
|
113
|
+
{
|
|
114
|
+
title: "List Ad Requests",
|
|
115
|
+
description: "Retrieve all ad requests for the current organization with optional status filtering and pagination.",
|
|
116
|
+
inputSchema: z.object({
|
|
117
|
+
status: z.string().optional().describe("Filter by request status: 'draft', 'pending_review', 'approved', 'active', 'paused', 'completed', or 'rejected'"),
|
|
118
|
+
page: z.number().optional().describe("Page number for pagination (default: 1)"),
|
|
119
|
+
limit: z.number().optional().describe("Number of items per page (default: 20)"),
|
|
120
|
+
}),
|
|
121
|
+
annotations: {
|
|
122
|
+
readOnlyHint: true,
|
|
123
|
+
destructiveHint: false,
|
|
124
|
+
idempotentHint: true,
|
|
125
|
+
openWorldHint: false,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
async (params) => {
|
|
129
|
+
try {
|
|
130
|
+
const result = await api("GET", "/ad-requests", undefined, {
|
|
131
|
+
status: params.status,
|
|
132
|
+
page: params.page,
|
|
133
|
+
limit: params.limit,
|
|
134
|
+
});
|
|
135
|
+
return jsonResult(result);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return errorResult(error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Get ad request analytics
|
|
143
|
+
server.registerTool(
|
|
144
|
+
"neuron_get_ad_request_analytics",
|
|
145
|
+
{
|
|
146
|
+
title: "Get Ad Request Analytics",
|
|
147
|
+
description: "Retrieve delivery analytics and performance breakdown for a specific ad request, including delivery rates and cost metrics.",
|
|
148
|
+
inputSchema: z.object({
|
|
149
|
+
adRequestId: z.string().describe("Unique identifier (UUID) of the ad request to get analytics for"),
|
|
150
|
+
}),
|
|
151
|
+
annotations: {
|
|
152
|
+
readOnlyHint: true,
|
|
153
|
+
destructiveHint: false,
|
|
154
|
+
idempotentHint: true,
|
|
155
|
+
openWorldHint: false,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
async (params) => {
|
|
159
|
+
try {
|
|
160
|
+
const result = await api("GET", `/ad-requests/${params.adRequestId}/analytics`);
|
|
161
|
+
return jsonResult(result);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return errorResult(error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Get ad request by ID
|
|
169
|
+
server.registerTool(
|
|
170
|
+
"neuron_get_ad_request",
|
|
171
|
+
{
|
|
172
|
+
title: "Get Ad Request",
|
|
173
|
+
description: "Get detailed information about a specific ad request, including pool selections if targeting mode is manual.",
|
|
174
|
+
inputSchema: z.object({
|
|
175
|
+
adRequestId: z.string().describe("Unique identifier (UUID) of the ad request"),
|
|
176
|
+
}),
|
|
177
|
+
annotations: {
|
|
178
|
+
readOnlyHint: true,
|
|
179
|
+
destructiveHint: false,
|
|
180
|
+
idempotentHint: true,
|
|
181
|
+
openWorldHint: false,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
async (params) => {
|
|
185
|
+
try {
|
|
186
|
+
const result = await api("GET", `/ad-requests/${params.adRequestId}`);
|
|
187
|
+
return jsonResult(result);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return errorResult(error);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Update ad request
|
|
195
|
+
server.registerTool(
|
|
196
|
+
"neuron_update_ad_request",
|
|
197
|
+
{
|
|
198
|
+
title: "Update Ad Request",
|
|
199
|
+
description: "Update a draft or rejected ad request. Only draft and rejected ad requests can be edited. Rejected requests revert to draft on update.",
|
|
200
|
+
inputSchema: z.object({
|
|
201
|
+
adRequestId: z.string().describe("Unique identifier (UUID) of the ad request to update"),
|
|
202
|
+
message: z.string().optional().describe("Updated ad message content"),
|
|
203
|
+
messageType: z.string().optional().describe("Updated message type: 'text', 'image', 'video', or 'document'"),
|
|
204
|
+
mediaUrl: z.string().optional().describe("Updated media URL (set to null to clear)"),
|
|
205
|
+
targetDemographics: z.object({
|
|
206
|
+
categories: z.array(z.string()).optional().describe("Updated target categories"),
|
|
207
|
+
regions: z.array(z.string()).optional().describe("Updated target regions"),
|
|
208
|
+
}).optional().describe("Updated demographic targeting criteria"),
|
|
209
|
+
targetingMode: z.enum(["auto", "manual"]).optional().describe("Targeting mode: 'auto' for system matching or 'manual' for hand-picked pool entries"),
|
|
210
|
+
targetType: z.enum(["contacts", "groups"]).optional().describe("Target type: 'contacts' or 'groups'"),
|
|
211
|
+
selectedPoolEntryIds: z.array(z.string()).optional().describe("Pool entry UUIDs for manual contact targeting"),
|
|
212
|
+
selectedGroupPoolEntryIds: z.array(z.string()).optional().describe("Group pool entry UUIDs for manual group targeting"),
|
|
213
|
+
targetGoal: z.number().optional().describe("Updated target number of deliveries"),
|
|
214
|
+
budget: z.number().optional().describe("Updated budget in kobo"),
|
|
215
|
+
}),
|
|
216
|
+
annotations: {
|
|
217
|
+
readOnlyHint: false,
|
|
218
|
+
destructiveHint: false,
|
|
219
|
+
idempotentHint: true,
|
|
220
|
+
openWorldHint: false,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
async (params) => {
|
|
224
|
+
try {
|
|
225
|
+
const { adRequestId, ...body } = params;
|
|
226
|
+
const result = await api("PUT", `/ad-requests/${adRequestId}`, body);
|
|
227
|
+
return jsonResult(result);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return errorResult(error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Delete ad request
|
|
235
|
+
server.registerTool(
|
|
236
|
+
"neuron_delete_ad_request",
|
|
237
|
+
{
|
|
238
|
+
title: "Delete Ad Request",
|
|
239
|
+
description: "Permanently delete a draft ad request. Only ad requests in draft status can be deleted.",
|
|
240
|
+
inputSchema: z.object({
|
|
241
|
+
adRequestId: z.string().describe("Unique identifier (UUID) of the draft ad request to delete"),
|
|
242
|
+
}),
|
|
243
|
+
annotations: {
|
|
244
|
+
readOnlyHint: false,
|
|
245
|
+
destructiveHint: true,
|
|
246
|
+
idempotentHint: false,
|
|
247
|
+
openWorldHint: false,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
async (params) => {
|
|
251
|
+
try {
|
|
252
|
+
const result = await api("DELETE", `/ad-requests/${params.adRequestId}`);
|
|
253
|
+
return jsonResult(result);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return errorResult(error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Submit ad request for review
|
|
261
|
+
server.registerTool(
|
|
262
|
+
"neuron_submit_ad_request",
|
|
263
|
+
{
|
|
264
|
+
title: "Submit Ad Request for Review",
|
|
265
|
+
description: "Submit a draft ad request for admin review. The request must be in draft status. After submission, an admin must approve it before it can be funded.",
|
|
266
|
+
inputSchema: z.object({
|
|
267
|
+
adRequestId: z.string().describe("Unique identifier (UUID) of the draft ad request to submit"),
|
|
268
|
+
}),
|
|
269
|
+
annotations: {
|
|
270
|
+
readOnlyHint: false,
|
|
271
|
+
destructiveHint: false,
|
|
272
|
+
idempotentHint: false,
|
|
273
|
+
openWorldHint: false,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
async (params) => {
|
|
277
|
+
try {
|
|
278
|
+
const result = await api("POST", `/ad-requests/${params.adRequestId}/submit`);
|
|
279
|
+
return jsonResult(result);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return errorResult(error);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Fund ad request from wallet
|
|
287
|
+
server.registerTool(
|
|
288
|
+
"neuron_fund_ad_request",
|
|
289
|
+
{
|
|
290
|
+
title: "Fund Ad Request",
|
|
291
|
+
description: "Fund an approved ad request from the organization wallet. This debits the budget amount and activates the campaign for distribution. If wallet balance is insufficient, returns shortfall information instead of an error.",
|
|
292
|
+
inputSchema: z.object({
|
|
293
|
+
adRequestId: z.string().describe("Unique identifier (UUID) of the approved ad request to fund"),
|
|
294
|
+
}),
|
|
295
|
+
annotations: {
|
|
296
|
+
readOnlyHint: false,
|
|
297
|
+
destructiveHint: false,
|
|
298
|
+
idempotentHint: false,
|
|
299
|
+
openWorldHint: false,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
async (params) => {
|
|
303
|
+
try {
|
|
304
|
+
const result = await api("POST", `/ad-requests/${params.adRequestId}/fund`);
|
|
305
|
+
return jsonResult(result);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return errorResult(error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Pause ad request
|
|
313
|
+
server.registerTool(
|
|
314
|
+
"neuron_pause_ad_request",
|
|
315
|
+
{
|
|
316
|
+
title: "Pause Ad Request",
|
|
317
|
+
description: "Pause an active ad request campaign. Distribution stops until resumed. Only active ad requests can be paused.",
|
|
318
|
+
inputSchema: z.object({
|
|
319
|
+
adRequestId: z.string().describe("Unique identifier (UUID) of the active ad request to pause"),
|
|
320
|
+
}),
|
|
321
|
+
annotations: {
|
|
322
|
+
readOnlyHint: false,
|
|
323
|
+
destructiveHint: false,
|
|
324
|
+
idempotentHint: true,
|
|
325
|
+
openWorldHint: false,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
async (params) => {
|
|
329
|
+
try {
|
|
330
|
+
const result = await api("POST", `/ad-requests/${params.adRequestId}/pause`);
|
|
331
|
+
return jsonResult(result);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
return errorResult(error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Resume ad request
|
|
339
|
+
server.registerTool(
|
|
340
|
+
"neuron_resume_ad_request",
|
|
341
|
+
{
|
|
342
|
+
title: "Resume Ad Request",
|
|
343
|
+
description: "Resume a paused ad request campaign. Distribution resumes from where it left off. Only paused ad requests can be resumed.",
|
|
344
|
+
inputSchema: z.object({
|
|
345
|
+
adRequestId: z.string().describe("Unique identifier (UUID) of the paused ad request to resume"),
|
|
346
|
+
}),
|
|
347
|
+
annotations: {
|
|
348
|
+
readOnlyHint: false,
|
|
349
|
+
destructiveHint: false,
|
|
350
|
+
idempotentHint: true,
|
|
351
|
+
openWorldHint: false,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
async (params) => {
|
|
355
|
+
try {
|
|
356
|
+
const result = await api("POST", `/ad-requests/${params.adRequestId}/resume`);
|
|
357
|
+
return jsonResult(result);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return errorResult(error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Stop ad request
|
|
365
|
+
server.registerTool(
|
|
366
|
+
"neuron_stop_ad_request",
|
|
367
|
+
{
|
|
368
|
+
title: "Stop Ad Request",
|
|
369
|
+
description: "Stop an active or paused ad request campaign permanently. Any remaining unspent budget is refunded to the organization wallet. This cannot be undone.",
|
|
370
|
+
inputSchema: z.object({
|
|
371
|
+
adRequestId: z.string().describe("Unique identifier (UUID) of the ad request to stop"),
|
|
372
|
+
}),
|
|
373
|
+
annotations: {
|
|
374
|
+
readOnlyHint: false,
|
|
375
|
+
destructiveHint: true,
|
|
376
|
+
idempotentHint: false,
|
|
377
|
+
openWorldHint: false,
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
async (params) => {
|
|
381
|
+
try {
|
|
382
|
+
const result = await api("POST", `/ad-requests/${params.adRequestId}/stop`);
|
|
383
|
+
return jsonResult(result);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
return errorResult(error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { jsonResult, errorResult, getAuthToken, getBaseUrl } from "../client.js";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import FormData from "form-data";
|
|
6
|
+
|
|
7
|
+
export function registerMediaTools(server: McpServer): void {
|
|
8
|
+
// Upload media
|
|
9
|
+
server.registerTool(
|
|
10
|
+
"neuron_upload_media",
|
|
11
|
+
{
|
|
12
|
+
title: "Upload Media",
|
|
13
|
+
description: "Upload a media file (image, video, audio, or document) by providing its base64-encoded content. Returns a URL for use in messages and broadcasts.",
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
base64: z.string().describe("Base64-encoded file content (without data URI prefix)"),
|
|
16
|
+
filename: z.string().describe("Original filename including extension (e.g., 'photo.jpg', 'report.pdf')"),
|
|
17
|
+
mimetype: z.string().describe("MIME type of the file (e.g., 'image/jpeg', 'image/png', 'application/pdf', 'video/mp4')"),
|
|
18
|
+
}),
|
|
19
|
+
annotations: {
|
|
20
|
+
readOnlyHint: false,
|
|
21
|
+
destructiveHint: false,
|
|
22
|
+
idempotentHint: false,
|
|
23
|
+
openWorldHint: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
async (params) => {
|
|
27
|
+
try {
|
|
28
|
+
const buffer = Buffer.from(params.base64, "base64");
|
|
29
|
+
const form = new FormData();
|
|
30
|
+
form.append("file", buffer, {
|
|
31
|
+
filename: params.filename,
|
|
32
|
+
contentType: params.mimetype,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const token = getAuthToken();
|
|
36
|
+
const response = await axios.post(`${getBaseUrl()}/media/upload`, form, {
|
|
37
|
+
headers: {
|
|
38
|
+
...form.getHeaders(),
|
|
39
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
40
|
+
},
|
|
41
|
+
timeout: 60000,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return jsonResult(response.data);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return errorResult(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|