@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.
@@ -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
+ }
@@ -1,7 +1,19 @@
1
1
  import { z } from "zod";
2
2
  import * as content from "../commands/content.js";
3
+ function formatTemplateSummary(data) {
4
+ const lines = [`Template: ${data.name} (${data.id})`];
5
+ if (data.slug)
6
+ lines.push(`Slug: ${data.slug}`);
7
+ lines.push(`Subject: ${data.subject ?? "-"}`);
8
+ if (data.currentVersion != null)
9
+ lines.push(`Version: ${data.currentVersion}`);
10
+ if (Array.isArray(data.variables) && data.variables.length > 0) {
11
+ lines.push(`Variables: ${data.variables.join(", ")}`);
12
+ }
13
+ return lines.join("\n");
14
+ }
3
15
  export function registerContentTools(server, client) {
4
- server.tool("content_create_template", "Create a new email content template with HTML and plain-text versions. Supports {{variable}} placeholders.", {
16
+ server.tool("template_create", "Create a new email content template with HTML and plain-text versions. Supports {{variable}} placeholders.", {
5
17
  name: z.string().describe("Template name, e.g. 'welcome-email'"),
6
18
  subject: z.string().describe("Email subject line"),
7
19
  bodyHtml: z.string().describe("HTML email body. Use {{variable}} for placeholders"),
@@ -10,9 +22,9 @@ export function registerContentTools(server, client) {
10
22
  description: z.string().optional().describe("Template description"),
11
23
  }, async (params) => {
12
24
  const data = await content.createTemplate(client, params);
13
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
25
+ return { content: [{ type: "text", text: formatTemplateSummary(data) }] };
14
26
  });
15
- server.tool("content_list_templates", "List all content templates for the current project", {
27
+ server.tool("template_list", "List all content templates for the current project", {
16
28
  query: z.string().optional().describe("Search templates by name, slug, or description"),
17
29
  isSystem: z.boolean().optional().describe("Filter: true for system templates, false for custom"),
18
30
  }, async (params) => {
@@ -22,13 +34,13 @@ export function registerContentTools(server, client) {
22
34
  const data = await content.listTemplates(client, listParams);
23
35
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
24
36
  });
25
- server.tool("content_get_template", "Get a content template by ID, including current version body and variables", {
37
+ server.tool("template_show", "Get a content template by ID, including current version body and variables", {
26
38
  templateId: z.string().describe("Template ID (tpl_ prefixed)"),
27
39
  }, async (params) => {
28
40
  const data = await content.getTemplate(client, params.templateId);
29
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
41
+ return { content: [{ type: "text", text: formatTemplateSummary(data) }] };
30
42
  });
31
- server.tool("content_update_template", "Update a content template. Creates a new immutable version — previous versions are preserved for result tracking.", {
43
+ server.tool("template_update", "Update a content template. Creates a new immutable version — previous versions are preserved for result tracking.", {
32
44
  templateId: z.string().describe("Template ID (tpl_ prefixed)"),
33
45
  subject: z.string().optional().describe("New email subject line"),
34
46
  bodyHtml: z.string().optional().describe("New HTML email body"),
@@ -40,21 +52,21 @@ export function registerContentTools(server, client) {
40
52
  }, async (params) => {
41
53
  const { templateId, ...rest } = params;
42
54
  const data = await content.updateTemplate(client, templateId, rest);
43
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
55
+ return { content: [{ type: "text", text: formatTemplateSummary(data) }] };
44
56
  });
45
- server.tool("content_delete_template", "Delete a content template and all its versions", {
57
+ server.tool("template_remove", "Delete a content template and all its versions", {
46
58
  templateId: z.string().describe("Template ID (tpl_ prefixed)"),
47
59
  }, async (params) => {
48
- const data = await content.deleteTemplate(client, params.templateId);
49
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
60
+ await content.deleteTemplate(client, params.templateId);
61
+ return { content: [{ type: "text", text: `Removed template ${params.templateId}.` }] };
50
62
  });
51
- server.tool("content_list_versions", "List all versions of a content template (newest first)", {
63
+ server.tool("template_version_list", "List all versions of a content template (newest first)", {
52
64
  templateId: z.string().describe("Template ID (tpl_ prefixed)"),
53
65
  }, async (params) => {
54
66
  const data = await content.listVersions(client, params.templateId);
55
67
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
56
68
  });
57
- server.tool("content_get_version", "Get a specific version of a content template", {
69
+ server.tool("template_version_show", "Get a specific version of a content template", {
58
70
  templateId: z.string().describe("Template ID (tpl_ prefixed)"),
59
71
  versionNumber: z.number().describe("Version number to retrieve"),
60
72
  }, async (params) => {
@@ -1,23 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import * as email from "../commands/email.js";
3
3
  export function registerEmailTools(server, client) {
4
- server.tool("email_get_settings", "Get email settings (sender address, domain, verification status)", {}, async () => {
4
+ server.tool("email_identity_show", "Get email domain and sender identity settings for the current environment", {}, async () => {
5
5
  const data = await email.getSettings(client);
6
6
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
7
7
  });
8
- server.tool("email_update_settings", "Update email sender settings (fromEmail, fromName)", {
9
- fromEmail: z.string().optional().describe("Sender email address"),
10
- fromName: z.string().optional().describe("Sender display name"),
11
- }, async (params) => {
12
- const data = await email.updateSettings(client, params);
13
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
14
- });
15
- server.tool("email_setup_domain", "Initiate domain verification for email sending. Returns DNS records, detected DNS provider details, and guided setup links.", {
8
+ server.tool("email_identity_setup_domain", "Initiate domain verification for email sending. Returns DNS records, detected DNS provider details, and guided setup links.", {
16
9
  domain: z.string().describe("Domain to verify (e.g. 'myapp.com')"),
17
- dnsProvider: z
18
- .string()
19
- .optional()
20
- .describe("Optional preferred DNS provider hint, e.g. 'cloudflare'."),
21
10
  }, async (params) => {
22
11
  const data = await email.setupDomain(client, params.domain);
23
12
  // Manual DNS setup — show records for copy-paste
@@ -36,22 +25,63 @@ export function registerEmailTools(server, client) {
36
25
  return {
37
26
  content: [{
38
27
  type: "text",
39
- text: `Domain ${params.domain} identity created in SES.\n\n${providerLine}${portalLine}${guidedLine}\nAdd these DNS records to your domain:\n${formatted}\n\nAfter adding the records, use email_check_domain_status to poll for verification.`,
28
+ text: `Domain ${params.domain} identity created in SES.\n\n${providerLine}${portalLine}${guidedLine}\nAdd these DNS records to your domain:\n${formatted}\n\nAfter adding the records, use email_identity_status to poll for verification.`,
40
29
  }],
41
30
  };
42
31
  });
43
- server.tool("email_get_dns_provider", "Detect DNS provider for a domain and return provider dashboard links", {
32
+ server.tool("email_identity_check_dns", "Check the configured email DNS records against public DNS", {}, async () => {
33
+ const data = await email.checkDnsRecords(client);
34
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
35
+ });
36
+ server.tool("email_identity_dns_provider", "Detect DNS provider for a domain and return provider dashboard links", {
44
37
  domain: z.string().optional().describe("Domain to detect; defaults to configured email domain"),
45
38
  }, async (params) => {
46
39
  const data = await email.getDnsProvider(client, params.domain);
47
40
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
48
41
  });
49
- server.tool("email_check_domain_status", "Poll SES verification status for your configured domain", {}, async () => {
42
+ server.tool("email_identity_status", "Poll SES verification status for your configured domain", {}, async () => {
50
43
  const data = await email.checkDomainStatus(client);
51
44
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
52
45
  });
53
- server.tool("email_remove_domain", "Remove domain and reset to default AscendKit sender", {}, async () => {
46
+ server.tool("email_identity_remove_domain", "Remove domain and reset to default AscendKit sender", {}, async () => {
54
47
  const data = await email.removeDomain(client);
55
48
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
56
49
  });
50
+ server.tool("email_identity_list", "List sender identities for the current environment and refresh their SES verification status", {}, async () => {
51
+ const data = await email.listIdentities(client);
52
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
53
+ });
54
+ server.tool("email_identity_add", "Add a sender email identity and request SES inbox verification if needed", {
55
+ email: z.string().describe("Email identity to add, e.g. 'hello@myapp.com'"),
56
+ displayName: z.string().optional().describe("Optional display name for this sender"),
57
+ }, async (params) => {
58
+ const data = await email.createIdentity(client, params);
59
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
60
+ });
61
+ server.tool("email_identity_resend", "Request SES to resend inbox verification for a pending sender identity", {
62
+ email: z.string().describe("Sender identity email"),
63
+ }, async (params) => {
64
+ const data = await email.resendIdentityVerification(client, params.email);
65
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
66
+ });
67
+ server.tool("email_identity_set_default", "Set the default sender identity for this environment", {
68
+ email: z.string().describe("Sender identity email"),
69
+ displayName: z.string().optional().describe("Optional updated display name"),
70
+ }, async (params) => {
71
+ const data = await email.setDefaultIdentity(client, params);
72
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
73
+ });
74
+ server.tool("email_identity_remove", "Remove a sender identity from this environment", {
75
+ email: z.string().describe("Sender identity email"),
76
+ }, async (params) => {
77
+ const data = await email.removeIdentity(client, params.email);
78
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
79
+ });
80
+ server.tool("email_identity_test", "Send a test email from the selected verified identity", {
81
+ to: z.string().describe("Recipient address for the test email"),
82
+ fromIdentityEmail: z.string().optional().describe("Verified sender identity to use"),
83
+ }, async (params) => {
84
+ const data = await email.sendTestEmail(client, params);
85
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
86
+ });
57
87
  }
@@ -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("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. Use mode='preview' for dry-run or mode='run' to apply changes.", {
5
+ mode: z
6
+ .enum(["preview", "run"])
7
+ .default("preview")
8
+ .describe("Use 'preview' for dry-run or 'run' to apply changes"),
9
+ clerkApiKey: z
10
+ .string()
11
+ .describe("Clerk secret API key (sk_live_... or sk_test_...)"),
12
+ clerkInstanceUrl: z
13
+ .string()
14
+ .optional()
15
+ .describe("Custom Clerk API URL (default: https://api.clerk.com)"),
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.mode !== "run";
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. Use mode='run' to apply changes.";
91
+ }
92
+ else if (totalImported > 0) {
93
+ text += "\n\nTo set up migration emails, use the import_migration_journey_create 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("import_migration_journey_create", "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
+ }
@@ -3,7 +3,7 @@ import * as journeys from "../commands/journeys.js";
3
3
  import { parseDelay } from "../utils/duration.js";
4
4
  import { formatJourneyAnalytics, formatJourneyList, formatJourneyWithGuidance, formatNodeList, formatSingleNode, formatTransitionList, formatSingleTransition, } from "../utils/journey-format.js";
5
5
  export function registerJourneyTools(server, client) {
6
- server.tool("journey_create", "Create a new user lifecycle journey. Journeys start in 'draft' status. Minimal create: just name + entryEvent + entryNode (entry node auto-created as a 'none' action placeholder). Add nodes and transitions incrementally with journey_add_node and journey_add_transition. Lifecycle: create → add nodes/transitions → activate → users enroll on entryEvent.", {
6
+ server.tool("journey_create", "Create a new user lifecycle journey. Journeys start in 'draft' status. Minimal create: just name + entryEvent + entryNode (entry node auto-created as a 'none' action placeholder). Add nodes and transitions incrementally with journey_node_add and journey_transition_add. Lifecycle: create → add nodes/transitions → activate → users enroll on entryEvent.", {
7
7
  name: z.string().describe("Journey name, e.g. 'Onboarding Flow'"),
8
8
  entryEvent: z
9
9
  .string()
@@ -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?, fromIdentityEmail?, 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()
@@ -45,7 +45,7 @@ export function registerJourneyTools(server, client) {
45
45
  const formatted = formatJourneyList(data);
46
46
  return { content: [{ type: "text", text: formatted }] };
47
47
  });
48
- server.tool("journey_get", "Get a journey by ID with full definition (nodes, transitions) and current stats (enrolled, active, completed).", {
48
+ server.tool("journey_show", "Get a journey by ID with full definition (nodes, transitions) and current stats (enrolled, active, completed).", {
49
49
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
50
50
  }, async (params) => {
51
51
  const data = await journeys.getJourney(client, params.journeyId);
@@ -69,7 +69,7 @@ export function registerJourneyTools(server, client) {
69
69
  nodes: z
70
70
  .record(z.record(z.unknown()))
71
71
  .optional()
72
- .describe("Updated node definitions"),
72
+ .describe("Updated node definitions. send_email actions may include fromIdentityEmail to override the environment default sender for that node."),
73
73
  transitions: z
74
74
  .array(z.record(z.unknown()))
75
75
  .optional()
@@ -80,7 +80,7 @@ export function registerJourneyTools(server, client) {
80
80
  const formatted = formatJourneyWithGuidance(data);
81
81
  return { content: [{ type: "text", text: formatted }] };
82
82
  });
83
- server.tool("journey_delete", "Delete a journey and all associated user states. This is permanent.", {
83
+ server.tool("journey_remove", "Delete a journey and all associated user states. This is permanent.", {
84
84
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
85
85
  }, async (params) => {
86
86
  await journeys.deleteJourney(client, params.journeyId);
@@ -89,13 +89,20 @@ export function registerJourneyTools(server, client) {
89
89
  };
90
90
  });
91
91
  // --- Lifecycle tools ---
92
- server.tool("journey_activate", "Activate a journey (draft → active). Once active, users are automatically enrolled when the entry event fires.", {
92
+ server.tool("journey_activate", "Activate a draft journey (draft → active). Once active, users are automatically enrolled when the entry event fires.", {
93
93
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
94
94
  }, async (params) => {
95
95
  const data = await journeys.activateJourney(client, params.journeyId);
96
96
  const formatted = formatJourneyWithGuidance(data);
97
97
  return { content: [{ type: "text", text: formatted }] };
98
98
  });
99
+ server.tool("journey_resume", "Resume a paused journey (paused → active). Transitions continue while paused, and queued actions are drained on resume.", {
100
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
101
+ }, async (params) => {
102
+ const data = await journeys.resumeJourney(client, params.journeyId);
103
+ const formatted = formatJourneyWithGuidance(data);
104
+ return { content: [{ type: "text", text: formatted }] };
105
+ });
99
106
  server.tool("journey_pause", "Pause an active journey. Users stay at their current nodes but actions are queued instead of executed. Transitions still occur.", {
100
107
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
101
108
  }, async (params) => {
@@ -119,14 +126,14 @@ export function registerJourneyTools(server, client) {
119
126
  return { content: [{ type: "text", text: formatted }] };
120
127
  });
121
128
  // --- Granular node tools ---
122
- server.tool("journey_list_nodes", "List all nodes in a journey with their actions, terminal status, and transition counts. Use this to inspect the journey graph before making changes.", {
129
+ server.tool("journey_node_list", "List all nodes in a journey with their actions, terminal status, and transition counts. Use this to inspect the journey graph before making changes.", {
123
130
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
124
131
  }, async (params) => {
125
132
  const data = await journeys.listNodes(client, params.journeyId);
126
133
  const formatted = formatNodeList(data);
127
134
  return { content: [{ type: "text", text: formatted }] };
128
135
  });
129
- server.tool("journey_add_node", "Add a node to a journey. Nodes define what happens at each step: send_email (with optional surveySlug), tag_user, advance_stage, or none (wait/branching). Node name is the stable identifier used in transitions.", {
136
+ server.tool("journey_node_add", "Add a node to a journey. Nodes define what happens at each step: send_email (with optional surveySlug), tag_user, advance_stage, or none (wait/branching). Node name is the stable identifier used in transitions.", {
130
137
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
131
138
  name: z
132
139
  .string()
@@ -138,6 +145,8 @@ export function registerJourneyTools(server, client) {
138
145
  surveySlug: z.string().optional().describe("Survey slug to include in email (for send_email)"),
139
146
  tagName: z.string().optional().describe("Tag to add (for tag_user)"),
140
147
  stageName: z.string().optional().describe("Stage to set (for advance_stage)"),
148
+ fromIdentityEmail: z.string().optional().describe("Verified email identity to use when this node sends email"),
149
+ variables: z.record(z.string()).optional().describe("Custom template variables for this node's email (e.g. {loginUrl: 'https://...', resetUrl: 'https://...'})"),
141
150
  })
142
151
  .optional()
143
152
  .describe("Action to execute when a user enters this node. Defaults to {type: 'none'}."),
@@ -151,7 +160,7 @@ export function registerJourneyTools(server, client) {
151
160
  const formatted = formatSingleNode(data, "Added", params.name);
152
161
  return { content: [{ type: "text", text: formatted }] };
153
162
  });
154
- server.tool("journey_edit_node", "Update a node's action or terminal flag by name. Only provided fields are changed. To clear an action, set type to 'none'. Removing templateSlug also removes surveySlug (survey rides on email).", {
163
+ server.tool("journey_node_update", "Update a node's action or terminal flag by name. Only provided fields are changed. To clear an action, set type to 'none'. Removing templateSlug also removes surveySlug (survey rides on email).", {
155
164
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
156
165
  nodeName: z.string().describe("Name of the node to edit"),
157
166
  action: z
@@ -161,6 +170,8 @@ export function registerJourneyTools(server, client) {
161
170
  surveySlug: z.string().optional().describe("Survey slug to include in email"),
162
171
  tagName: z.string().optional().describe("Tag to add"),
163
172
  stageName: z.string().optional().describe("Stage to set"),
173
+ fromIdentityEmail: z.string().optional().describe("Verified email identity to use when this node sends email"),
174
+ variables: z.record(z.string()).optional().describe("Custom template variables for this node's email (e.g. {loginUrl: 'https://...', resetUrl: 'https://...'})"),
164
175
  })
165
176
  .optional()
166
177
  .describe("New action definition (replaces the entire action)"),
@@ -174,7 +185,7 @@ export function registerJourneyTools(server, client) {
174
185
  const formatted = formatSingleNode(data, "Updated", nodeName);
175
186
  return { content: [{ type: "text", text: formatted }] };
176
187
  });
177
- server.tool("journey_remove_node", "Remove a node from a journey by name. All transitions to/from this node are automatically deleted (cascade). Users currently at this node are exited. This is permanent.", {
188
+ server.tool("journey_node_remove", "Remove a node from a journey by name. All transitions to/from this node are automatically deleted (cascade). Users currently at this node are exited. This is permanent.", {
178
189
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
179
190
  nodeName: z.string().describe("Name of the node to remove"),
180
191
  }, async (params) => {
@@ -183,7 +194,7 @@ export function registerJourneyTools(server, client) {
183
194
  return { content: [{ type: "text", text: formatted }] };
184
195
  });
185
196
  // --- Granular transition tools ---
186
- server.tool("journey_list_transitions", "List all transitions in a journey. Shows 'from → to' with trigger type (on event / after delay). Filterable by source or target node.", {
197
+ server.tool("journey_transition_list", "List all transitions in a journey. Shows 'from → to' with trigger type (on event / after delay). Filterable by source or target node.", {
187
198
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
188
199
  from_node: z.string().optional().describe("Filter by source node name"),
189
200
  to_node: z.string().optional().describe("Filter by target node name"),
@@ -193,7 +204,7 @@ export function registerJourneyTools(server, client) {
193
204
  const formatted = formatTransitionList(data);
194
205
  return { content: [{ type: "text", text: formatted }] };
195
206
  });
196
- server.tool("journey_add_transition", "Add a transition between two nodes. Use 'on' for event triggers (e.g. on: 'user.login') or 'after' for delays (e.g. after: '3 day', '12 hr', '30 m', '2 week'). Bare numbers default to minutes. Also accepts the structured 'trigger' object. Transition name is auto-generated from '{from}-to-{to}' if not provided.", {
207
+ server.tool("journey_transition_add", "Add a transition between two nodes. Use 'on' for event triggers (e.g. on: 'user.login') or 'after' for delays (e.g. after: '3 day', '12 hr', '30 m', '2 week'). Bare numbers default to minutes. Also accepts the structured 'trigger' object. Transition name is auto-generated from '{from}-to-{to}' if not provided.", {
197
208
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
198
209
  from: z.string().describe("Source node name"),
199
210
  to: z.string().describe("Target node name"),
@@ -248,7 +259,7 @@ export function registerJourneyTools(server, client) {
248
259
  const formatted = formatSingleTransition(data, "Added", transitionName);
249
260
  return { content: [{ type: "text", text: formatted }] };
250
261
  });
251
- server.tool("journey_edit_transition", "Update a transition's trigger or priority by name. Use 'on' for event triggers or 'after' for delays (e.g. '3 day', '12 hr', '30 m'). Also accepts the structured 'trigger' object. Use journey_list_transitions to find transition names.", {
262
+ server.tool("journey_transition_update", "Update a transition's trigger or priority by name. Use 'on' for event triggers or 'after' for delays (e.g. '3 day', '12 hr', '30 m'). Also accepts the structured 'trigger' object. Use journey_transition_list to find transition names.", {
252
263
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
253
264
  transitionName: z.string().describe("Name of the transition to edit"),
254
265
  on: z
@@ -291,7 +302,7 @@ export function registerJourneyTools(server, client) {
291
302
  const formatted = formatSingleTransition(data, "Updated", transitionName);
292
303
  return { content: [{ type: "text", text: formatted }] };
293
304
  });
294
- server.tool("journey_remove_transition", "Remove a transition from a journey by name. This is permanent. Use journey_list_transitions to find transition names.", {
305
+ server.tool("journey_transition_remove", "Remove a transition from a journey by name. This is permanent. Use journey_transition_list to find transition names.", {
295
306
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
296
307
  transitionName: z.string().describe("Name of the transition to remove"),
297
308
  }, async (params) => {