@ascendkit/cli 0.1.11 → 0.2.6

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.
@@ -0,0 +1,25 @@
1
+ export async function createCampaign(client, params) {
2
+ return client.managedPost("/api/v1/campaigns", params);
3
+ }
4
+ export async function listCampaigns(client, status, limit = 200) {
5
+ const params = new URLSearchParams();
6
+ if (status)
7
+ params.set("status", status);
8
+ params.set("limit", String(limit));
9
+ return client.managedGet(`/api/v1/campaigns?${params.toString()}`);
10
+ }
11
+ export async function getCampaign(client, campaignId) {
12
+ return client.managedGet(`/api/v1/campaigns/${campaignId}`);
13
+ }
14
+ export async function updateCampaign(client, campaignId, params) {
15
+ return client.managedPut(`/api/v1/campaigns/${campaignId}`, params);
16
+ }
17
+ export async function deleteCampaign(client, campaignId) {
18
+ return client.managedDelete(`/api/v1/campaigns/${campaignId}`);
19
+ }
20
+ export async function previewAudience(client, audienceFilter) {
21
+ return client.managedPost("/api/v1/campaigns/preview", { audienceFilter });
22
+ }
23
+ export async function getCampaignAnalytics(client, campaignId) {
24
+ return client.managedGet(`/api/v1/campaigns/${campaignId}/analytics`);
25
+ }
@@ -0,0 +1,75 @@
1
+ import { AscendKitClient } from "../api/client.js";
2
+ export interface ImportUserRecord {
3
+ sourceId: string;
4
+ email: string;
5
+ name?: string;
6
+ image?: string;
7
+ emailVerified?: boolean;
8
+ hadPassword?: boolean;
9
+ oauthProviders?: Array<{
10
+ providerId: string;
11
+ accountId: string;
12
+ }>;
13
+ metadata?: Record<string, unknown>;
14
+ tags?: string[];
15
+ createdAt?: string;
16
+ lastLoginAt?: string;
17
+ }
18
+ export interface ImportUsersPayload {
19
+ source: string;
20
+ users: ImportUserRecord[];
21
+ authSettings?: {
22
+ enabledProviders?: string[];
23
+ oauthClientIds?: Record<string, string>;
24
+ };
25
+ dryRun?: boolean;
26
+ }
27
+ export interface ImportResult {
28
+ imported: number;
29
+ duplicates: Array<{
30
+ email: string;
31
+ sourceId: string;
32
+ }>;
33
+ errors: Array<{
34
+ email: string;
35
+ sourceId: string;
36
+ reason: string;
37
+ }>;
38
+ warnings: string[];
39
+ }
40
+ export declare function importUsers(client: AscendKitClient, payload: ImportUsersPayload): Promise<ImportResult>;
41
+ export declare function instantiateMigrationJourney(client: AscendKitClient, fromIdentityEmail?: string): Promise<unknown>;
42
+ interface ClerkUser {
43
+ id: string;
44
+ email_addresses?: Array<{
45
+ id: string;
46
+ email_address: string;
47
+ verification?: {
48
+ status: string;
49
+ };
50
+ }>;
51
+ primary_email_address_id?: string;
52
+ first_name?: string;
53
+ last_name?: string;
54
+ image_url?: string;
55
+ password_enabled?: boolean;
56
+ external_accounts?: Array<{
57
+ id: string;
58
+ provider: string;
59
+ provider_user_id: string;
60
+ }>;
61
+ public_metadata?: Record<string, unknown>;
62
+ private_metadata?: Record<string, unknown>;
63
+ created_at?: number;
64
+ last_sign_in_at?: number;
65
+ last_active_at?: number;
66
+ }
67
+ export declare function transformClerkUser(clerk: ClerkUser): ImportUserRecord;
68
+ export declare function fetchClerkUsers(apiKey: string, instanceUrl?: string): Promise<ClerkUser[]>;
69
+ export interface ClerkDerivedSettings {
70
+ hasCredentials: boolean;
71
+ providers: string[];
72
+ }
73
+ export declare function deriveSettingsFromUsers(users: ImportUserRecord[]): ClerkDerivedSettings;
74
+ export declare function parseClerkExport(filePath: string): ClerkUser[];
75
+ export {};
@@ -0,0 +1,97 @@
1
+ import { readFileSync } from "fs";
2
+ export async function importUsers(client, payload) {
3
+ return client.managedPost("/api/import/users", payload);
4
+ }
5
+ export async function instantiateMigrationJourney(client, fromIdentityEmail) {
6
+ return client.managedPost("/api/import/instantiate-migration-journey", {
7
+ fromIdentityEmail: fromIdentityEmail ?? null,
8
+ });
9
+ }
10
+ function clerkProviderToAscendKit(provider) {
11
+ const map = {
12
+ oauth_google: "google",
13
+ oauth_github: "github",
14
+ oauth_linkedin: "linkedin",
15
+ oauth_linkedin_oidc: "linkedin",
16
+ };
17
+ return map[provider] ?? provider.replace(/^oauth_/, "");
18
+ }
19
+ export function transformClerkUser(clerk) {
20
+ const primaryEmail = clerk.email_addresses?.find((e) => e.id === clerk.primary_email_address_id);
21
+ const email = primaryEmail?.email_address ?? clerk.email_addresses?.[0]?.email_address ?? "";
22
+ const emailVerified = primaryEmail?.verification?.status === "verified";
23
+ const name = [clerk.first_name, clerk.last_name].filter(Boolean).join(" ") || undefined;
24
+ const oauthProviders = (clerk.external_accounts ?? []).map((ext) => ({
25
+ providerId: clerkProviderToAscendKit(ext.provider),
26
+ accountId: ext.provider_user_id,
27
+ }));
28
+ // Clerk timestamps are Unix ms
29
+ const createdAt = clerk.created_at
30
+ ? new Date(clerk.created_at).toISOString()
31
+ : undefined;
32
+ const lastLoginAt = clerk.last_sign_in_at
33
+ ? new Date(clerk.last_sign_in_at).toISOString()
34
+ : undefined;
35
+ return {
36
+ sourceId: clerk.id,
37
+ email,
38
+ name,
39
+ image: clerk.image_url || undefined,
40
+ emailVerified,
41
+ hadPassword: clerk.password_enabled ?? false,
42
+ oauthProviders,
43
+ metadata: clerk.public_metadata ?? undefined,
44
+ createdAt,
45
+ lastLoginAt,
46
+ };
47
+ }
48
+ export async function fetchClerkUsers(apiKey, instanceUrl) {
49
+ const baseUrl = instanceUrl ?? "https://api.clerk.com";
50
+ const allUsers = [];
51
+ let offset = 0;
52
+ const limit = 100;
53
+ while (true) {
54
+ const url = `${baseUrl}/v1/users?limit=${limit}&offset=${offset}`;
55
+ const response = await fetch(url, {
56
+ headers: {
57
+ Authorization: `Bearer ${apiKey}`,
58
+ "Content-Type": "application/json",
59
+ },
60
+ });
61
+ if (!response.ok) {
62
+ const text = await response.text();
63
+ throw new Error(`Clerk API error ${response.status}: ${text}`);
64
+ }
65
+ const users = (await response.json());
66
+ if (users.length === 0)
67
+ break;
68
+ allUsers.push(...users);
69
+ offset += users.length;
70
+ if (users.length < limit)
71
+ break;
72
+ }
73
+ return allUsers;
74
+ }
75
+ export function deriveSettingsFromUsers(users) {
76
+ let hasCredentials = false;
77
+ const providers = new Set();
78
+ for (const u of users) {
79
+ if (u.hadPassword)
80
+ hasCredentials = true;
81
+ for (const p of u.oauthProviders ?? []) {
82
+ providers.add(p.providerId);
83
+ }
84
+ }
85
+ return { hasCredentials, providers: [...providers].sort() };
86
+ }
87
+ export function parseClerkExport(filePath) {
88
+ const raw = readFileSync(filePath, "utf-8");
89
+ const parsed = JSON.parse(raw);
90
+ if (Array.isArray(parsed)) {
91
+ return parsed;
92
+ }
93
+ if (parsed.users && Array.isArray(parsed.users)) {
94
+ return parsed.users;
95
+ }
96
+ throw new Error("Unrecognized Clerk export format. Expected a JSON array of users or an object with a 'users' key.");
97
+ }
@@ -41,6 +41,7 @@ export interface AddNodeParams {
41
41
  surveySlug?: string;
42
42
  tagName?: string;
43
43
  stageName?: string;
44
+ fromIdentityEmail?: string;
44
45
  };
45
46
  terminal?: boolean;
46
47
  }
@@ -51,6 +52,7 @@ export interface EditNodeParams {
51
52
  surveySlug?: string;
52
53
  tagName?: string;
53
54
  stageName?: string;
55
+ fromIdentityEmail?: string;
54
56
  };
55
57
  terminal?: boolean;
56
58
  }
@@ -17,7 +17,7 @@ export interface McpCreateProjectParams {
17
17
  }
18
18
  export interface McpCreateEnvironmentParams {
19
19
  projectId: string;
20
- name: string;
20
+ name?: string;
21
21
  description?: string;
22
22
  tier: string;
23
23
  }
@@ -34,8 +34,24 @@ export declare function mcpListProjects(client: AscendKitClient): Promise<unknow
34
34
  export declare function mcpCreateProject(client: AscendKitClient, params: McpCreateProjectParams): Promise<unknown>;
35
35
  export declare function mcpListEnvironments(client: AscendKitClient, projectId: string): Promise<unknown>;
36
36
  export declare function mcpCreateEnvironment(client: AscendKitClient, params: McpCreateEnvironmentParams): Promise<unknown>;
37
+ export declare function updateEnvironment(projectId: string, envId: string, name?: string, description?: string): Promise<unknown>;
37
38
  export declare function promoteEnvironment(environmentId: string, targetTier: string): Promise<unknown>;
39
+ export interface McpUpdateEnvironmentParams {
40
+ projectId: string;
41
+ environmentId: string;
42
+ name?: string;
43
+ description?: string;
44
+ }
45
+ export declare function mcpUpdateEnvironment(client: AscendKitClient, params: McpUpdateEnvironmentParams): Promise<unknown>;
38
46
  export declare function mcpPromoteEnvironment(client: AscendKitClient, params: {
39
47
  environmentId: string;
40
48
  targetTier: string;
41
49
  }): Promise<unknown>;
50
+ export interface McpUpdateEnvironmentVariablesParams {
51
+ projectId: string;
52
+ envId: string;
53
+ variables: Record<string, string>;
54
+ }
55
+ export declare function mcpUpdateEnvironmentVariables(client: AscendKitClient, params: McpUpdateEnvironmentVariablesParams): Promise<unknown>;
56
+ export declare function getEnvironment(projectId: string, envId: string): Promise<Record<string, unknown>>;
57
+ export declare function updateEnvironmentVariables(projectId: string, envId: string, variables: Record<string, string>): Promise<unknown>;
@@ -184,7 +184,7 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
184
184
  const existingPublicKey = readEnvValue(original, "ASCENDKIT_ENV_KEY");
185
185
  const existingSecretKey = readEnvValue(original, "ASCENDKIT_SECRET_KEY");
186
186
  const existingWebhookSecret = readEnvValue(original, "ASCENDKIT_WEBHOOK_SECRET") ?? "";
187
- const resolvedApiUrl = existingApiUrl ?? apiUrl;
187
+ const resolvedApiUrl = apiUrl;
188
188
  const resolvedPublicKey = preserveExistingKeys
189
189
  ? (existingPublicKey && existingPublicKey.trim() ? existingPublicKey : publicKey)
190
190
  : publicKey;
@@ -535,10 +535,43 @@ export async function mcpListEnvironments(client, projectId) {
535
535
  export async function mcpCreateEnvironment(client, params) {
536
536
  return client.platformRequest("POST", `/api/platform/projects/${params.projectId}/environments`, { name: params.name, description: params.description ?? "", tier: params.tier });
537
537
  }
538
+ export async function updateEnvironment(projectId, envId, name, description) {
539
+ const auth = requireAuth();
540
+ const body = {};
541
+ if (name)
542
+ body.name = name;
543
+ if (description !== undefined)
544
+ body.description = description;
545
+ return apiRequest(auth.apiUrl, "PATCH", `/api/platform/projects/${projectId}/environments/${envId}`, body, auth.token);
546
+ }
538
547
  export async function promoteEnvironment(environmentId, targetTier) {
539
548
  const auth = requireAuth();
540
549
  return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier, dryRun: false }, auth.token);
541
550
  }
551
+ export async function mcpUpdateEnvironment(client, params) {
552
+ const body = {};
553
+ if (params.name)
554
+ body.name = params.name;
555
+ if (params.description !== undefined)
556
+ body.description = params.description;
557
+ return client.platformRequest("PATCH", `/api/platform/projects/${params.projectId}/environments/${params.environmentId}`, body);
558
+ }
542
559
  export async function mcpPromoteEnvironment(client, params) {
543
560
  return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: params.targetTier, dryRun: false });
544
561
  }
562
+ export async function mcpUpdateEnvironmentVariables(client, params) {
563
+ return client.platformRequest("PATCH", `/api/platform/projects/${params.projectId}/environments/${params.envId}`, { variables: params.variables });
564
+ }
565
+ export async function getEnvironment(projectId, envId) {
566
+ const auth = requireAuth();
567
+ const envs = await apiRequest(auth.apiUrl, "GET", `/api/platform/projects/${projectId}/environments`, undefined, auth.token);
568
+ const env = envs.find(e => e.id === envId);
569
+ if (!env) {
570
+ throw new Error(`Environment ${envId} not found in project ${projectId}.`);
571
+ }
572
+ return env;
573
+ }
574
+ export async function updateEnvironmentVariables(projectId, envId, variables) {
575
+ const auth = requireAuth();
576
+ return apiRequest(auth.apiUrl, "PATCH", `/api/platform/projects/${projectId}/environments/${envId}`, { variables }, auth.token);
577
+ }
package/dist/mcp.js CHANGED
@@ -10,6 +10,8 @@ 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";
14
16
  const client = new AscendKitClient({
15
17
  apiUrl: process.env.ASCENDKIT_API_URL ?? DEFAULT_API_URL,
@@ -34,6 +36,8 @@ registerSurveyTools(server, client);
34
36
  registerEmailTools(server, client);
35
37
  registerJourneyTools(server, client);
36
38
  registerWebhookTools(server, client);
39
+ registerCampaignTools(server, client);
40
+ registerImportTools(server, client);
37
41
  async function main() {
38
42
  const transport = new StdioServerTransport();
39
43
  await server.connect(transport);
@@ -35,12 +35,31 @@ export function registerAuthTools(server, client) {
35
35
  const data = await auth.updateProviders(client, params.providers);
36
36
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
37
37
  });
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.", {
38
+ server.tool("auth_setup_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
39
  provider: z.string().describe('OAuth provider name, e.g. "google", "github"'),
40
40
  clientId: z.string().optional().describe("OAuth client ID (to set credentials directly)"),
41
41
  clientSecret: z.string().optional().describe("OAuth client secret (to set credentials directly)"),
42
42
  callbackUrl: z.string().optional().describe("Auth callback URL for this provider"),
43
+ useProxy: z.boolean().optional().describe("Clear custom credentials and use AscendKit managed proxy credentials"),
43
44
  }, async (params) => {
45
+ if (params.useProxy && (params.clientId || params.clientSecret)) {
46
+ return {
47
+ content: [{
48
+ type: "text",
49
+ text: "Cannot use useProxy with custom credentials. Either set useProxy to clear credentials, or provide clientId and clientSecret.",
50
+ }],
51
+ isError: true,
52
+ };
53
+ }
54
+ if (params.useProxy) {
55
+ const data = await auth.deleteOAuthCredentials(client, params.provider);
56
+ return {
57
+ content: [{
58
+ type: "text",
59
+ text: `Cleared custom OAuth credentials for ${params.provider}. Now using AscendKit managed proxy credentials.\n\n${JSON.stringify(data, null, 2)}`,
60
+ }],
61
+ };
62
+ }
44
63
  if ((params.clientId && !params.clientSecret) || (!params.clientId && params.clientSecret)) {
45
64
  return {
46
65
  content: [{
@@ -72,4 +91,22 @@ export function registerAuthTools(server, client) {
72
91
  const data = await auth.listUsers(client);
73
92
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
74
93
  });
94
+ server.tool("auth_delete_user", "Deactivate a user (soft delete). The user record is preserved but marked inactive.", {
95
+ userId: z.string().describe("User ID (usr_ prefixed)"),
96
+ }, async (params) => {
97
+ const data = await auth.deleteUser(client, params.userId);
98
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
99
+ });
100
+ server.tool("auth_bulk_delete_users", "Bulk deactivate users (soft delete). All specified users are marked inactive.", {
101
+ userIds: z.array(z.string()).min(1).max(100).describe("Array of user IDs (usr_ prefixed)"),
102
+ }, async (params) => {
103
+ const data = await auth.bulkDeleteUsers(client, params.userIds);
104
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
105
+ });
106
+ server.tool("auth_reactivate_user", "Reactivate a previously deactivated user.", {
107
+ userId: z.string().describe("User ID (usr_ prefixed)"),
108
+ }, async (params) => {
109
+ const data = await auth.reactivateUser(client, params.userId);
110
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
111
+ });
75
112
  }
@@ -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;
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import * as campaigns from "../commands/campaigns.js";
3
+ export function registerCampaignTools(server, client) {
4
+ server.tool("campaign_create", "Create a new email campaign targeting users that match an audience filter. Campaigns start as drafts; set scheduledAt to schedule for future delivery. Campaign lifecycle: create → preview audience → schedule → sending → sent.", {
5
+ name: z.string().describe("Campaign name, e.g. 'March Newsletter'"),
6
+ templateId: z.string().describe("Email template ID (tpl_ prefixed) to use for the campaign"),
7
+ audienceFilter: z
8
+ .record(z.unknown())
9
+ .describe("Filter object to select target users (e.g. { tags: { $in: ['premium'] } }). Allowed fields: createdAt, emailVerified, lastLoginAt, tags, stage, name, email, waitlistStatus, metadata.*"),
10
+ scheduledAt: z
11
+ .string()
12
+ .optional()
13
+ .describe("ISO 8601 datetime to schedule sending (omit to keep as draft)"),
14
+ fromIdentityEmail: z
15
+ .string()
16
+ .optional()
17
+ .describe("Verified email identity to use as sender. Falls back to environment default."),
18
+ }, async (params) => {
19
+ const data = await campaigns.createCampaign(client, params);
20
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
21
+ });
22
+ server.tool("campaign_list", "List campaigns for the current project. Optionally filter by status: draft, scheduled, sending, sent, failed, or cancelled.", {
23
+ status: z
24
+ .enum(["draft", "scheduled", "sending", "sent", "failed", "cancelled"])
25
+ .optional()
26
+ .describe("Filter by campaign status"),
27
+ }, async (params) => {
28
+ const data = await campaigns.listCampaigns(client, params.status);
29
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30
+ });
31
+ server.tool("campaign_get", "Get full details of a campaign including its status, template, audience filter, and schedule.", {
32
+ campaignId: z.string().describe("Campaign ID (cmp_ prefixed)"),
33
+ }, async (params) => {
34
+ const data = await campaigns.getCampaign(client, params.campaignId);
35
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
36
+ });
37
+ server.tool("campaign_update", "Update a draft, scheduled, or failed campaign. You can change the name, template, audience filter, or schedule. Only provided fields are updated.", {
38
+ campaignId: z.string().describe("Campaign ID (cmp_ prefixed)"),
39
+ name: z.string().optional().describe("New campaign name"),
40
+ templateId: z.string().optional().describe("New email template ID (tpl_ prefixed)"),
41
+ audienceFilter: z
42
+ .record(z.unknown())
43
+ .optional()
44
+ .describe("New audience filter object"),
45
+ scheduledAt: z
46
+ .string()
47
+ .optional()
48
+ .describe("New scheduled send time (ISO 8601 datetime)"),
49
+ fromIdentityEmail: z
50
+ .string()
51
+ .optional()
52
+ .describe("Verified email identity to use as sender. Falls back to environment default."),
53
+ }, async (params) => {
54
+ const { campaignId, ...rest } = params;
55
+ const data = await campaigns.updateCampaign(client, campaignId, rest);
56
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
57
+ });
58
+ server.tool("campaign_delete", "Delete a draft or failed campaign, or cancel a scheduled/sending campaign. Sent campaigns cannot be deleted.", {
59
+ campaignId: z.string().describe("Campaign ID (cmp_ prefixed)"),
60
+ }, async (params) => {
61
+ const data = await campaigns.deleteCampaign(client, params.campaignId);
62
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
63
+ });
64
+ server.tool("campaign_preview_audience", "Preview how many users match an audience filter before creating or updating a campaign. Returns the count and a sample of matching users.", {
65
+ audienceFilter: z
66
+ .record(z.unknown())
67
+ .describe("Filter object to preview (e.g. { tags: { $in: ['premium'] } }). Allowed fields: createdAt, emailVerified, lastLoginAt, tags, stage, name, email, waitlistStatus, metadata.*"),
68
+ }, async (params) => {
69
+ const data = await campaigns.previewAudience(client, params.audienceFilter);
70
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
71
+ });
72
+ server.tool("campaign_analytics", "Get campaign performance analytics: delivery stats (sent, failed, bounced), engagement metrics (opened, clicked), and calculated rates.", {
73
+ campaignId: z.string().describe("Campaign ID (cmp_ prefixed)"),
74
+ }, async (params) => {
75
+ const data = await campaigns.getCampaignAnalytics(client, params.campaignId);
76
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
77
+ });
78
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AscendKitClient } from "../api/client.js";
3
+ export declare function registerImportTools(server: McpServer, client: AscendKitClient): void;
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import * as importCmd from "../commands/import.js";
3
+ export function registerImportTools(server, client) {
4
+ server.tool("auth_import_clerk", "Import users from Clerk into the current AscendKit environment. Fetches all users from the Clerk API, transforms them to AscendKit format, and pushes them to the import endpoint. Tags users by auth type: import:needs-password-reset or import:social-only. Defaults to dry-run — pass execute=true to apply changes.", {
5
+ clerkApiKey: z
6
+ .string()
7
+ .describe("Clerk secret API key (sk_live_... or sk_test_...)"),
8
+ clerkInstanceUrl: z
9
+ .string()
10
+ .optional()
11
+ .describe("Custom Clerk API URL (default: https://api.clerk.com)"),
12
+ execute: z
13
+ .boolean()
14
+ .optional()
15
+ .describe("Set to true to apply changes. Default is dry-run (preview only)."),
16
+ importUsers: z
17
+ .boolean()
18
+ .optional()
19
+ .describe("Import users (default: true)"),
20
+ importSettings: z
21
+ .boolean()
22
+ .optional()
23
+ .describe("Import auth settings — OAuth provider config (default: true)"),
24
+ }, async (params) => {
25
+ try {
26
+ const dryRun = !(params.execute ?? false);
27
+ const shouldImportUsers = params.importUsers ?? true;
28
+ const shouldImportSettings = params.importSettings ?? true;
29
+ const rawUsers = await importCmd.fetchClerkUsers(params.clerkApiKey, params.clerkInstanceUrl);
30
+ const users = rawUsers.map(importCmd.transformClerkUser);
31
+ if (users.length === 0) {
32
+ return {
33
+ content: [{ type: "text", text: "No users found in Clerk." }],
34
+ };
35
+ }
36
+ let totalImported = 0;
37
+ let totalDuplicates = 0;
38
+ const allErrors = [];
39
+ const allWarnings = [];
40
+ // Import users in batches of 500
41
+ if (shouldImportUsers) {
42
+ const batchSize = 500;
43
+ for (let i = 0; i < users.length; i += batchSize) {
44
+ const batch = users.slice(i, i + batchSize);
45
+ const result = await importCmd.importUsers(client, {
46
+ source: "clerk",
47
+ users: batch,
48
+ dryRun,
49
+ });
50
+ totalImported += result.imported;
51
+ totalDuplicates += result.duplicates.length;
52
+ allErrors.push(...result.errors);
53
+ allWarnings.push(...result.warnings);
54
+ }
55
+ }
56
+ // Import auth settings (OAuth provider config)
57
+ if (shouldImportSettings) {
58
+ const providers = new Set();
59
+ for (const u of users) {
60
+ for (const p of u.oauthProviders ?? []) {
61
+ providers.add(p.providerId);
62
+ }
63
+ }
64
+ if (providers.size > 0) {
65
+ const settingsResult = await importCmd.importUsers(client, {
66
+ source: "clerk",
67
+ users: [],
68
+ authSettings: { enabledProviders: [...providers] },
69
+ dryRun,
70
+ });
71
+ allWarnings.push(...settingsResult.warnings);
72
+ }
73
+ }
74
+ const summary = {
75
+ fetched: users.length,
76
+ dryRun,
77
+ importUsers: shouldImportUsers,
78
+ importSettings: shouldImportSettings,
79
+ };
80
+ if (shouldImportUsers) {
81
+ summary.imported = totalImported;
82
+ summary.duplicates = totalDuplicates;
83
+ }
84
+ if (allErrors.length > 0)
85
+ summary.errors = allErrors;
86
+ if (allWarnings.length > 0)
87
+ summary.warnings = allWarnings;
88
+ let text = JSON.stringify(summary, null, 2);
89
+ if (dryRun) {
90
+ text += "\n\nThis was a dry run. Pass execute=true to apply changes.";
91
+ }
92
+ else if (totalImported > 0) {
93
+ text += "\n\nTo set up migration emails, use the journey_create_migration tool.";
94
+ }
95
+ return { content: [{ type: "text", text }] };
96
+ }
97
+ catch (err) {
98
+ return {
99
+ content: [{
100
+ type: "text",
101
+ text: `Import failed: ${err instanceof Error ? err.message : String(err)}`,
102
+ }],
103
+ isError: true,
104
+ };
105
+ }
106
+ });
107
+ server.tool("journey_create_migration", "Create pre-built migration email templates and draft journeys for notifying imported users. Creates 5 email templates (announcement, go-live, reminder, password reset, password reset reminder) and 2 journeys: announcement cadence (all users, Day 0/3/7) and password reset cadence (credential users, Day 1/4). Idempotent — skips already-existing templates/journeys.", {
108
+ fromIdentityEmail: z
109
+ .string()
110
+ .optional()
111
+ .describe("Verified email identity to use as sender for migration emails"),
112
+ }, async (params) => {
113
+ const data = await importCmd.instantiateMigrationJourney(client, params.fromIdentityEmail);
114
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
115
+ });
116
+ }
@@ -14,7 +14,7 @@ export function registerJourneyTools(server, client) {
14
14
  nodes: z
15
15
  .record(z.record(z.unknown()))
16
16
  .optional()
17
- .describe("Map of node names to definitions. Each node has 'action' ({type, templateSlug?, surveySlug?, tagName?, stageName?}) and optional 'terminal' (boolean). If omitted, the entry node is auto-created as a 'none' action placeholder."),
17
+ .describe("Map of node names to definitions. Each node has 'action' ({type, templateSlug?, surveySlug?, tagName?, stageName?, variables?}) and optional 'terminal' (boolean). If omitted, the entry node is auto-created as a 'none' action placeholder."),
18
18
  transitions: z
19
19
  .array(z.record(z.unknown()))
20
20
  .optional()
@@ -138,6 +138,8 @@ export function registerJourneyTools(server, client) {
138
138
  surveySlug: z.string().optional().describe("Survey slug to include in email (for send_email)"),
139
139
  tagName: z.string().optional().describe("Tag to add (for tag_user)"),
140
140
  stageName: z.string().optional().describe("Stage to set (for advance_stage)"),
141
+ fromIdentityEmail: z.string().optional().describe("Verified email identity to use when this node sends email"),
142
+ variables: z.record(z.string()).optional().describe("Custom template variables for this node's email (e.g. {loginUrl: 'https://...', resetUrl: 'https://...'})"),
141
143
  })
142
144
  .optional()
143
145
  .describe("Action to execute when a user enters this node. Defaults to {type: 'none'}."),
@@ -161,6 +163,8 @@ export function registerJourneyTools(server, client) {
161
163
  surveySlug: z.string().optional().describe("Survey slug to include in email"),
162
164
  tagName: z.string().optional().describe("Tag to add"),
163
165
  stageName: z.string().optional().describe("Stage to set"),
166
+ fromIdentityEmail: z.string().optional().describe("Verified email identity to use when this node sends email"),
167
+ variables: z.record(z.string()).optional().describe("Custom template variables for this node's email (e.g. {loginUrl: 'https://...', resetUrl: 'https://...'})"),
164
168
  })
165
169
  .optional()
166
170
  .describe("New action definition (replaces the entire action)"),