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,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
|
+
}
|