@ascendkit/cli 0.2.0 → 0.3.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.
@@ -1,7 +1,7 @@
1
1
  import { hostname, platform as osPlatform, release } from "node:os";
2
2
  import { readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { loadAuth, saveAuth, deleteAuth, saveEnvContext, ensureGitignore } from "../utils/credentials.js";
4
+ import { loadAuth, loadEnvContext, saveAuth, deleteAuth, saveEnvContext, ensureGitignore, } from "../utils/credentials.js";
5
5
  import { DEFAULT_API_URL, DEFAULT_PORTAL_URL } from "../constants.js";
6
6
  const POLL_INTERVAL_MS = 2000;
7
7
  const DEVICE_CODE_EXPIRY_MS = 300_000; // 5 minutes
@@ -16,6 +16,14 @@ const ASCENDKIT_ENV_KEYS = [
16
16
  ];
17
17
  const ASCENDKIT_BLOCK_START = "# --- AscendKit Managed Environment Variables ---";
18
18
  const ASCENDKIT_BLOCK_END = "# --- End AscendKit Managed Environment Variables ---";
19
+ function normalizePromotionTier(targetTier) {
20
+ const normalized = targetTier.trim().toLowerCase();
21
+ if (normalized === "production")
22
+ return "prod";
23
+ if (normalized === "staging")
24
+ return "beta";
25
+ return normalized;
26
+ }
19
27
  async function promptYesNo(question, defaultYes) {
20
28
  const suffix = defaultYes ? "[Y/n]" : "[y/N]";
21
29
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -184,13 +192,14 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
184
192
  const existingPublicKey = readEnvValue(original, "ASCENDKIT_ENV_KEY");
185
193
  const existingSecretKey = readEnvValue(original, "ASCENDKIT_SECRET_KEY");
186
194
  const existingWebhookSecret = readEnvValue(original, "ASCENDKIT_WEBHOOK_SECRET") ?? "";
187
- const resolvedApiUrl = existingApiUrl ?? apiUrl;
195
+ const incomingSecretKey = (secretKey ?? "").trim();
196
+ const resolvedApiUrl = apiUrl;
188
197
  const resolvedPublicKey = preserveExistingKeys
189
198
  ? (existingPublicKey && existingPublicKey.trim() ? existingPublicKey : publicKey)
190
199
  : publicKey;
191
200
  const resolvedSecretKey = preserveExistingKeys
192
- ? (existingSecretKey && existingSecretKey.trim() ? existingSecretKey : secretKey)
193
- : secretKey;
201
+ ? (existingSecretKey && existingSecretKey.trim() ? existingSecretKey : incomingSecretKey)
202
+ : (incomingSecretKey || existingSecretKey || "");
194
203
  const resolvedWebhookSecret = options?.webhookSecret ?? existingWebhookSecret;
195
204
  return rewriteAscendKitEnvBlock(filePath, {
196
205
  NEXT_PUBLIC_ASCENDKIT_API_URL: resolvedApiUrl,
@@ -201,6 +210,124 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
201
210
  ASCENDKIT_WEBHOOK_SECRET: resolvedWebhookSecret,
202
211
  });
203
212
  }
213
+ async function syncLocalEnvFiles(apiUrl, publicKey, secretKey) {
214
+ const cwd = process.cwd();
215
+ const folders = await findEnvFolders(cwd);
216
+ const updatedFiles = [];
217
+ for (const folder of folders) {
218
+ for (const filePath of folder.examples) {
219
+ const changed = await updateEnvExampleFile(filePath);
220
+ if (changed)
221
+ updatedFiles.push(path.relative(cwd, filePath));
222
+ }
223
+ for (const filePath of folder.runtime) {
224
+ const changed = await updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey);
225
+ if (changed)
226
+ updatedFiles.push(path.relative(cwd, filePath));
227
+ }
228
+ }
229
+ return updatedFiles;
230
+ }
231
+ function saveActiveEnvironmentContext(result) {
232
+ saveEnvContext({
233
+ publicKey: result.environment.publicKey,
234
+ projectId: result.project.id,
235
+ projectName: result.project.name,
236
+ environmentId: result.environment.id,
237
+ environmentName: result.environment.name,
238
+ tier: result.environment.tier,
239
+ });
240
+ }
241
+ async function activateEnvironment(auth, result, options) {
242
+ const promptForFileUpdates = options?.promptForFileUpdates ?? true;
243
+ const shouldSetupWebhook = options?.setupWebhook ?? true;
244
+ const logOutput = options?.logOutput ?? true;
245
+ saveActiveEnvironmentContext(result);
246
+ if (logOutput) {
247
+ console.log(` → Project: ${result.project.name}`);
248
+ console.log(` → Environment: ${result.environment.name}`);
249
+ console.log(` → Role: ${result.role}`);
250
+ }
251
+ const cwd = process.cwd();
252
+ const folders = await findEnvFolders(cwd);
253
+ const updatedFiles = [];
254
+ const consentedRuntimeFolders = [];
255
+ if (folders.length > 0) {
256
+ if (logOutput) {
257
+ console.log(`\nFound env files in ${folders.length} folder(s).`);
258
+ }
259
+ for (const folder of folders) {
260
+ let update = true;
261
+ if (promptForFileUpdates) {
262
+ const fileNames = [...folder.examples, ...folder.runtime]
263
+ .map(f => path.basename(f)).join(", ");
264
+ update = await promptYesNo(` ${folder.label} (${fileNames}) — update?`, true);
265
+ }
266
+ if (!update)
267
+ continue;
268
+ for (const filePath of folder.examples) {
269
+ const changed = await updateEnvExampleFile(filePath);
270
+ if (changed)
271
+ updatedFiles.push(path.relative(cwd, filePath));
272
+ }
273
+ if (folder.runtime.length > 0) {
274
+ consentedRuntimeFolders.push(folder);
275
+ for (const filePath of folder.runtime) {
276
+ const changed = await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey);
277
+ if (changed)
278
+ updatedFiles.push(path.relative(cwd, filePath));
279
+ }
280
+ }
281
+ }
282
+ }
283
+ if (logOutput) {
284
+ console.log("\nUpdated files:");
285
+ if (updatedFiles.length === 0) {
286
+ console.log(" (none)");
287
+ }
288
+ else {
289
+ for (const filePath of updatedFiles)
290
+ console.log(` - ${filePath}`);
291
+ }
292
+ if (!result.environment.secretKey) {
293
+ console.log("\nNote: your role does not have access to the environment secret key.");
294
+ }
295
+ }
296
+ if (!shouldSetupWebhook) {
297
+ return { updatedFiles, webhookSecret: null };
298
+ }
299
+ const webhookSecret = await setupWebhook(auth, result.environment.publicKey);
300
+ if (webhookSecret) {
301
+ if (consentedRuntimeFolders.length > 0) {
302
+ for (const folder of consentedRuntimeFolders) {
303
+ for (const filePath of folder.runtime) {
304
+ await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey, { preserveExistingKeys: true, webhookSecret });
305
+ }
306
+ }
307
+ if (logOutput) {
308
+ console.log("\nWebhook secret written to .env file(s).");
309
+ }
310
+ }
311
+ else if (logOutput) {
312
+ console.log(`\nAdd this to your environment:`);
313
+ console.log(` ASCENDKIT_WEBHOOK_SECRET=${webhookSecret}`);
314
+ }
315
+ }
316
+ if (logOutput) {
317
+ const remainingSteps = [];
318
+ if (!webhookSecret) {
319
+ remainingSteps.push("Set up a webhook endpoint (run ascendkit set-env again, or use the portal).");
320
+ }
321
+ remainingSteps.push("Ensure frontend and backend are using keys from the same AscendKit environment.");
322
+ if (remainingSteps.length > 0) {
323
+ console.log("\nWhat you still need to do:");
324
+ for (let i = 0; i < remainingSteps.length; i++) {
325
+ console.log(` ${i + 1}. ${remainingSteps[i]}`);
326
+ }
327
+ }
328
+ }
329
+ return { updatedFiles, webhookSecret };
330
+ }
204
331
  function generateDeviceCode() {
205
332
  const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no I, O, 0, 1 for readability
206
333
  let code = "";
@@ -223,6 +350,28 @@ async function apiRequest(apiUrl, method, path, body, token, publicKey) {
223
350
  const response = await fetch(`${apiUrl}${path}`, init);
224
351
  if (!response.ok) {
225
352
  const text = await response.text();
353
+ if (!text) {
354
+ throw new Error(`API error ${response.status}: ${response.statusText}`);
355
+ }
356
+ try {
357
+ const parsed = JSON.parse(text);
358
+ if (typeof parsed.error === "string") {
359
+ throw new Error(parsed.error);
360
+ }
361
+ if (typeof parsed.detail === "string") {
362
+ throw new Error(parsed.detail);
363
+ }
364
+ if (parsed.detail
365
+ && typeof parsed.detail === "object"
366
+ && typeof parsed.detail.message === "string") {
367
+ throw new Error(parsed.detail.message);
368
+ }
369
+ }
370
+ catch (error) {
371
+ if (error instanceof Error && error.message !== text) {
372
+ throw error;
373
+ }
374
+ }
226
375
  throw new Error(`API error ${response.status}: ${text}`);
227
376
  }
228
377
  const json = await response.json();
@@ -330,6 +479,14 @@ export async function listProjects() {
330
479
  const auth = requireAuth();
331
480
  return apiRequest(auth.apiUrl, "GET", "/api/platform/projects", undefined, auth.token);
332
481
  }
482
+ export async function showProject(projectId) {
483
+ const projects = await listProjects();
484
+ const project = projects.find((item) => item.id === projectId);
485
+ if (!project) {
486
+ throw new Error(`Project ${projectId} not found.`);
487
+ }
488
+ return project;
489
+ }
333
490
  export async function createProject(name, description, enabledServices) {
334
491
  const auth = requireAuth();
335
492
  return apiRequest(auth.apiUrl, "POST", "/api/platform/projects", {
@@ -349,26 +506,7 @@ export async function useEnvironment(tier, projectId) {
349
506
  if (!env) {
350
507
  throw new Error(`No ${tier} environment found for project ${projectId}. Available: ${envs.map(e => e.tier).join(", ")}`);
351
508
  }
352
- // Fetch project name for echo-back
353
- let projectName = projectId;
354
- try {
355
- const projects = await apiRequest(auth.apiUrl, "GET", "/api/platform/projects", undefined, auth.token);
356
- const project = projects.find(p => p.id === projectId);
357
- if (project)
358
- projectName = project.name;
359
- }
360
- catch { /* non-critical */ }
361
- saveEnvContext({
362
- publicKey: env.publicKey,
363
- projectId,
364
- projectName,
365
- environmentId: env.id,
366
- environmentName: env.name,
367
- tier: env.tier,
368
- });
369
- console.log(`Active environment: ${env.name} (${env.tier})`);
370
- console.log(`Project: ${projectName}`);
371
- console.log(`Public key: ${env.publicKey}`);
509
+ await setEnv(env.publicKey);
372
510
  }
373
511
  async function setupWebhook(auth, publicKey) {
374
512
  // Never run webhook setup in non-interactive mode — selecting a webhook
@@ -434,82 +572,11 @@ async function setupWebhook(auth, publicKey) {
434
572
  export async function setEnv(publicKey) {
435
573
  const auth = requireAuth();
436
574
  const result = await apiRequest(auth.apiUrl, "GET", `/api/platform/resolve-key?pk=${encodeURIComponent(publicKey)}`, undefined, auth.token);
437
- saveEnvContext({
438
- publicKey: result.environment.publicKey,
439
- projectId: result.project.id,
440
- projectName: result.project.name,
441
- environmentId: result.environment.id,
442
- environmentName: result.environment.name,
443
- tier: result.environment.tier,
575
+ await activateEnvironment(auth, result, {
576
+ promptForFileUpdates: true,
577
+ setupWebhook: true,
578
+ logOutput: true,
444
579
  });
445
- console.log(` → Project: ${result.project.name}`);
446
- console.log(` → Environment: ${result.environment.name}`);
447
- console.log(` → Role: ${result.role}`);
448
- const cwd = process.cwd();
449
- const folders = await findEnvFolders(cwd);
450
- const updatedFiles = [];
451
- const consentedRuntimeFolders = [];
452
- if (folders.length > 0) {
453
- console.log(`\nFound env files in ${folders.length} folder(s).`);
454
- for (const folder of folders) {
455
- const fileNames = [...folder.examples, ...folder.runtime]
456
- .map(f => path.basename(f)).join(", ");
457
- const update = await promptYesNo(` ${folder.label} (${fileNames}) — update?`, true);
458
- if (!update)
459
- continue;
460
- for (const filePath of folder.examples) {
461
- const changed = await updateEnvExampleFile(filePath);
462
- if (changed)
463
- updatedFiles.push(path.relative(cwd, filePath));
464
- }
465
- if (folder.runtime.length > 0) {
466
- consentedRuntimeFolders.push(folder);
467
- for (const filePath of folder.runtime) {
468
- const changed = await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "");
469
- if (changed)
470
- updatedFiles.push(path.relative(cwd, filePath));
471
- }
472
- }
473
- }
474
- }
475
- console.log("\nUpdated files:");
476
- if (updatedFiles.length === 0) {
477
- console.log(" (none)");
478
- }
479
- else {
480
- for (const filePath of updatedFiles)
481
- console.log(` - ${filePath}`);
482
- }
483
- if (!result.environment.secretKey) {
484
- console.log("\nNote: your role does not have access to the environment secret key.");
485
- }
486
- // --- Webhook setup ---
487
- const webhookSecret = await setupWebhook(auth, result.environment.publicKey);
488
- if (webhookSecret) {
489
- if (consentedRuntimeFolders.length > 0) {
490
- for (const folder of consentedRuntimeFolders) {
491
- for (const filePath of folder.runtime) {
492
- await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "", { preserveExistingKeys: true, webhookSecret });
493
- }
494
- }
495
- console.log("\nWebhook secret written to .env file(s).");
496
- }
497
- else {
498
- console.log(`\nAdd this to your environment:`);
499
- console.log(` ASCENDKIT_WEBHOOK_SECRET=${webhookSecret}`);
500
- }
501
- }
502
- const remainingSteps = [];
503
- if (!webhookSecret) {
504
- remainingSteps.push("Set up a webhook endpoint (run ascendkit set-env again, or use the portal).");
505
- }
506
- remainingSteps.push("Ensure frontend and backend are using keys from the same AscendKit environment.");
507
- if (remainingSteps.length > 0) {
508
- console.log("\nWhat you still need to do:");
509
- for (let i = 0; i < remainingSteps.length; i++) {
510
- console.log(` ${i + 1}. ${remainingSteps[i]}`);
511
- }
512
- }
513
580
  }
514
581
  export async function mcpLogin(client, params) {
515
582
  const data = await client.publicRequest("POST", "/api/platform/auth/token", {
@@ -517,8 +584,20 @@ export async function mcpLogin(client, params) {
517
584
  password: params.password,
518
585
  });
519
586
  client.configurePlatform(data.token);
587
+ saveAuth({ token: data.token, apiUrl: client.currentApiUrl });
588
+ ensureGitignore();
520
589
  return data;
521
590
  }
591
+ export async function mcpSetEnvironmentByPublicKey(client, publicKey) {
592
+ const result = await client.platformRequest("GET", `/api/platform/resolve-key?pk=${encodeURIComponent(publicKey)}`);
593
+ client.configure(result.environment.publicKey);
594
+ const { updatedFiles } = await activateEnvironment({ token: "__mcp__", apiUrl: client.currentApiUrl }, result, {
595
+ promptForFileUpdates: false,
596
+ setupWebhook: false,
597
+ logOutput: false,
598
+ });
599
+ return { ...result, updatedFiles };
600
+ }
522
601
  export async function mcpListProjects(client) {
523
602
  return client.platformRequest("GET", "/api/platform/projects");
524
603
  }
@@ -546,7 +625,7 @@ export async function updateEnvironment(projectId, envId, name, description) {
546
625
  }
547
626
  export async function promoteEnvironment(environmentId, targetTier) {
548
627
  const auth = requireAuth();
549
- return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier, dryRun: false }, auth.token);
628
+ return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier: normalizePromotionTier(targetTier), dryRun: false }, auth.token);
550
629
  }
551
630
  export async function mcpUpdateEnvironment(client, params) {
552
631
  const body = {};
@@ -557,5 +636,44 @@ export async function mcpUpdateEnvironment(client, params) {
557
636
  return client.platformRequest("PATCH", `/api/platform/projects/${params.projectId}/environments/${params.environmentId}`, body);
558
637
  }
559
638
  export async function mcpPromoteEnvironment(client, params) {
560
- return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: params.targetTier, dryRun: false });
639
+ return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: normalizePromotionTier(params.targetTier), dryRun: false });
640
+ }
641
+ function resolveActiveEnvironmentIds(params) {
642
+ if (params?.projectId && params?.envId) {
643
+ return { projectId: params.projectId, envId: params.envId };
644
+ }
645
+ const ctx = loadEnvContext();
646
+ if (!ctx?.projectId || !ctx.environmentId) {
647
+ throw new Error("No active environment found. Use ascendkit_set_env or ascendkit set-env first.");
648
+ }
649
+ return {
650
+ projectId: params?.projectId ?? ctx.projectId,
651
+ envId: params?.envId ?? ctx.environmentId,
652
+ };
653
+ }
654
+ export async function mcpGetEnvironment(client, params) {
655
+ const target = resolveActiveEnvironmentIds(params);
656
+ const envs = await client.platformRequest("GET", `/api/platform/projects/${target.projectId}/environments`);
657
+ const env = envs.find((item) => item.id === target.envId);
658
+ if (!env) {
659
+ throw new Error(`Environment ${target.envId} not found in project ${target.projectId}.`);
660
+ }
661
+ return env;
662
+ }
663
+ export async function mcpUpdateEnvironmentVariables(client, params) {
664
+ const target = resolveActiveEnvironmentIds(params);
665
+ return client.platformRequest("PATCH", `/api/platform/projects/${target.projectId}/environments/${target.envId}`, { variables: params.variables });
666
+ }
667
+ export async function getEnvironment(projectId, envId) {
668
+ const auth = requireAuth();
669
+ const envs = await apiRequest(auth.apiUrl, "GET", `/api/platform/projects/${projectId}/environments`, undefined, auth.token);
670
+ const env = envs.find(e => e.id === envId);
671
+ if (!env) {
672
+ throw new Error(`Environment ${envId} not found in project ${projectId}.`);
673
+ }
674
+ return env;
675
+ }
676
+ export async function updateEnvironmentVariables(projectId, envId, variables) {
677
+ const auth = requireAuth();
678
+ return apiRequest(auth.apiUrl, "PATCH", `/api/platform/projects/${projectId}/environments/${envId}`, { variables }, auth.token);
561
679
  }
@@ -2,10 +2,10 @@ export async function createSurvey(client, params) {
2
2
  return client.managedPost("/api/surveys", params);
3
3
  }
4
4
  export async function listSurveys(client) {
5
- return client.get("/api/surveys");
5
+ return client.managedGet("/api/surveys");
6
6
  }
7
7
  export async function getSurvey(client, surveyId) {
8
- return client.get(`/api/surveys/${surveyId}`);
8
+ return client.managedGet(`/api/surveys/${surveyId}`);
9
9
  }
10
10
  export async function updateSurvey(client, surveyId, params) {
11
11
  return client.managedPut(`/api/surveys/${surveyId}`, params);
@@ -17,13 +17,13 @@ export async function distributeSurvey(client, surveyId, userIds) {
17
17
  return client.managedPost(`/api/surveys/${surveyId}/distribute`, { userIds });
18
18
  }
19
19
  export async function listInvitations(client, surveyId) {
20
- return client.get(`/api/surveys/${surveyId}/invitations`);
20
+ return client.managedGet(`/api/surveys/${surveyId}/invitations`);
21
21
  }
22
22
  export async function getAnalytics(client, surveyId) {
23
- return client.get(`/api/surveys/${surveyId}/analytics`);
23
+ return client.managedGet(`/api/surveys/${surveyId}/analytics`);
24
24
  }
25
25
  export async function listQuestions(client, surveyId) {
26
- return client.get(`/api/surveys/${surveyId}/questions`);
26
+ return client.managedGet(`/api/surveys/${surveyId}/questions`);
27
27
  }
28
28
  export async function addQuestion(client, surveyId, params) {
29
29
  return client.managedPost(`/api/surveys/${surveyId}/questions`, params);
package/dist/mcp.js CHANGED
@@ -10,21 +10,51 @@ import { registerPlatformTools } from "./tools/platform.js";
10
10
  import { registerEmailTools } from "./tools/email.js";
11
11
  import { registerJourneyTools } from "./tools/journeys.js";
12
12
  import { registerWebhookTools } from "./tools/webhooks.js";
13
+ import { registerCampaignTools } from "./tools/campaigns.js";
14
+ import { registerImportTools } from "./tools/import.js";
13
15
  import { DEFAULT_API_URL } from "./constants.js";
16
+ import { loadAuth, loadEnvContext } from "./utils/credentials.js";
17
+ import * as platformCommands from "./commands/platform.js";
18
+ import { CLI_VERSION } from "./api/client.js";
14
19
  const client = new AscendKitClient({
15
20
  apiUrl: process.env.ASCENDKIT_API_URL ?? DEFAULT_API_URL,
16
21
  });
22
+ const persistedAuth = loadAuth();
23
+ if (persistedAuth?.token) {
24
+ client.configurePlatform(persistedAuth.token, persistedAuth.apiUrl);
25
+ }
26
+ const persistedEnv = loadEnvContext();
27
+ if (persistedEnv?.publicKey) {
28
+ client.configure(persistedEnv.publicKey, persistedAuth?.apiUrl ?? process.env.ASCENDKIT_API_URL ?? DEFAULT_API_URL);
29
+ }
17
30
  const server = new McpServer({
18
31
  name: "ascendkit",
19
- version: "0.1.0",
32
+ version: CLI_VERSION,
20
33
  });
21
- server.tool("ascendkit_configure", "Configure AscendKit with your project's public key. Must be called before using any other AscendKit tools.", {
34
+ server.tool("ascendkit_configure", "Configure AscendKit with your project's public key. If platform auth is available, this also persists the shared .ascendkit environment context used by both MCP and CLI.", {
22
35
  publicKey: z.string().describe("Your project's public key (pk_dev_..., pk_beta_..., or pk_prod_...)"),
23
36
  apiUrl: z.string().optional().describe("AscendKit API URL (default: https://api.ascendkit.dev)"),
24
37
  }, async (params) => {
25
38
  client.configure(params.publicKey, params.apiUrl);
39
+ if (client.platformConfigured) {
40
+ try {
41
+ const data = await platformCommands.mcpSetEnvironmentByPublicKey(client, params.publicKey);
42
+ return {
43
+ content: [{
44
+ type: "text",
45
+ text: `Active environment: ${data.environment.name} (${data.environment.tier})\n` +
46
+ `Project: ${data.project.name}\n` +
47
+ `Public key: ${data.environment.publicKey}\n` +
48
+ `Updated env files: ${Array.isArray(data.updatedFiles) && data.updatedFiles.length > 0 ? data.updatedFiles.join(", ") : "(none)"}`,
49
+ }],
50
+ };
51
+ }
52
+ catch {
53
+ // Fall back to in-memory configure if resolve/persist fails.
54
+ }
55
+ }
26
56
  return {
27
- content: [{ type: "text", text: `Configured for project ${params.publicKey}` }],
57
+ content: [{ type: "text", text: `Configured for environment ${params.publicKey}` }],
28
58
  };
29
59
  });
30
60
  registerPlatformTools(server, client);
@@ -34,6 +64,8 @@ registerSurveyTools(server, client);
34
64
  registerEmailTools(server, client);
35
65
  registerJourneyTools(server, client);
36
66
  registerWebhookTools(server, client);
67
+ registerCampaignTools(server, client);
68
+ registerImportTools(server, client);
37
69
  async function main() {
38
70
  const transport = new StdioServerTransport();
39
71
  await server.connect(transport);
@@ -1,11 +1,25 @@
1
1
  import { z } from "zod";
2
2
  import * as auth from "../commands/auth.js";
3
+ function formatAuthSettings(data) {
4
+ const providers = Array.isArray(data.providers) ? data.providers : [];
5
+ const features = data.features ?? {};
6
+ const lines = [
7
+ `Providers: ${providers.length > 0 ? providers.join(", ") : "none"}`,
8
+ `Email verification: ${features.emailVerification ? "on" : "off"}`,
9
+ `Waitlist: ${features.waitlist ? "on" : "off"}`,
10
+ `Password reset: ${features.passwordReset ? "on" : "off"}`,
11
+ `Require username: ${features.requireUsername ? "on" : "off"}`,
12
+ ];
13
+ if (data.sessionDuration)
14
+ lines.push(`Session: ${data.sessionDuration}`);
15
+ return lines.join("\n");
16
+ }
3
17
  export function registerAuthTools(server, client) {
4
- server.tool("auth_get_settings", "Get authentication settings for the current project (enabled providers, features, OAuth configs)", {}, async () => {
18
+ server.tool("auth_show", "Get authentication settings for the current project (enabled providers, features, OAuth configs)", {}, async () => {
5
19
  const data = await auth.getSettings(client);
6
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
20
+ return { content: [{ type: "text", text: formatAuthSettings(data) }] };
7
21
  });
8
- server.tool("auth_update_settings", "Update authentication settings: providers, features, session duration. Credentials and magic-link are mutually exclusive. Social-only login is valid. emailVerification, passwordReset, requireUsername only apply when credentials is active; waitlist applies to all modes.", {
22
+ server.tool("auth_update", "Update authentication settings: providers, features, session duration. Credentials and magic-link are mutually exclusive. Social-only login is valid. emailVerification, passwordReset, requireUsername only apply when credentials is active; waitlist applies to all modes.", {
9
23
  providers: z
10
24
  .array(z.string())
11
25
  .optional()
@@ -25,22 +39,48 @@ export function registerAuthTools(server, client) {
25
39
  .describe('Session duration, e.g. "7d", "24h"'),
26
40
  }, async (params) => {
27
41
  const data = await auth.updateSettings(client, params);
28
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
42
+ return { content: [{ type: "text", text: formatAuthSettings(data) }] };
29
43
  });
30
- server.tool("auth_update_providers", "Set which auth providers are enabled for the project. Credentials and magic-link are mutually exclusive. Social-only login (e.g. [\"google\"]) is valid. At least one provider required.", {
44
+ server.tool("auth_provider_set", "Set which auth providers are enabled for the project. Credentials and magic-link are mutually exclusive. Social-only login (e.g. [\"google\"]) is valid. At least one provider required.", {
31
45
  providers: z
32
46
  .array(z.string())
33
47
  .describe('List of provider names, e.g. ["credentials", "google", "github"]'),
34
48
  }, async (params) => {
35
49
  const data = await auth.updateProviders(client, params.providers);
36
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
50
+ return { content: [{ type: "text", text: formatAuthSettings(data) }] };
51
+ });
52
+ server.tool("auth_provider_list", "List enabled auth providers for the current project.", {}, async () => {
53
+ const data = await auth.getSettings(client);
54
+ const providers = Array.isArray(data.providers) ? data.providers : [];
55
+ return {
56
+ content: [{ type: "text", text: providers.length > 0 ? providers.join("\n") : "No providers configured." }],
57
+ };
37
58
  });
38
- server.tool("auth_setup_oauth", "Configure OAuth credentials for a provider. Pass clientId and clientSecret to set credentials directly, or omit them to get a portal URL for browser-based entry. Note: credentials in tool args may appear in logs — use the portal URL for sensitive environments.", {
59
+ server.tool("auth_oauth", "Configure OAuth credentials for a provider. Pass clientId and clientSecret for custom credentials, or useProxy to revert to AscendKit managed credentials. Omit all to get a portal URL for browser-based entry.", {
39
60
  provider: z.string().describe('OAuth provider name, e.g. "google", "github"'),
40
61
  clientId: z.string().optional().describe("OAuth client ID (to set credentials directly)"),
41
62
  clientSecret: z.string().optional().describe("OAuth client secret (to set credentials directly)"),
42
63
  callbackUrl: z.string().optional().describe("Auth callback URL for this provider"),
64
+ useProxy: z.boolean().optional().describe("Clear custom credentials and use AscendKit managed proxy credentials"),
43
65
  }, async (params) => {
66
+ if (params.useProxy && (params.clientId || params.clientSecret)) {
67
+ return {
68
+ content: [{
69
+ type: "text",
70
+ text: "Cannot use useProxy with custom credentials. Either set useProxy to clear credentials, or provide clientId and clientSecret.",
71
+ }],
72
+ isError: true,
73
+ };
74
+ }
75
+ if (params.useProxy) {
76
+ const data = await auth.deleteOAuthCredentials(client, params.provider);
77
+ return {
78
+ content: [{
79
+ type: "text",
80
+ text: `Cleared custom OAuth credentials for ${params.provider}. Now using AscendKit managed proxy credentials.\n\n${JSON.stringify(data, null, 2)}`,
81
+ }],
82
+ };
83
+ }
44
84
  if ((params.clientId && !params.clientSecret) || (!params.clientId && params.clientSecret)) {
45
85
  return {
46
86
  content: [{
@@ -68,8 +108,26 @@ export function registerAuthTools(server, client) {
68
108
  }],
69
109
  };
70
110
  });
71
- server.tool("auth_list_users", "List all project users (end-users who signed up via the SDK)", {}, async () => {
111
+ server.tool("auth_user_list", "List all project users (end-users who signed up via the SDK)", {}, async () => {
72
112
  const data = await auth.listUsers(client);
73
113
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
74
114
  });
115
+ server.tool("auth_user_remove", "Deactivate a user (soft delete). The user record is preserved but marked inactive.", {
116
+ userId: z.string().describe("User ID (usr_ prefixed)"),
117
+ }, async (params) => {
118
+ const data = await auth.deleteUser(client, params.userId);
119
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
120
+ });
121
+ server.tool("auth_user_bulk_remove", "Bulk deactivate users (soft delete). All specified users are marked inactive.", {
122
+ userIds: z.array(z.string()).min(1).max(100).describe("Array of user IDs (usr_ prefixed)"),
123
+ }, async (params) => {
124
+ const data = await auth.bulkDeleteUsers(client, params.userIds);
125
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
126
+ });
127
+ server.tool("auth_user_reactivate", "Reactivate a previously deactivated user.", {
128
+ userId: z.string().describe("User ID (usr_ prefixed)"),
129
+ }, async (params) => {
130
+ const data = await auth.reactivateUser(client, params.userId);
131
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
132
+ });
75
133
  }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AscendKitClient } from "../api/client.js";
3
+ export declare function registerCampaignTools(server: McpServer, client: AscendKitClient): void;