neuron-mcp-server 1.0.1 → 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.
Files changed (150) hide show
  1. package/.env +13 -0
  2. package/AI_PERSONALITY_DESIGN_RESEARCH.md +1020 -0
  3. package/Dockerfile +18 -0
  4. package/README.md +146 -131
  5. package/dist/client.d.ts +6 -0
  6. package/dist/client.js +50 -12
  7. package/dist/client.js.map +1 -1
  8. package/dist/index.js +51 -7
  9. package/dist/index.js.map +1 -1
  10. package/dist/tools/ai.js +9 -3
  11. package/dist/tools/ai.js.map +1 -1
  12. package/dist/tools/approvals.d.ts +2 -0
  13. package/dist/tools/approvals.js +181 -0
  14. package/dist/tools/approvals.js.map +1 -0
  15. package/dist/tools/audit.js +7 -4
  16. package/dist/tools/audit.js.map +1 -1
  17. package/dist/tools/auth.js +119 -19
  18. package/dist/tools/auth.js.map +1 -1
  19. package/dist/tools/billing.js +22 -6
  20. package/dist/tools/billing.js.map +1 -1
  21. package/dist/tools/blog.js +98 -34
  22. package/dist/tools/blog.js.map +1 -1
  23. package/dist/tools/bot-api-keys.js +27 -12
  24. package/dist/tools/bot-api-keys.js.map +1 -1
  25. package/dist/tools/bot-api.js +53 -27
  26. package/dist/tools/bot-api.js.map +1 -1
  27. package/dist/tools/bots.js +116 -68
  28. package/dist/tools/bots.js.map +1 -1
  29. package/dist/tools/broadcasts.js +78 -38
  30. package/dist/tools/broadcasts.js.map +1 -1
  31. package/dist/tools/builtin-tools.js +19 -12
  32. package/dist/tools/builtin-tools.js.map +1 -1
  33. package/dist/tools/campaigns.js +149 -61
  34. package/dist/tools/campaigns.js.map +1 -1
  35. package/dist/tools/channels.js +163 -53
  36. package/dist/tools/channels.js.map +1 -1
  37. package/dist/tools/contact-lists.js +111 -51
  38. package/dist/tools/contact-lists.js.map +1 -1
  39. package/dist/tools/contacts.js +117 -44
  40. package/dist/tools/contacts.js.map +1 -1
  41. package/dist/tools/conversations.js +95 -50
  42. package/dist/tools/conversations.js.map +1 -1
  43. package/dist/tools/flows.d.ts +2 -0
  44. package/dist/tools/flows.js +242 -0
  45. package/dist/tools/flows.js.map +1 -0
  46. package/dist/tools/group-management.d.ts +2 -0
  47. package/dist/tools/group-management.js +239 -0
  48. package/dist/tools/group-management.js.map +1 -0
  49. package/dist/tools/leads.d.ts +2 -0
  50. package/dist/tools/leads.js +100 -0
  51. package/dist/tools/leads.js.map +1 -0
  52. package/dist/tools/list-campaigns.d.ts +2 -0
  53. package/dist/tools/list-campaigns.js +152 -0
  54. package/dist/tools/list-campaigns.js.map +1 -0
  55. package/dist/tools/list-pool.js +250 -33
  56. package/dist/tools/list-pool.js.map +1 -1
  57. package/dist/tools/media.js +12 -7
  58. package/dist/tools/media.js.map +1 -1
  59. package/dist/tools/newsletters.js +106 -26
  60. package/dist/tools/newsletters.js.map +1 -1
  61. package/dist/tools/organizations.js +76 -35
  62. package/dist/tools/organizations.js.map +1 -1
  63. package/dist/tools/outbound-webhooks.js +32 -20
  64. package/dist/tools/outbound-webhooks.js.map +1 -1
  65. package/dist/tools/payouts.js +44 -17
  66. package/dist/tools/payouts.js.map +1 -1
  67. package/dist/tools/personas.d.ts +2 -0
  68. package/dist/tools/personas.js +141 -0
  69. package/dist/tools/personas.js.map +1 -0
  70. package/dist/tools/polls.d.ts +2 -0
  71. package/dist/tools/polls.js +30 -0
  72. package/dist/tools/polls.js.map +1 -0
  73. package/dist/tools/pool.js +101 -43
  74. package/dist/tools/pool.js.map +1 -1
  75. package/dist/tools/products.d.ts +2 -0
  76. package/dist/tools/products.js +277 -0
  77. package/dist/tools/products.js.map +1 -0
  78. package/dist/tools/profile-privacy.d.ts +2 -0
  79. package/dist/tools/profile-privacy.js +195 -0
  80. package/dist/tools/profile-privacy.js.map +1 -0
  81. package/dist/tools/reflections.js +25 -13
  82. package/dist/tools/reflections.js.map +1 -1
  83. package/dist/tools/scheduled-messages.d.ts +2 -0
  84. package/dist/tools/scheduled-messages.js +124 -0
  85. package/dist/tools/scheduled-messages.js.map +1 -0
  86. package/dist/tools/social-channels.d.ts +2 -0
  87. package/dist/tools/social-channels.js +318 -0
  88. package/dist/tools/social-channels.js.map +1 -0
  89. package/dist/tools/tasks.d.ts +2 -0
  90. package/dist/tools/tasks.js +155 -0
  91. package/dist/tools/tasks.js.map +1 -0
  92. package/dist/tools/tools.js +86 -40
  93. package/dist/tools/tools.js.map +1 -1
  94. package/dist/tools/wallets.js +34 -12
  95. package/dist/tools/wallets.js.map +1 -1
  96. package/dist/tools/webhooks.js +35 -20
  97. package/dist/tools/webhooks.js.map +1 -1
  98. package/dist/tools/whatsapp-actions.d.ts +2 -0
  99. package/dist/tools/whatsapp-actions.js +186 -0
  100. package/dist/tools/whatsapp-actions.js.map +1 -0
  101. package/package.json +3 -31
  102. package/scripts/setup-delivahere-bot.ts +804 -0
  103. package/scripts/test-delivahere-bot.ts +128 -0
  104. package/scripts/update-letschop-prompt.ts +194 -0
  105. package/server.json +26 -0
  106. package/smithery.yaml +19 -0
  107. package/src/client.ts +169 -0
  108. package/src/index.ts +256 -0
  109. package/src/tools/ai.ts +35 -0
  110. package/src/tools/approvals.ts +231 -0
  111. package/src/tools/audit.ts +49 -0
  112. package/src/tools/auth.ts +536 -0
  113. package/src/tools/billing.ts +79 -0
  114. package/src/tools/blog.ts +264 -0
  115. package/src/tools/bot-api-keys.ts +87 -0
  116. package/src/tools/bot-api.ts +193 -0
  117. package/src/tools/bots.ts +320 -0
  118. package/src/tools/broadcasts.ts +224 -0
  119. package/src/tools/builtin-tools.ts +108 -0
  120. package/src/tools/campaigns.ts +429 -0
  121. package/src/tools/channels.ts +520 -0
  122. package/src/tools/contact-lists.ts +320 -0
  123. package/src/tools/contacts.ts +395 -0
  124. package/src/tools/conversations.ts +349 -0
  125. package/src/tools/flows.ts +298 -0
  126. package/src/tools/group-management.ts +265 -0
  127. package/src/tools/knowledge-bases.ts +451 -0
  128. package/src/tools/leads.ts +104 -0
  129. package/src/tools/list-campaigns.ts +190 -0
  130. package/src/tools/list-pool.ts +389 -0
  131. package/src/tools/media.ts +50 -0
  132. package/src/tools/newsletters.ts +271 -0
  133. package/src/tools/organizations.ts +287 -0
  134. package/src/tools/outbound-webhooks.ts +181 -0
  135. package/src/tools/payouts.ts +133 -0
  136. package/src/tools/personas.ts +145 -0
  137. package/src/tools/polls.ts +32 -0
  138. package/src/tools/pool.ts +293 -0
  139. package/src/tools/products.ts +280 -0
  140. package/src/tools/profile-privacy.ts +215 -0
  141. package/src/tools/reflections.ts +124 -0
  142. package/src/tools/scheduled-messages.ts +144 -0
  143. package/src/tools/social-channels.ts +396 -0
  144. package/src/tools/tasks.ts +198 -0
  145. package/src/tools/tools.ts +255 -0
  146. package/src/tools/wallets.ts +107 -0
  147. package/src/tools/webhooks.ts +166 -0
  148. package/src/tools/whatsapp-actions.ts +203 -0
  149. package/tsconfig.json +20 -0
  150. package/LICENSE +0 -21
@@ -0,0 +1,349 @@
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
+ /**
6
+ * Register conversation-related MCP tools
7
+ */
8
+ export function registerConversationTools(server: McpServer): void {
9
+ // List conversations for a bot
10
+ server.registerTool(
11
+ "neuron_list_conversations",
12
+ {
13
+ title: "List Conversations",
14
+ description: "Retrieve a paginated list of conversations for a specific bot, with optional filtering by status.",
15
+ inputSchema: z.object({
16
+ botId: z.string().describe("Unique identifier (UUID) of the bot whose conversations to list"),
17
+ status: z.string().optional().describe("Filter by conversation status (e.g., 'active', 'closed')"),
18
+ cursor: z.string().optional().describe("Opaque pagination cursor from a previous response for retrieving the next page"),
19
+ limit: z.number().int().positive().optional().describe("Maximum number of conversations to return per page"),
20
+ }),
21
+ annotations: {
22
+ readOnlyHint: true,
23
+ destructiveHint: false,
24
+ idempotentHint: true,
25
+ openWorldHint: false,
26
+ },
27
+ },
28
+ async (params) => {
29
+ try {
30
+ const result = await api("GET", `conversations/bot/${params.botId}`, undefined, {
31
+ status: params.status,
32
+ cursor: params.cursor,
33
+ limit: params.limit,
34
+ });
35
+ return jsonResult(result);
36
+ } catch (error) {
37
+ return errorResult(error);
38
+ }
39
+ }
40
+ );
41
+
42
+ // Get a specific conversation
43
+ server.registerTool(
44
+ "neuron_get_conversation",
45
+ {
46
+ title: "Get Conversation",
47
+ description: "Retrieve detailed information about a specific conversation, including its status, participants, and metadata.",
48
+ inputSchema: z.object({
49
+ id: z.string().describe("Unique identifier (UUID) of the conversation"),
50
+ }),
51
+ annotations: {
52
+ readOnlyHint: true,
53
+ destructiveHint: false,
54
+ idempotentHint: true,
55
+ openWorldHint: false,
56
+ },
57
+ },
58
+ async (params) => {
59
+ try {
60
+ const result = await api("GET", `conversations/${params.id}`);
61
+ return jsonResult(result);
62
+ } catch (error) {
63
+ return errorResult(error);
64
+ }
65
+ }
66
+ );
67
+
68
+ // Get messages from a conversation
69
+ server.registerTool(
70
+ "neuron_get_messages",
71
+ {
72
+ title: "Get Conversation Messages",
73
+ description: "Retrieve messages from a specific conversation in chronological order, with optional cursor-based pagination.",
74
+ inputSchema: z.object({
75
+ id: z.string().describe("Unique identifier (UUID) of the conversation"),
76
+ cursor: z.string().optional().describe("Opaque pagination cursor from a previous response for retrieving the next page"),
77
+ limit: z.number().int().positive().optional().describe("Maximum number of messages to return per page"),
78
+ }),
79
+ annotations: {
80
+ readOnlyHint: true,
81
+ destructiveHint: false,
82
+ idempotentHint: true,
83
+ openWorldHint: false,
84
+ },
85
+ },
86
+ async (params) => {
87
+ try {
88
+ const result = await api("GET", `conversations/${params.id}/messages`, undefined, {
89
+ cursor: params.cursor,
90
+ limit: params.limit,
91
+ });
92
+ return jsonResult(result);
93
+ } catch (error) {
94
+ return errorResult(error);
95
+ }
96
+ }
97
+ );
98
+
99
+ // Takeover a conversation
100
+ server.registerTool(
101
+ "neuron_takeover_conversation",
102
+ {
103
+ title: "Takeover Conversation",
104
+ description: "Transfer control of a conversation from the bot to a human agent. The bot will stop auto-responding until the conversation is released.",
105
+ inputSchema: z.object({
106
+ id: z.string().describe("Unique identifier (UUID) of the conversation to take over"),
107
+ }),
108
+ annotations: {
109
+ readOnlyHint: false,
110
+ destructiveHint: false,
111
+ idempotentHint: true,
112
+ openWorldHint: false,
113
+ },
114
+ },
115
+ async (params) => {
116
+ try {
117
+ const result = await api("POST", `conversations/${params.id}/takeover`);
118
+ return jsonResult(result);
119
+ } catch (error) {
120
+ return errorResult(error);
121
+ }
122
+ }
123
+ );
124
+
125
+ // Release a conversation
126
+ server.registerTool(
127
+ "neuron_release_conversation",
128
+ {
129
+ title: "Release Conversation",
130
+ description: "Release a conversation back to the bot, returning control from a human agent. The bot will resume auto-responding.",
131
+ inputSchema: z.object({
132
+ id: z.string().describe("Unique identifier (UUID) of the conversation to release"),
133
+ }),
134
+ annotations: {
135
+ readOnlyHint: false,
136
+ destructiveHint: false,
137
+ idempotentHint: true,
138
+ openWorldHint: false,
139
+ },
140
+ },
141
+ async (params) => {
142
+ try {
143
+ const result = await api("POST", `conversations/${params.id}/release`);
144
+ return jsonResult(result);
145
+ } catch (error) {
146
+ return errorResult(error);
147
+ }
148
+ }
149
+ );
150
+
151
+ // Send a message to a conversation
152
+ server.registerTool(
153
+ "neuron_send_message",
154
+ {
155
+ title: "Send Message",
156
+ description: "Send a message to an existing conversation. Supports text, image, and document message types with optional media attachments.",
157
+ inputSchema: z.object({
158
+ id: z.string().describe("Unique identifier (UUID) of the conversation"),
159
+ content: z.string().describe("Message content to send"),
160
+ messageType: z.string().optional().describe("Type of message: 'text' (default), 'image', or 'document'"),
161
+ mediaUrl: z.string().optional().describe("URL of media to attach (required for image/document message types)"),
162
+ }),
163
+ annotations: {
164
+ readOnlyHint: false,
165
+ destructiveHint: false,
166
+ idempotentHint: false,
167
+ openWorldHint: false,
168
+ },
169
+ },
170
+ async (params) => {
171
+ try {
172
+ const body: Record<string, unknown> = { content: params.content };
173
+ if (params.messageType) body.messageType = params.messageType;
174
+ if (params.mediaUrl) body.mediaUrl = params.mediaUrl;
175
+ const result = await api("POST", `conversations/${params.id}/send`, body);
176
+ return jsonResult(result);
177
+ } catch (error) {
178
+ return errorResult(error);
179
+ }
180
+ }
181
+ );
182
+
183
+ // Compose a new message (start new conversation)
184
+ server.registerTool(
185
+ "neuron_compose_message",
186
+ {
187
+ title: "Compose Message",
188
+ description: "Compose and send a new message to a phone number via a specific channel. Automatically creates a new conversation if one does not exist.",
189
+ annotations: {
190
+ readOnlyHint: false,
191
+ destructiveHint: false,
192
+ idempotentHint: false,
193
+ openWorldHint: false,
194
+ },
195
+ inputSchema: z.object({
196
+ channelId: z.string().describe("Unique identifier (UUID) of the WhatsApp channel to send through"),
197
+ to: z.string().describe("Recipient phone number (E.164 format, e.g., '2348012345678')"),
198
+ text: z.string().describe("Message text content"),
199
+ messageType: z.string().optional().describe("Type of message: 'text' (default), 'image', or 'document'"),
200
+ mediaUrl: z.string().optional().describe("URL of media to attach (required for non-text message types)"),
201
+ contactName: z.string().optional().describe("Display name for the recipient contact"),
202
+ sendAt: z.string().optional().describe("ISO 8601 date-time for scheduled delivery (e.g., '2025-12-31T10:00:00Z'). Message sends immediately if omitted."),
203
+ }),
204
+ },
205
+ async (params) => {
206
+ try {
207
+ const body: Record<string, unknown> = {
208
+ channelId: params.channelId,
209
+ to: params.to,
210
+ text: params.text,
211
+ };
212
+ if (params.messageType) body.messageType = params.messageType;
213
+ if (params.mediaUrl) body.mediaUrl = params.mediaUrl;
214
+ if (params.contactName) body.contactName = params.contactName;
215
+ if (params.sendAt) body.sendAt = params.sendAt;
216
+ const result = await api("POST", "conversations/compose", body);
217
+ return jsonResult(result);
218
+ } catch (error) {
219
+ return errorResult(error);
220
+ }
221
+ }
222
+ );
223
+
224
+ // Simple WhatsApp send — auto-resolves channel
225
+ server.registerTool(
226
+ "neuron_send_whatsapp",
227
+ {
228
+ title: "Send WhatsApp Message",
229
+ description:
230
+ "Send a WhatsApp message to a phone number or group. Auto-resolves which channel to use " +
231
+ "(org default > first connected). Supports text, image, audio, video, and document types.",
232
+ annotations: {
233
+ readOnlyHint: false,
234
+ destructiveHint: false,
235
+ idempotentHint: false,
236
+ openWorldHint: false,
237
+ },
238
+ inputSchema: z.object({
239
+ to: z.string().describe("Recipient: phone number (e.g., '2348012345678') or group JID (e.g., '120363XXX@g.us')"),
240
+ text: z.string().describe("Message text content"),
241
+ messageType: z.string().optional().describe("Message type: 'text' (default), 'image', 'audio', 'video', or 'document'"),
242
+ mediaUrl: z.string().optional().describe("URL of media to attach (required for non-text message types)"),
243
+ channelId: z.string().optional().describe("Unique identifier (UUID) of a specific channel to override auto-resolution"),
244
+ contactName: z.string().optional().describe("Display name for the recipient contact"),
245
+ sendAt: z.string().optional().describe("ISO 8601 date-time for scheduled delivery (e.g., '2025-12-31T10:00:00Z'). Message sends immediately if omitted."),
246
+ }),
247
+ },
248
+ async (params) => {
249
+ try {
250
+ const body: Record<string, unknown> = {
251
+ to: params.to,
252
+ text: params.text,
253
+ };
254
+ if (params.messageType) body.messageType = params.messageType;
255
+ if (params.mediaUrl) body.mediaUrl = params.mediaUrl;
256
+ if (params.channelId) body.channelId = params.channelId;
257
+ if (params.contactName) body.contactName = params.contactName;
258
+ if (params.sendAt) body.sendAt = params.sendAt;
259
+ const result = await api("POST", "conversations/send-message", body);
260
+ return jsonResult(result);
261
+ } catch (error) {
262
+ return errorResult(error);
263
+ }
264
+ }
265
+ );
266
+
267
+ // Edit a message
268
+ server.registerTool(
269
+ "neuron_edit_message",
270
+ {
271
+ title: "Edit Message",
272
+ description: "Edit the content of a previously sent message in a conversation. Only the message sender can edit their messages.",
273
+ annotations: {
274
+ readOnlyHint: false,
275
+ destructiveHint: false,
276
+ idempotentHint: true,
277
+ openWorldHint: false,
278
+ },
279
+ inputSchema: z.object({
280
+ id: z.string().describe("Unique identifier (UUID) of the conversation"),
281
+ msgId: z.string().describe("Unique identifier (UUID) of the message to edit"),
282
+ content: z.string().describe("New text content to replace the existing message"),
283
+ }),
284
+ },
285
+ async (params) => {
286
+ try {
287
+ const result = await api("PUT", `conversations/${params.id}/messages/${params.msgId}`, {
288
+ content: params.content,
289
+ });
290
+ return jsonResult(result);
291
+ } catch (error) {
292
+ return errorResult(error);
293
+ }
294
+ }
295
+ );
296
+
297
+ // Delete a message
298
+ server.registerTool(
299
+ "neuron_delete_message",
300
+ {
301
+ title: "Delete Message",
302
+ description: "Permanently delete a specific message from a conversation. This action cannot be undone.",
303
+ inputSchema: z.object({
304
+ id: z.string().describe("Unique identifier (UUID) of the conversation"),
305
+ msgId: z.string().describe("Unique identifier (UUID) of the message to delete"),
306
+ }),
307
+ annotations: {
308
+ readOnlyHint: false,
309
+ destructiveHint: true,
310
+ idempotentHint: true,
311
+ openWorldHint: false,
312
+ },
313
+ },
314
+ async (params) => {
315
+ try {
316
+ const result = await api("DELETE", `conversations/${params.id}/messages/${params.msgId}`);
317
+ return jsonResult(result);
318
+ } catch (error) {
319
+ return errorResult(error);
320
+ }
321
+ }
322
+ );
323
+
324
+ // Close a conversation
325
+ server.registerTool(
326
+ "neuron_close_conversation",
327
+ {
328
+ title: "Close Conversation",
329
+ description: "Close an active conversation, marking it as completed. The bot will stop responding and the conversation is archived.",
330
+ inputSchema: z.object({
331
+ id: z.string().describe("Unique identifier (UUID) of the conversation to close"),
332
+ }),
333
+ annotations: {
334
+ readOnlyHint: false,
335
+ destructiveHint: false,
336
+ idempotentHint: true,
337
+ openWorldHint: false,
338
+ },
339
+ },
340
+ async (params) => {
341
+ try {
342
+ const result = await api("POST", `conversations/${params.id}/close`);
343
+ return jsonResult(result);
344
+ } catch (error) {
345
+ return errorResult(error);
346
+ }
347
+ }
348
+ );
349
+ }
@@ -0,0 +1,298 @@
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 READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
6
+ const WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
7
+ const DELETE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
8
+
9
+ /* ── Shared schemas ────────────────────────────────────────────────── */
10
+
11
+ const nodeSchema = z.object({
12
+ id: z.string().describe("Stable node id unique within the flow, e.g. 'n_a1b2' or 't_msg'."),
13
+ type: z.string().describe("Registry key — e.g. 'trigger.message_received', 'logic.branch', 'action.send_message'. Use neuron_get_flow_catalog to see all available types."),
14
+ config: z.record(z.unknown()).default({}).describe("Node-specific configuration. Values may contain {{…}} templates resolved at runtime against trigger, vars, and nodes."),
15
+ position: z.object({ x: z.number(), y: z.number() }).optional().describe("Canvas position (UI layout). Use x=300 as center, space nodes ~160px apart vertically."),
16
+ label: z.string().optional().describe("Human label overriding the registry default."),
17
+ });
18
+
19
+ const edgeSchema = z.object({
20
+ id: z.string().describe("Unique edge id, e.g. 'e_msg_reply'."),
21
+ source: z.string().describe("Source node id."),
22
+ target: z.string().describe("Target node id."),
23
+ sourceHandle: z.string().optional().describe("Which output port of the source node this edge leaves from. Branch nodes use 'true'/'false', switch/router use case ids, wait nodes use 'default'/'timeout', lookup uses 'found'/'not_found'. Defaults to 'default'."),
24
+ });
25
+
26
+ const graphSchema = z.object({
27
+ nodes: z.array(nodeSchema).describe("Array of flow nodes."),
28
+ edges: z.array(edgeSchema).describe("Array of edges connecting nodes. Each edge fires when its source node emits on the matching sourceHandle."),
29
+ });
30
+
31
+ /* ── Registration ──────────────────────────────────────────────────── */
32
+
33
+ export function registerFlowTools(server: McpServer): void {
34
+ /* ── Catalog ─────────────────────────────────────────────────────── */
35
+
36
+ server.registerTool(
37
+ "neuron_get_flow_catalog",
38
+ {
39
+ title: "Get Flow Node Catalog",
40
+ description:
41
+ "Retrieve the COMPLETE catalog for authoring flows. ALWAYS call this first when building or editing a flow — do not rely on memory, the full node set changes. The response has two parts:\n" +
42
+ " • `data`: every node type (all triggers, logic, and actions) with its config `fields` (including `options` enums and `required`), output `handles`, and `dynamicHandles`.\n" +
43
+ " • `reference`: the authoring guide you need for anything non-trivial —\n" +
44
+ " - `conditions`: the ConditionGroup JSON shape + the full operator vocabulary (used by branch/filter/switch/loop and trigger filters). Note: on LOGIC nodes a rule's `field` is a {{template}}; on a TRIGGER's config.conditions it is a raw payload path.\n" +
45
+ " - `compositeFields`: the exact JSON for field kinds the flat schema can't express — `cases` (switch), `assignments` (set_fields), `weekdays`, and the delay amount/unit note.\n" +
46
+ " - `triggerConditions`: every trigger also accepts an optional config.conditions AND/OR filter over the raw event payload.\n" +
47
+ " - `templating`, `errorHandling` (__errorHandling), `handles` (wiring map), and `examples` (simple, branching AND/OR, and complex switch+set_fields+expression graphs to imitate).\n" +
48
+ "The catalog is grouped by kind: trigger (entry points), logic (routing/waits/data), action (side effects). There are dozens of node types — read `data` for the authoritative list rather than assuming.",
49
+ inputSchema: z.object({}),
50
+ annotations: READ,
51
+ },
52
+ async () => {
53
+ try {
54
+ return jsonResult(await api("GET", "/flows/catalog"));
55
+ } catch (error) {
56
+ return errorResult(error);
57
+ }
58
+ },
59
+ );
60
+
61
+ /* ── CRUD ────────────────────────────────────────────────────────── */
62
+
63
+ server.registerTool(
64
+ "neuron_list_flows",
65
+ {
66
+ title: "List Flows",
67
+ description: "List all automation flows in the organization. Returns each flow's id, name, description, enabled status, trigger events, run count, and last run time.",
68
+ inputSchema: z.object({}),
69
+ annotations: READ,
70
+ },
71
+ async () => {
72
+ try {
73
+ return jsonResult(await api("GET", "/flows"));
74
+ } catch (error) {
75
+ return errorResult(error);
76
+ }
77
+ },
78
+ );
79
+
80
+ server.registerTool(
81
+ "neuron_create_flow",
82
+ {
83
+ title: "Create Flow",
84
+ description:
85
+ "Create a new automation flow. A flow is a directed graph of nodes connected by edges. " +
86
+ "The graph MUST start with at least one trigger node (entry point). Edges connect nodes via output handles.\n\n" +
87
+ "BEFORE BUILDING: call neuron_get_flow_catalog and read its `reference` — it carries the JSON shapes you cannot guess: the ConditionGroup + operator list for branch/filter/switch conditions, the `cases`/`assignments`/`weekdays` composite shapes, the handle wiring map, and copy-ready `examples`. Condition `field` on a logic node must be a {{template}} (e.g. '{{trigger.text}}'); on a trigger's config.conditions it is a raw payload path (e.g. 'text').\n\n" +
88
+ "AFTER SAVING: the flow always saves, but the response includes a `warnings` array flagging likely bugs (unknown node type, missing required field, malformed conditions/cases/assignments, edges wired to a handle a node never emits, missing trigger). Always check it and fix any warnings — a warning means that part will silently no-op at run time.\n\n" +
89
+ "TEMPLATE RESOLUTION: Node config values support {{…}} templates resolved at runtime:\n" +
90
+ " {{trigger.text}} — the triggering message text\n" +
91
+ " {{trigger.contactPhone}} — the sender's phone\n" +
92
+ " {{trigger.senderName}} — the sender's name\n" +
93
+ " {{vars.myVar}} — a variable set by logic.set_variable\n" +
94
+ " {{nodes.n_abc.output.text}} — output from a previous node\n" +
95
+ " {{= amount * 1.1 }} — inline expression (arithmetic, comparisons, ternary)\n\n" +
96
+ "ERROR HANDLING: Any node's config can include __errorHandling: { continueOnFail: true, retryCount: 3, retryDelayMs: 1000 } " +
97
+ "to retry on failure with exponential backoff and/or continue on the 'error' handle instead of failing the run.\n\n" +
98
+ "WIRING RULES:\n" +
99
+ " - logic.branch: 'true' and 'false' handles\n" +
100
+ " - logic.switch / logic.ai_router: one handle per case id, plus 'default'\n" +
101
+ " - logic.wait_for_reply: 'default' (replied) and 'timeout'\n" +
102
+ " - logic.wait_for_approval: 'approved', 'rejected', 'timeout'\n" +
103
+ " - logic.wait_first_of: 'reply', 'event', 'timeout'\n" +
104
+ " - logic.loop: 'loop' (body) and 'done'\n" +
105
+ " - action.lookup_contact: 'found' and 'not_found'\n" +
106
+ " - action.call_flow: 'default' (returned), 'timeout', 'error'\n" +
107
+ " - All other nodes: 'default' handle (or 'error' when continueOnFail is on)",
108
+ inputSchema: z.object({
109
+ name: z.string().describe("Human-readable name for the flow."),
110
+ description: z.string().optional().describe("Optional description of what the flow does."),
111
+ graph: graphSchema.optional().describe("The flow graph — nodes and edges. Omit to create an empty flow (edit later in the UI)."),
112
+ enabled: z.boolean().optional().describe("Whether the flow should be active immediately (default: false)."),
113
+ }),
114
+ annotations: WRITE,
115
+ },
116
+ async ({ name, ...rest }) => {
117
+ try {
118
+ return jsonResult(await api("POST", "/flows", { name, ...rest }));
119
+ } catch (error) {
120
+ return errorResult(error);
121
+ }
122
+ },
123
+ );
124
+
125
+ server.registerTool(
126
+ "neuron_get_flow",
127
+ {
128
+ title: "Get Flow",
129
+ description: "Retrieve a single flow by id, including its full graph (nodes and edges), enabled status, and run stats.",
130
+ inputSchema: z.object({
131
+ id: z.string().describe("UUID of the flow."),
132
+ }),
133
+ annotations: READ,
134
+ },
135
+ async ({ id }) => {
136
+ try {
137
+ return jsonResult(await api("GET", `/flows/${id}`));
138
+ } catch (error) {
139
+ return errorResult(error);
140
+ }
141
+ },
142
+ );
143
+
144
+ server.registerTool(
145
+ "neuron_update_flow",
146
+ {
147
+ title: "Update Flow",
148
+ description:
149
+ "Update a flow's name, description, graph, or enabled state. Only provided fields are changed. " +
150
+ "When updating the graph, provide the COMPLETE graph (all nodes and edges) — it replaces the existing graph entirely. " +
151
+ "When a graph is supplied the response includes a non-blocking `warnings` array (same checks as create_flow) — check it and fix any flagged issues.",
152
+ inputSchema: z.object({
153
+ id: z.string().describe("UUID of the flow to update."),
154
+ name: z.string().optional().describe("New name."),
155
+ description: z.string().nullable().optional().describe("New description (null to clear)."),
156
+ graph: graphSchema.optional().describe("Complete replacement graph. Provide ALL nodes and edges."),
157
+ enabled: z.boolean().optional().describe("Enable or disable the flow."),
158
+ }),
159
+ annotations: WRITE,
160
+ },
161
+ async ({ id, ...body }) => {
162
+ try {
163
+ return jsonResult(await api("PUT", `/flows/${id}`, body));
164
+ } catch (error) {
165
+ return errorResult(error);
166
+ }
167
+ },
168
+ );
169
+
170
+ server.registerTool(
171
+ "neuron_delete_flow",
172
+ {
173
+ title: "Delete Flow",
174
+ description: "Soft-delete a flow. It stops running but can be restored. Running executions are not cancelled.",
175
+ inputSchema: z.object({
176
+ id: z.string().describe("UUID of the flow to delete."),
177
+ }),
178
+ annotations: DELETE,
179
+ },
180
+ async ({ id }) => {
181
+ try {
182
+ return jsonResult(await api("DELETE", `/flows/${id}`));
183
+ } catch (error) {
184
+ return errorResult(error);
185
+ }
186
+ },
187
+ );
188
+
189
+ server.registerTool(
190
+ "neuron_toggle_flow",
191
+ {
192
+ title: "Toggle Flow",
193
+ description: "Enable or disable a flow. Disabled flows stop receiving events but retain their graph.",
194
+ inputSchema: z.object({
195
+ id: z.string().describe("UUID of the flow."),
196
+ enabled: z.boolean().describe("true to enable, false to disable."),
197
+ }),
198
+ annotations: WRITE,
199
+ },
200
+ async ({ id, enabled }) => {
201
+ try {
202
+ return jsonResult(await api("POST", `/flows/${id}/toggle`, { enabled }));
203
+ } catch (error) {
204
+ return errorResult(error);
205
+ }
206
+ },
207
+ );
208
+
209
+ /* ── Execution ───────────────────────────────────────────────────── */
210
+
211
+ server.registerTool(
212
+ "neuron_run_flow",
213
+ {
214
+ title: "Run Flow",
215
+ description:
216
+ "Manually trigger a flow run for testing. Starts at the 'trigger.manual' node if present, otherwise the first trigger node. " +
217
+ "Optionally provide a payload object that becomes {{trigger.*}} in the flow.",
218
+ inputSchema: z.object({
219
+ id: z.string().describe("UUID of the flow to run."),
220
+ payload: z.record(z.unknown()).optional().describe("Test payload available as {{trigger.*}} in the flow — e.g. { text: 'hello', contactPhone: '2348012345678', senderName: 'Test User' }."),
221
+ nodeId: z.string().optional().describe("Specific trigger node id to start from (for flows with multiple triggers)."),
222
+ }),
223
+ annotations: WRITE,
224
+ },
225
+ async ({ id, ...body }) => {
226
+ try {
227
+ return jsonResult(await api("POST", `/flows/${id}/run`, body));
228
+ } catch (error) {
229
+ return errorResult(error);
230
+ }
231
+ },
232
+ );
233
+
234
+ server.registerTool(
235
+ "neuron_list_flow_runs",
236
+ {
237
+ title: "List Flow Runs",
238
+ description: "List execution runs for a flow, newest first. Each run shows status (running/waiting/completed/failed/cancelled), trigger type, step count, and timing.",
239
+ inputSchema: z.object({
240
+ id: z.string().describe("UUID of the flow."),
241
+ page: z.number().int().optional().describe("Page number (default: 1)."),
242
+ limit: z.number().int().optional().describe("Items per page (default: 20, max: 100)."),
243
+ }),
244
+ annotations: READ,
245
+ },
246
+ async ({ id, page, limit }) => {
247
+ try {
248
+ const params = new URLSearchParams();
249
+ if (page) params.set("page", String(page));
250
+ if (limit) params.set("limit", String(limit));
251
+ const qs = params.toString();
252
+ return jsonResult(await api("GET", `/flows/${id}/runs${qs ? `?${qs}` : ""}`));
253
+ } catch (error) {
254
+ return errorResult(error);
255
+ }
256
+ },
257
+ );
258
+
259
+ server.registerTool(
260
+ "neuron_get_flow_run",
261
+ {
262
+ title: "Get Flow Run",
263
+ description:
264
+ "Get a single flow run with its full step-by-step execution trace. Each step shows the node type, input/output, status (ok/error/skipped/wait), duration, and which handle fired. " +
265
+ "Use this to debug flows — see exactly what happened at each step.",
266
+ inputSchema: z.object({
267
+ runId: z.string().describe("UUID of the run."),
268
+ }),
269
+ annotations: READ,
270
+ },
271
+ async ({ runId }) => {
272
+ try {
273
+ return jsonResult(await api("GET", `/flows/runs/${runId}`));
274
+ } catch (error) {
275
+ return errorResult(error);
276
+ }
277
+ },
278
+ );
279
+
280
+ server.registerTool(
281
+ "neuron_cancel_flow_run",
282
+ {
283
+ title: "Cancel Flow Run",
284
+ description: "Cancel a running or waiting flow execution. Steps already completed are not rolled back.",
285
+ inputSchema: z.object({
286
+ runId: z.string().describe("UUID of the run to cancel."),
287
+ }),
288
+ annotations: WRITE,
289
+ },
290
+ async ({ runId }) => {
291
+ try {
292
+ return jsonResult(await api("POST", `/flows/runs/${runId}/cancel`));
293
+ } catch (error) {
294
+ return errorResult(error);
295
+ }
296
+ },
297
+ );
298
+ }