@ascendkit/cli 0.2.6 → 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.
@@ -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
@@ -13,20 +13,48 @@ import { registerWebhookTools } from "./tools/webhooks.js";
13
13
  import { registerCampaignTools } from "./tools/campaigns.js";
14
14
  import { registerImportTools } from "./tools/import.js";
15
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";
16
19
  const client = new AscendKitClient({
17
20
  apiUrl: process.env.ASCENDKIT_API_URL ?? DEFAULT_API_URL,
18
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
+ }
19
30
  const server = new McpServer({
20
31
  name: "ascendkit",
21
- version: "0.1.0",
32
+ version: CLI_VERSION,
22
33
  });
23
- 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.", {
24
35
  publicKey: z.string().describe("Your project's public key (pk_dev_..., pk_beta_..., or pk_prod_...)"),
25
36
  apiUrl: z.string().optional().describe("AscendKit API URL (default: https://api.ascendkit.dev)"),
26
37
  }, async (params) => {
27
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
+ }
28
56
  return {
29
- content: [{ type: "text", text: `Configured for project ${params.publicKey}` }],
57
+ content: [{ type: "text", text: `Configured for environment ${params.publicKey}` }],
30
58
  };
31
59
  });
32
60
  registerPlatformTools(server, client);
@@ -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,17 +39,24 @@ 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 for custom credentials, or useProxy to revert to AscendKit managed credentials. Omit all to get a portal URL for browser-based entry.", {
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)"),
@@ -87,23 +108,23 @@ export function registerAuthTools(server, client) {
87
108
  }],
88
109
  };
89
110
  });
90
- 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 () => {
91
112
  const data = await auth.listUsers(client);
92
113
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
93
114
  });
94
- server.tool("auth_delete_user", "Deactivate a user (soft delete). The user record is preserved but marked inactive.", {
115
+ server.tool("auth_user_remove", "Deactivate a user (soft delete). The user record is preserved but marked inactive.", {
95
116
  userId: z.string().describe("User ID (usr_ prefixed)"),
96
117
  }, async (params) => {
97
118
  const data = await auth.deleteUser(client, params.userId);
98
119
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
99
120
  });
100
- server.tool("auth_bulk_delete_users", "Bulk deactivate users (soft delete). All specified users are marked inactive.", {
121
+ server.tool("auth_user_bulk_remove", "Bulk deactivate users (soft delete). All specified users are marked inactive.", {
101
122
  userIds: z.array(z.string()).min(1).max(100).describe("Array of user IDs (usr_ prefixed)"),
102
123
  }, async (params) => {
103
124
  const data = await auth.bulkDeleteUsers(client, params.userIds);
104
125
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
105
126
  });
106
- server.tool("auth_reactivate_user", "Reactivate a previously deactivated user.", {
127
+ server.tool("auth_user_reactivate", "Reactivate a previously deactivated user.", {
107
128
  userId: z.string().describe("User ID (usr_ prefixed)"),
108
129
  }, async (params) => {
109
130
  const data = await auth.reactivateUser(client, params.userId);
@@ -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
  }
@@ -1,7 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import * as importCmd from "../commands/import.js";
3
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.", {
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"),
5
9
  clerkApiKey: z
6
10
  .string()
7
11
  .describe("Clerk secret API key (sk_live_... or sk_test_...)"),
@@ -9,10 +13,6 @@ export function registerImportTools(server, client) {
9
13
  .string()
10
14
  .optional()
11
15
  .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
16
  importUsers: z
17
17
  .boolean()
18
18
  .optional()
@@ -23,7 +23,7 @@ export function registerImportTools(server, client) {
23
23
  .describe("Import auth settings — OAuth provider config (default: true)"),
24
24
  }, async (params) => {
25
25
  try {
26
- const dryRun = !(params.execute ?? false);
26
+ const dryRun = params.mode !== "run";
27
27
  const shouldImportUsers = params.importUsers ?? true;
28
28
  const shouldImportSettings = params.importSettings ?? true;
29
29
  const rawUsers = await importCmd.fetchClerkUsers(params.clerkApiKey, params.clerkInstanceUrl);
@@ -87,10 +87,10 @@ export function registerImportTools(server, client) {
87
87
  summary.warnings = allWarnings;
88
88
  let text = JSON.stringify(summary, null, 2);
89
89
  if (dryRun) {
90
- text += "\n\nThis was a dry run. Pass execute=true to apply changes.";
90
+ text += "\n\nThis was a dry run. Use mode='run' to apply changes.";
91
91
  }
92
92
  else if (totalImported > 0) {
93
- text += "\n\nTo set up migration emails, use the journey_create_migration tool.";
93
+ text += "\n\nTo set up migration emails, use the import_migration_journey_create tool.";
94
94
  }
95
95
  return { content: [{ type: "text", text }] };
96
96
  }
@@ -104,7 +104,7 @@ export function registerImportTools(server, client) {
104
104
  };
105
105
  }
106
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.", {
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
108
  fromIdentityEmail: z
109
109
  .string()
110
110
  .optional()
@@ -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?, variables?}) 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()
@@ -153,7 +160,7 @@ export function registerJourneyTools(server, client) {
153
160
  const formatted = formatSingleNode(data, "Added", params.name);
154
161
  return { content: [{ type: "text", text: formatted }] };
155
162
  });
156
- 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).", {
157
164
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
158
165
  nodeName: z.string().describe("Name of the node to edit"),
159
166
  action: z
@@ -178,7 +185,7 @@ export function registerJourneyTools(server, client) {
178
185
  const formatted = formatSingleNode(data, "Updated", nodeName);
179
186
  return { content: [{ type: "text", text: formatted }] };
180
187
  });
181
- 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.", {
182
189
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
183
190
  nodeName: z.string().describe("Name of the node to remove"),
184
191
  }, async (params) => {
@@ -187,7 +194,7 @@ export function registerJourneyTools(server, client) {
187
194
  return { content: [{ type: "text", text: formatted }] };
188
195
  });
189
196
  // --- Granular transition tools ---
190
- 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.", {
191
198
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
192
199
  from_node: z.string().optional().describe("Filter by source node name"),
193
200
  to_node: z.string().optional().describe("Filter by target node name"),
@@ -197,7 +204,7 @@ export function registerJourneyTools(server, client) {
197
204
  const formatted = formatTransitionList(data);
198
205
  return { content: [{ type: "text", text: formatted }] };
199
206
  });
200
- 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.", {
201
208
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
202
209
  from: z.string().describe("Source node name"),
203
210
  to: z.string().describe("Target node name"),
@@ -252,7 +259,7 @@ export function registerJourneyTools(server, client) {
252
259
  const formatted = formatSingleTransition(data, "Added", transitionName);
253
260
  return { content: [{ type: "text", text: formatted }] };
254
261
  });
255
- 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.", {
256
263
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
257
264
  transitionName: z.string().describe("Name of the transition to edit"),
258
265
  on: z
@@ -295,7 +302,7 @@ export function registerJourneyTools(server, client) {
295
302
  const formatted = formatSingleTransition(data, "Updated", transitionName);
296
303
  return { content: [{ type: "text", text: formatted }] };
297
304
  });
298
- 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.", {
299
306
  journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
300
307
  transitionName: z.string().describe("Name of the transition to remove"),
301
308
  }, async (params) => {