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.
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 +4 -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,536 @@
1
+ import { z } from "zod";
2
+ import http from "node:http";
3
+ import net from "node:net";
4
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { api, jsonResult, textResult, errorResult, setAuthToken, getAuthToken, getBaseUrl, setBaseUrl, listInstances } from "../client.js";
6
+
7
+ /** Find an available port by binding to port 0. */
8
+ function findAvailablePort(): Promise<number> {
9
+ return new Promise((resolve, reject) => {
10
+ const srv = net.createServer();
11
+ srv.listen(0, "127.0.0.1", () => {
12
+ const addr = srv.address();
13
+ if (addr && typeof addr === "object") {
14
+ const port = addr.port;
15
+ srv.close(() => resolve(port));
16
+ } else {
17
+ srv.close(() => reject(new Error("Could not determine port")));
18
+ }
19
+ });
20
+ srv.on("error", reject);
21
+ });
22
+ }
23
+
24
+ /** Start a temporary HTTP server that waits for a callback with a token. */
25
+ function startCallbackServer(port: number): {
26
+ server: http.Server;
27
+ tokenPromise: Promise<string>;
28
+ shutdown: () => void;
29
+ } {
30
+ let resolveToken: (token: string) => void;
31
+ let rejectToken: (err: Error) => void;
32
+ const tokenPromise = new Promise<string>((resolve, reject) => {
33
+ resolveToken = resolve;
34
+ rejectToken = reject;
35
+ });
36
+
37
+ const server = http.createServer((req, res) => {
38
+ const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
39
+
40
+ if (url.pathname === "/callback") {
41
+ const token = url.searchParams.get("token");
42
+ if (token) {
43
+ res.writeHead(200, { "Content-Type": "text/html" });
44
+ res.end("<html><body><h2>Authorization successful!</h2><p>You can close this tab and return to your terminal.</p></body></html>");
45
+ resolveToken(token);
46
+ } else {
47
+ res.writeHead(400, { "Content-Type": "text/plain" });
48
+ res.end("Missing token parameter");
49
+ }
50
+ } else {
51
+ res.writeHead(404, { "Content-Type": "text/plain" });
52
+ res.end("Not found");
53
+ }
54
+ });
55
+
56
+ server.listen(port, "127.0.0.1");
57
+
58
+ const shutdown = () => {
59
+ server.close();
60
+ rejectToken(new Error("Server shut down"));
61
+ };
62
+
63
+ return { server, tokenPromise, shutdown };
64
+ }
65
+
66
+ /** Poll the backend for session approval. */
67
+ async function pollForApproval(sessionId: string, timeoutMs: number, intervalMs: number): Promise<string | null> {
68
+ const deadline = Date.now() + timeoutMs;
69
+
70
+ while (Date.now() < deadline) {
71
+ await new Promise((r) => setTimeout(r, intervalMs));
72
+
73
+ try {
74
+ const result = await api<{ status: string; token?: string }>("GET", `/auth/mcp/sessions/${sessionId}`);
75
+ const data = result.data as { status: string; token?: string } | undefined;
76
+
77
+ if (data?.status === "approved" && data.token) {
78
+ return data.token;
79
+ }
80
+ if (data?.status === "consumed" || data?.status === "expired") {
81
+ return null;
82
+ }
83
+ } catch {
84
+ // Ignore transient errors, keep polling
85
+ }
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ // Track pending browser auth session for non-blocking flow
92
+ let pendingSessionId: string | null = null;
93
+
94
+ export function registerAuthTools(server: McpServer): void {
95
+ // Register
96
+ server.registerTool(
97
+ "neuron_register",
98
+ {
99
+ title: "Register New User",
100
+ description: "Register a new user account with name, email, password, and organization name. Creates both the user and an initial organization.",
101
+ inputSchema: z.object({
102
+ name: z.string().describe("Full name of the user"),
103
+ email: z.string().email().describe("Email address for the new account"),
104
+ password: z.string().min(8).describe("Password for the account (minimum 8 characters)"),
105
+ orgName: z.string().describe("Name of the organization to create for this user"),
106
+ }),
107
+ annotations: {
108
+ readOnlyHint: false,
109
+ destructiveHint: false,
110
+ idempotentHint: false,
111
+ openWorldHint: false,
112
+ },
113
+ },
114
+ async (params) => {
115
+ try {
116
+ const result = await api("POST", "/auth/register", {
117
+ name: params.name,
118
+ email: params.email,
119
+ password: params.password,
120
+ orgName: params.orgName,
121
+ });
122
+ return jsonResult(result);
123
+ } catch (error) {
124
+ return errorResult(error);
125
+ }
126
+ },
127
+ );
128
+
129
+ // Login (multi-mode)
130
+ server.registerTool(
131
+ "neuron_login",
132
+ {
133
+ title: "Login to Neuron",
134
+ description:
135
+ "Authenticate with Neuron. Three modes:\n" +
136
+ "1. No args: Opens a browser URL for secure authorization (recommended — no credentials shared with AI)\n" +
137
+ "2. token: Paste an MCP token from the Neuron dashboard\n" +
138
+ "3. email+password: Legacy login (credentials visible to AI)",
139
+ inputSchema: z.object({
140
+ token: z.string().optional().describe("MCP token from the Neuron dashboard (Settings > MCP Tokens)"),
141
+ email: z.string().email().optional().describe("Email address for legacy credential-based login"),
142
+ password: z.string().optional().describe("Password for legacy credential-based login"),
143
+ }).refine(
144
+ (data) => {
145
+ const hasToken = !!data.token;
146
+ const hasCredentials = !!data.email && !!data.password;
147
+ const hasPartialCredentials = !!data.email !== !!data.password;
148
+ if (hasPartialCredentials) return false;
149
+ if (hasToken && hasCredentials) return false;
150
+ return true;
151
+ },
152
+ { message: "Provide either token, email+password, or nothing for browser auth" },
153
+ ),
154
+ annotations: {
155
+ readOnlyHint: false,
156
+ destructiveHint: false,
157
+ idempotentHint: true,
158
+ openWorldHint: false,
159
+ },
160
+ },
161
+ async (params) => {
162
+ try {
163
+ // Mode 1: Direct token
164
+ if (params.token) {
165
+ setAuthToken(params.token);
166
+ const me = await api("GET", "/auth/me");
167
+ if (!(me as any).success) {
168
+ setAuthToken("");
169
+ return errorResult(new Error("Invalid token. Please check and try again."));
170
+ }
171
+ const user = (me as any).data;
172
+ return textResult(
173
+ `Authenticated successfully as ${user.name} (${user.email})` +
174
+ (user.organization ? ` — org: ${user.organization.name}` : ""),
175
+ );
176
+ }
177
+
178
+ // Mode 2: Legacy email/password
179
+ if (params.email && params.password) {
180
+ const result = await api("POST", "/auth/login", {
181
+ email: params.email,
182
+ password: params.password,
183
+ });
184
+ if ((result as any).data?.accessToken) {
185
+ setAuthToken((result as any).data.accessToken);
186
+ }
187
+ return jsonResult(result);
188
+ }
189
+
190
+ // Mode 3: Browser auth (default — no args)
191
+ const isRemote = process.env.MCP_TRANSPORT === "http";
192
+
193
+ // Check if background polling already set the token
194
+ if (getAuthToken()) {
195
+ try {
196
+ const me = await api("GET", "/auth/me");
197
+ const user = (me as any).data;
198
+ if (user && (me as any).success) {
199
+ pendingSessionId = null;
200
+ return textResult(
201
+ `Already authenticated as ${user.name} (${user.email})` +
202
+ (user.organization ? ` — org: ${user.organization.name}` : ""),
203
+ );
204
+ }
205
+ } catch {
206
+ // Token invalid, continue with auth flow
207
+ setAuthToken("");
208
+ }
209
+ }
210
+
211
+ // Check if we have a pending session that was already approved
212
+ if (pendingSessionId) {
213
+ const token = await pollForApproval(pendingSessionId, 5000, 1000);
214
+ if (token) {
215
+ pendingSessionId = null;
216
+ setAuthToken(token);
217
+ const me = await api("GET", "/auth/me");
218
+ const user = (me as any).data;
219
+ return textResult(
220
+ `Authenticated successfully as ${user?.name || "user"} (${user?.email || "unknown"})` +
221
+ (user?.organization ? ` — org: ${user.organization.name}` : ""),
222
+ );
223
+ }
224
+ // Session still pending or expired — create a new one
225
+ }
226
+
227
+ // For local/stdio mode, start a callback server. For remote/HTTP mode, skip it.
228
+ let callbackUrl: string | undefined;
229
+ let callbackPort: number | undefined;
230
+ if (!isRemote) {
231
+ try {
232
+ callbackPort = await findAvailablePort();
233
+ callbackUrl = `http://127.0.0.1:${callbackPort}/callback`;
234
+ } catch {
235
+ // Can't bind locally — skip callback server
236
+ }
237
+ }
238
+
239
+ // Create session on backend
240
+ const sessionResult = await api<{ sessionId: string; authUrl: string }>(
241
+ "POST",
242
+ "/auth/mcp/sessions",
243
+ { callbackUrl: callbackUrl || "http://127.0.0.1:0/callback" },
244
+ );
245
+
246
+ const sessionData = (sessionResult as any).data;
247
+ if (!sessionData?.sessionId || !sessionData?.authUrl) {
248
+ return errorResult(new Error("Failed to create auth session. Is the server running?"));
249
+ }
250
+
251
+ const { sessionId, authUrl } = sessionData;
252
+ pendingSessionId = sessionId;
253
+
254
+ // Start background polling (non-blocking)
255
+ const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
256
+ const POLL_INTERVAL_MS = 3000;
257
+
258
+ // Fire-and-forget: poll in background and auto-set token when approved
259
+ pollForApproval(sessionId, TIMEOUT_MS, POLL_INTERVAL_MS).then((token) => {
260
+ if (token) {
261
+ setAuthToken(token);
262
+ pendingSessionId = null;
263
+ console.error("✅ Neuron authorization approved. Token set automatically.");
264
+ }
265
+ }).catch(() => {});
266
+
267
+ // Also start local callback server if available (for local/stdio mode only)
268
+ if (callbackPort && !isRemote) {
269
+ try {
270
+ const { tokenPromise, shutdown } = startCallbackServer(callbackPort);
271
+ tokenPromise.then((token) => {
272
+ setAuthToken(token);
273
+ pendingSessionId = null;
274
+ console.error("✅ Neuron authorization approved via callback. Token set automatically.");
275
+ }).catch(() => {});
276
+ // Auto-shutdown after timeout
277
+ setTimeout(() => shutdown(), TIMEOUT_MS);
278
+ } catch {
279
+ // Callback server not critical
280
+ }
281
+ }
282
+
283
+ // Return immediately with the URL — don't block!
284
+ return textResult(
285
+ `🔗 Open this URL to authorize Neuron:\n\n ${authUrl}\n\n` +
286
+ `After approving in your browser, run neuron_login() again (no args) to confirm.\n\n` +
287
+ `Alternatively, generate a token at Settings > MCP Tokens in the Neuron dashboard and run:\n` +
288
+ `neuron_login({ token: "your-token-here" })`,
289
+ );
290
+ } catch (error) {
291
+ return errorResult(error);
292
+ }
293
+ },
294
+ );
295
+
296
+ // Refresh Token
297
+ server.registerTool(
298
+ "neuron_refresh_token",
299
+ {
300
+ title: "Refresh Access Token",
301
+ description: "Refresh the access token using a refresh token. Automatically sets the new access token for subsequent API calls.",
302
+ inputSchema: z.object({
303
+ refreshToken: z.string().describe("Refresh token obtained from the initial login response"),
304
+ }),
305
+ annotations: {
306
+ readOnlyHint: false,
307
+ destructiveHint: false,
308
+ idempotentHint: true,
309
+ openWorldHint: false,
310
+ },
311
+ },
312
+ async (params) => {
313
+ try {
314
+ const result = await api("POST", "/auth/refresh", {
315
+ refreshToken: params.refreshToken,
316
+ });
317
+ if ((result as any).data?.accessToken) {
318
+ setAuthToken((result as any).data.accessToken);
319
+ }
320
+ return jsonResult(result);
321
+ } catch (error) {
322
+ return errorResult(error);
323
+ }
324
+ },
325
+ );
326
+
327
+ // Switch Organization
328
+ server.registerTool(
329
+ "neuron_switch_org",
330
+ {
331
+ title: "Switch Organization",
332
+ description: "Switch to a different organization. Returns a new access token scoped to the target organization and automatically sets it.",
333
+ inputSchema: z.object({
334
+ orgId: z.string().describe("Unique identifier (UUID) of the organization to switch to"),
335
+ }),
336
+ annotations: {
337
+ readOnlyHint: false,
338
+ destructiveHint: false,
339
+ idempotentHint: true,
340
+ openWorldHint: false,
341
+ },
342
+ },
343
+ async (params) => {
344
+ try {
345
+ const result = await api("POST", "/auth/switch-org", { orgId: params.orgId });
346
+ if ((result as any).data?.accessToken) {
347
+ setAuthToken((result as any).data.accessToken);
348
+ }
349
+ return jsonResult(result);
350
+ } catch (error) {
351
+ return errorResult(error);
352
+ }
353
+ },
354
+ );
355
+
356
+ // List Organizations
357
+ server.registerTool(
358
+ "neuron_list_organizations",
359
+ {
360
+ title: "List Organizations",
361
+ description: "Retrieve all organizations the authenticated user belongs to, including their roles in each.",
362
+ inputSchema: z.object({}),
363
+ annotations: {
364
+ readOnlyHint: true,
365
+ destructiveHint: false,
366
+ idempotentHint: true,
367
+ openWorldHint: false,
368
+ },
369
+ },
370
+ async () => {
371
+ try {
372
+ const result = await api("GET", "/auth/organizations");
373
+ return jsonResult(result);
374
+ } catch (error) {
375
+ return errorResult(error);
376
+ }
377
+ },
378
+ );
379
+
380
+ // Get Profile
381
+ server.registerTool(
382
+ "neuron_get_profile",
383
+ {
384
+ title: "Get User Profile",
385
+ description: "Retrieve the authenticated user's profile information including name, email, role, and current organization.",
386
+ inputSchema: z.object({}),
387
+ annotations: {
388
+ readOnlyHint: true,
389
+ destructiveHint: false,
390
+ idempotentHint: true,
391
+ openWorldHint: false,
392
+ },
393
+ },
394
+ async () => {
395
+ try {
396
+ const result = await api("GET", "/auth/me");
397
+ return jsonResult(result);
398
+ } catch (error) {
399
+ return errorResult(error);
400
+ }
401
+ },
402
+ );
403
+
404
+ // Who Am I
405
+ server.registerTool(
406
+ "neuron_whoami",
407
+ {
408
+ title: "Check Current Account",
409
+ description: "Check which Neuron account is currently logged in, including user name, email, role, and organization.",
410
+ inputSchema: z.object({}),
411
+ annotations: {
412
+ readOnlyHint: true,
413
+ destructiveHint: false,
414
+ idempotentHint: true,
415
+ openWorldHint: false,
416
+ },
417
+ },
418
+ async () => {
419
+ if (!getAuthToken()) {
420
+ return textResult("Not logged in. Use neuron_login() to authenticate.");
421
+ }
422
+ try {
423
+ const result = await api("GET", "/auth/me");
424
+ const user = (result as any).data;
425
+ if (!user || !(result as any).success) {
426
+ return textResult("Not logged in or token is invalid. Use neuron_login() to authenticate.");
427
+ }
428
+ const lines = [
429
+ `Logged in as: ${user.name} (${user.email})`,
430
+ `Role: ${user.role || "member"}`,
431
+ user.organization ? `Organization: ${user.organization.name}` : null,
432
+ ].filter(Boolean);
433
+ return textResult(lines.join("\n"));
434
+ } catch (error) {
435
+ return errorResult(error);
436
+ }
437
+ },
438
+ );
439
+
440
+ // Switch Instance
441
+ server.registerTool(
442
+ "neuron_switch_instance",
443
+ {
444
+ title: "Switch Neuron Instance",
445
+ description:
446
+ "Switch the MCP server to target a different Neuron backend instance. " +
447
+ "Auth tokens are preserved per instance — if you've previously logged in to an instance, " +
448
+ "switching back restores your session automatically.\n\n" +
449
+ "Call with no args to list known instances and see which is active.",
450
+ inputSchema: z.object({
451
+ url: z.string().optional().describe(
452
+ "Base API URL of the Neuron instance (e.g., 'https://api.client.com/api/v1'). " +
453
+ "Omit to list known instances.",
454
+ ),
455
+ }),
456
+ annotations: {
457
+ readOnlyHint: false,
458
+ destructiveHint: false,
459
+ idempotentHint: true,
460
+ openWorldHint: false,
461
+ },
462
+ },
463
+ async (params) => {
464
+ try {
465
+ // List mode: no URL provided
466
+ if (!params.url) {
467
+ const instances = listInstances();
468
+ const lines = instances.map((i) =>
469
+ `${i.active ? "→ " : " "}${i.url} ${i.authenticated ? "(authenticated)" : "(not authenticated)"}`,
470
+ );
471
+ return textResult(
472
+ `Known instances:\n${lines.join("\n")}\n\n` +
473
+ `Current: ${getBaseUrl()}\n` +
474
+ `Pass a url to switch: neuron_switch_instance({ url: "https://api.example.com/api/v1" })`,
475
+ );
476
+ }
477
+
478
+ // Normalize: strip trailing slashes
479
+ const url = params.url.replace(/\/+$/, "");
480
+
481
+ const previous = getBaseUrl();
482
+ if (url === previous) {
483
+ return textResult(`Already targeting ${url}.`);
484
+ }
485
+
486
+ setBaseUrl(url);
487
+
488
+ // Check if the restored token is still valid
489
+ const token = getAuthToken();
490
+ if (token) {
491
+ try {
492
+ const me = await api("GET", "/auth/me");
493
+ const user = (me as any).data;
494
+ if (user && (me as any).success) {
495
+ return textResult(
496
+ `Switched to ${url}\n` +
497
+ `Restored session: ${user.name} (${user.email})` +
498
+ (user.organization ? ` — org: ${user.organization.name}` : ""),
499
+ );
500
+ }
501
+ } catch {
502
+ // Token expired — clear it for this instance
503
+ setAuthToken("");
504
+ }
505
+ }
506
+
507
+ return textResult(
508
+ `Switched to ${url}\n` +
509
+ `Not authenticated on this instance. Use neuron_login() to authenticate.`,
510
+ );
511
+ } catch (error) {
512
+ return errorResult(error);
513
+ }
514
+ },
515
+ );
516
+
517
+ // Logout
518
+ server.registerTool(
519
+ "neuron_logout",
520
+ {
521
+ title: "Logout",
522
+ description: "Clear the current auth token locally. Does NOT revoke server-side MCP tokens — revoke from the Neuron dashboard (Settings > MCP Tokens) for full invalidation.",
523
+ inputSchema: z.object({}),
524
+ annotations: {
525
+ readOnlyHint: false,
526
+ destructiveHint: false,
527
+ idempotentHint: true,
528
+ openWorldHint: false,
529
+ },
530
+ },
531
+ async () => {
532
+ setAuthToken("");
533
+ return textResult("Logged out. Token cleared locally.\n\nTo revoke MCP tokens server-side, go to Settings > MCP Tokens in the Neuron dashboard.");
534
+ },
535
+ );
536
+ }
@@ -0,0 +1,79 @@
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 registerBillingTools(server: McpServer): void {
6
+ // Get billing info
7
+ server.registerTool(
8
+ "neuron_get_billing",
9
+ {
10
+ title: "Get Billing Info",
11
+ description: "Retrieve the current billing information, subscription plan, and usage details for the organization.",
12
+ inputSchema: z.object({}),
13
+ annotations: {
14
+ readOnlyHint: true,
15
+ destructiveHint: false,
16
+ idempotentHint: true,
17
+ openWorldHint: false,
18
+ },
19
+ },
20
+ async () => {
21
+ try {
22
+ const result = await api("GET", "/billing");
23
+ return jsonResult(result);
24
+ } catch (error) {
25
+ return errorResult(error);
26
+ }
27
+ }
28
+ );
29
+
30
+ // Checkout
31
+ server.registerTool(
32
+ "neuron_checkout",
33
+ {
34
+ title: "Checkout Subscription",
35
+ description: "Initiate a checkout session for a subscription plan upgrade. Returns a checkout URL or confirmation to complete the purchase.",
36
+ inputSchema: z.object({
37
+ plan: z.enum(["pro", "business"]).describe("Subscription plan to purchase: 'pro' or 'business'"),
38
+ }),
39
+ annotations: {
40
+ readOnlyHint: false,
41
+ destructiveHint: false,
42
+ idempotentHint: false,
43
+ openWorldHint: false,
44
+ },
45
+ },
46
+ async (params) => {
47
+ try {
48
+ const result = await api("POST", "/billing/checkout", { plan: params.plan });
49
+ return jsonResult(result);
50
+ } catch (error) {
51
+ return errorResult(error);
52
+ }
53
+ }
54
+ );
55
+
56
+ // Cancel subscription
57
+ server.registerTool(
58
+ "neuron_cancel_subscription",
59
+ {
60
+ title: "Cancel Subscription",
61
+ description: "Cancel the current active subscription. Access continues until the end of the current billing period.",
62
+ inputSchema: z.object({}),
63
+ annotations: {
64
+ readOnlyHint: false,
65
+ destructiveHint: true,
66
+ idempotentHint: true,
67
+ openWorldHint: false,
68
+ },
69
+ },
70
+ async () => {
71
+ try {
72
+ const result = await api("POST", "/billing/cancel");
73
+ return jsonResult(result);
74
+ } catch (error) {
75
+ return errorResult(error);
76
+ }
77
+ }
78
+ );
79
+ }