@ascendkit/cli 0.1.10

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/dist/api/client.d.ts +34 -0
  3. package/dist/api/client.js +155 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +1153 -0
  6. package/dist/commands/auth.d.ts +17 -0
  7. package/dist/commands/auth.js +29 -0
  8. package/dist/commands/content.d.ts +25 -0
  9. package/dist/commands/content.js +28 -0
  10. package/dist/commands/email.d.ts +37 -0
  11. package/dist/commands/email.js +39 -0
  12. package/dist/commands/journeys.d.ts +86 -0
  13. package/dist/commands/journeys.js +69 -0
  14. package/dist/commands/platform.d.ts +35 -0
  15. package/dist/commands/platform.js +517 -0
  16. package/dist/commands/surveys.d.ts +51 -0
  17. package/dist/commands/surveys.js +41 -0
  18. package/dist/commands/webhooks.d.ts +16 -0
  19. package/dist/commands/webhooks.js +28 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +29 -0
  22. package/dist/mcp.d.ts +2 -0
  23. package/dist/mcp.js +40 -0
  24. package/dist/tools/auth.d.ts +3 -0
  25. package/dist/tools/auth.js +75 -0
  26. package/dist/tools/content.d.ts +3 -0
  27. package/dist/tools/content.js +64 -0
  28. package/dist/tools/email.d.ts +3 -0
  29. package/dist/tools/email.js +57 -0
  30. package/dist/tools/journeys.d.ts +3 -0
  31. package/dist/tools/journeys.js +302 -0
  32. package/dist/tools/platform.d.ts +3 -0
  33. package/dist/tools/platform.js +63 -0
  34. package/dist/tools/surveys.d.ts +3 -0
  35. package/dist/tools/surveys.js +212 -0
  36. package/dist/tools/webhooks.d.ts +3 -0
  37. package/dist/tools/webhooks.js +56 -0
  38. package/dist/types.d.ts +96 -0
  39. package/dist/types.js +4 -0
  40. package/dist/utils/credentials.d.ts +27 -0
  41. package/dist/utils/credentials.js +90 -0
  42. package/dist/utils/duration.d.ts +16 -0
  43. package/dist/utils/duration.js +47 -0
  44. package/dist/utils/journey-format.d.ts +112 -0
  45. package/dist/utils/journey-format.js +200 -0
  46. package/dist/utils/survey-format.d.ts +60 -0
  47. package/dist/utils/survey-format.js +164 -0
  48. package/package.json +37 -0
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { AscendKitClient } from "./api/client.js";
5
+ import { registerAuthTools } from "./tools/auth.js";
6
+ import { registerContentTools } from "./tools/content.js";
7
+ import { registerSurveyTools } from "./tools/surveys.js";
8
+ const args = process.argv.slice(2);
9
+ const publicKey = args[0];
10
+ const apiUrl = args[1] ?? process.env.ASCENDKIT_API_URL ?? "http://localhost:8000";
11
+ if (!publicKey) {
12
+ console.error("Usage: ascendkit-mcp <public-key> [api-url]");
13
+ console.error(" public-key Your project's public key (pk_live_...)");
14
+ console.error(" api-url AscendKit API URL (default: http://localhost:8000)");
15
+ process.exit(1);
16
+ }
17
+ const client = new AscendKitClient({ apiUrl, publicKey });
18
+ const server = new McpServer({
19
+ name: "ascendkit",
20
+ version: "0.1.0",
21
+ });
22
+ registerAuthTools(server, client);
23
+ registerContentTools(server, client);
24
+ registerSurveyTools(server, client);
25
+ async function main() {
26
+ const transport = new StdioServerTransport();
27
+ await server.connect(transport);
28
+ }
29
+ main().catch(console.error);
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/mcp.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { AscendKitClient } from "./api/client.js";
6
+ import { registerAuthTools } from "./tools/auth.js";
7
+ import { registerContentTools } from "./tools/content.js";
8
+ import { registerSurveyTools } from "./tools/surveys.js";
9
+ import { registerPlatformTools } from "./tools/platform.js";
10
+ import { registerEmailTools } from "./tools/email.js";
11
+ import { registerJourneyTools } from "./tools/journeys.js";
12
+ import { registerWebhookTools } from "./tools/webhooks.js";
13
+ const client = new AscendKitClient({
14
+ apiUrl: process.env.ASCENDKIT_API_URL ?? "https://api.ascendkit.com",
15
+ });
16
+ const server = new McpServer({
17
+ name: "ascendkit",
18
+ version: "0.1.0",
19
+ });
20
+ server.tool("ascendkit_configure", "Configure AscendKit with your project's public key. Must be called before using any other AscendKit tools.", {
21
+ publicKey: z.string().describe("Your project's public key (pk_dev_..., pk_beta_..., or pk_prod_...)"),
22
+ apiUrl: z.string().optional().describe("AscendKit API URL (default: https://api.ascendkit.com)"),
23
+ }, async (params) => {
24
+ client.configure(params.publicKey, params.apiUrl);
25
+ return {
26
+ content: [{ type: "text", text: `Configured for project ${params.publicKey}` }],
27
+ };
28
+ });
29
+ registerPlatformTools(server, client);
30
+ registerAuthTools(server, client);
31
+ registerContentTools(server, client);
32
+ registerSurveyTools(server, client);
33
+ registerEmailTools(server, client);
34
+ registerJourneyTools(server, client);
35
+ registerWebhookTools(server, client);
36
+ async function main() {
37
+ const transport = new StdioServerTransport();
38
+ await server.connect(transport);
39
+ }
40
+ main().catch(console.error);
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AscendKitClient } from "../api/client.js";
3
+ export declare function registerAuthTools(server: McpServer, client: AscendKitClient): void;
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ import * as auth from "../commands/auth.js";
3
+ export function registerAuthTools(server, client) {
4
+ server.tool("auth_get_settings", "Get authentication settings for the current project (enabled providers, features, OAuth configs)", {}, async () => {
5
+ const data = await auth.getSettings(client);
6
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
7
+ });
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.", {
9
+ providers: z
10
+ .array(z.string())
11
+ .optional()
12
+ .describe('Enabled auth providers, e.g. ["credentials", "google", "github"]'),
13
+ features: z
14
+ .object({
15
+ emailVerification: z.boolean().optional(),
16
+ waitlist: z.boolean().optional(),
17
+ passwordReset: z.boolean().optional(),
18
+ requireUsername: z.boolean().optional(),
19
+ })
20
+ .optional()
21
+ .describe("Feature toggles for auth"),
22
+ sessionDuration: z
23
+ .string()
24
+ .optional()
25
+ .describe('Session duration, e.g. "7d", "24h"'),
26
+ }, async (params) => {
27
+ const data = await auth.updateSettings(client, params);
28
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
29
+ });
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.", {
31
+ providers: z
32
+ .array(z.string())
33
+ .describe('List of provider names, e.g. ["credentials", "google", "github"]'),
34
+ }, async (params) => {
35
+ const data = await auth.updateProviders(client, params.providers);
36
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
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.", {
39
+ provider: z.string().describe('OAuth provider name, e.g. "google", "github"'),
40
+ clientId: z.string().optional().describe("OAuth client ID (to set credentials directly)"),
41
+ clientSecret: z.string().optional().describe("OAuth client secret (to set credentials directly)"),
42
+ callbackUrl: z.string().optional().describe("Auth callback URL for this provider"),
43
+ }, async (params) => {
44
+ if ((params.clientId && !params.clientSecret) || (!params.clientId && params.clientSecret)) {
45
+ return {
46
+ content: [{
47
+ type: "text",
48
+ text: "Both clientId and clientSecret are required to set credentials directly.",
49
+ }],
50
+ isError: true,
51
+ };
52
+ }
53
+ if (params.clientId && params.clientSecret) {
54
+ const data = await auth.updateOAuthCredentials(client, params.provider, params.clientId, params.clientSecret, params.callbackUrl);
55
+ return {
56
+ content: [{
57
+ type: "text",
58
+ text: `OAuth credentials saved for ${params.provider}.\n\n${JSON.stringify(data, null, 2)}`,
59
+ }],
60
+ };
61
+ }
62
+ const portalUrl = process.env.ASCENDKIT_PORTAL_URL ?? "http://localhost:3000";
63
+ const url = auth.getOAuthSetupUrl(portalUrl, params.provider, client.currentPublicKey ?? undefined);
64
+ return {
65
+ content: [{
66
+ type: "text",
67
+ text: `Open this URL in your browser to configure ${params.provider} OAuth credentials:\n\n${url}\n\nFor sensitive environments, enter credentials through the browser portal.`,
68
+ }],
69
+ };
70
+ });
71
+ server.tool("auth_list_users", "List all project users (end-users who signed up via the SDK)", {}, async () => {
72
+ const data = await auth.listUsers(client);
73
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
74
+ });
75
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AscendKitClient } from "../api/client.js";
3
+ export declare function registerContentTools(server: McpServer, client: AscendKitClient): void;
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ import * as content from "../commands/content.js";
3
+ 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.", {
5
+ name: z.string().describe("Template name, e.g. 'welcome-email'"),
6
+ subject: z.string().describe("Email subject line"),
7
+ bodyHtml: z.string().describe("HTML email body. Use {{variable}} for placeholders"),
8
+ bodyText: z.string().describe("Plain-text email body. Use {{variable}} for placeholders"),
9
+ slug: z.string().optional().describe("URL-safe identifier for programmatic lookup, e.g. 'welcome-email'"),
10
+ description: z.string().optional().describe("Template description"),
11
+ }, async (params) => {
12
+ const data = await content.createTemplate(client, params);
13
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
14
+ });
15
+ server.tool("content_list_templates", "List all content templates for the current project", {
16
+ query: z.string().optional().describe("Search templates by name, slug, or description"),
17
+ isSystem: z.boolean().optional().describe("Filter: true for system templates, false for custom"),
18
+ }, async (params) => {
19
+ const listParams = (params.query || params.isSystem !== undefined)
20
+ ? { query: params.query, isSystem: params.isSystem }
21
+ : undefined;
22
+ const data = await content.listTemplates(client, listParams);
23
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
24
+ });
25
+ server.tool("content_get_template", "Get a content template by ID, including current version body and variables", {
26
+ templateId: z.string().describe("Template ID (tpl_ prefixed)"),
27
+ }, async (params) => {
28
+ const data = await content.getTemplate(client, params.templateId);
29
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30
+ });
31
+ server.tool("content_update_template", "Update a content template. Creates a new immutable version — previous versions are preserved for result tracking.", {
32
+ templateId: z.string().describe("Template ID (tpl_ prefixed)"),
33
+ subject: z.string().optional().describe("New email subject line"),
34
+ bodyHtml: z.string().optional().describe("New HTML email body"),
35
+ bodyText: z.string().optional().describe("New plain-text email body"),
36
+ changeNote: z
37
+ .string()
38
+ .optional()
39
+ .describe("Note describing what changed in this version"),
40
+ }, async (params) => {
41
+ const { templateId, ...rest } = params;
42
+ const data = await content.updateTemplate(client, templateId, rest);
43
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
44
+ });
45
+ server.tool("content_delete_template", "Delete a content template and all its versions", {
46
+ templateId: z.string().describe("Template ID (tpl_ prefixed)"),
47
+ }, async (params) => {
48
+ const data = await content.deleteTemplate(client, params.templateId);
49
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
50
+ });
51
+ server.tool("content_list_versions", "List all versions of a content template (newest first)", {
52
+ templateId: z.string().describe("Template ID (tpl_ prefixed)"),
53
+ }, async (params) => {
54
+ const data = await content.listVersions(client, params.templateId);
55
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
56
+ });
57
+ server.tool("content_get_version", "Get a specific version of a content template", {
58
+ templateId: z.string().describe("Template ID (tpl_ prefixed)"),
59
+ versionNumber: z.number().describe("Version number to retrieve"),
60
+ }, async (params) => {
61
+ const data = await content.getVersion(client, params.templateId, params.versionNumber);
62
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
63
+ });
64
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AscendKitClient } from "../api/client.js";
3
+ export declare function registerEmailTools(server: McpServer, client: AscendKitClient): void;
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ import * as email from "../commands/email.js";
3
+ export function registerEmailTools(server, client) {
4
+ server.tool("email_get_settings", "Get email settings (sender address, domain, verification status)", {}, async () => {
5
+ const data = await email.getSettings(client);
6
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
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.", {
16
+ 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
+ }, async (params) => {
22
+ const data = await email.setupDomain(client, params.domain);
23
+ // Manual DNS setup — show records for copy-paste
24
+ const records = data.dnsRecords ?? [];
25
+ const formatted = records.map((r) => ` ${r.type}\t${r.name}\t→\t${r.value}`).join("\n");
26
+ const provider = data.dnsProvider;
27
+ const providerLine = provider?.name
28
+ ? `Detected provider: ${provider.name} (${provider.confidence ?? "unknown"})\n`
29
+ : "";
30
+ const portalLine = provider?.portalUrl
31
+ ? `Provider console: ${provider.portalUrl}\n`
32
+ : "";
33
+ const guidedLine = provider?.assistantSetupUrl
34
+ ? `Guided setup: ${provider.assistantSetupUrl}\n`
35
+ : "";
36
+ return {
37
+ content: [{
38
+ 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.`,
40
+ }],
41
+ };
42
+ });
43
+ server.tool("email_get_dns_provider", "Detect DNS provider for a domain and return provider dashboard links", {
44
+ domain: z.string().optional().describe("Domain to detect; defaults to configured email domain"),
45
+ }, async (params) => {
46
+ const data = await email.getDnsProvider(client, params.domain);
47
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
48
+ });
49
+ server.tool("email_check_domain_status", "Poll SES verification status for your configured domain", {}, async () => {
50
+ const data = await email.checkDomainStatus(client);
51
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
52
+ });
53
+ server.tool("email_remove_domain", "Remove domain and reset to default AscendKit sender", {}, async () => {
54
+ const data = await email.removeDomain(client);
55
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
56
+ });
57
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AscendKitClient } from "../api/client.js";
3
+ export declare function registerJourneyTools(server: McpServer, client: AscendKitClient): void;
@@ -0,0 +1,302 @@
1
+ import { z } from "zod";
2
+ import * as journeys from "../commands/journeys.js";
3
+ import { parseDelay } from "../utils/duration.js";
4
+ import { formatJourneyAnalytics, formatJourneyList, formatJourneyWithGuidance, formatNodeList, formatSingleNode, formatTransitionList, formatSingleTransition, } from "../utils/journey-format.js";
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.", {
7
+ name: z.string().describe("Journey name, e.g. 'Onboarding Flow'"),
8
+ entryEvent: z
9
+ .string()
10
+ .describe("Event that triggers enrollment, e.g. 'user.created'"),
11
+ entryNode: z
12
+ .string()
13
+ .describe("Name of the first node users enter"),
14
+ nodes: z
15
+ .record(z.record(z.unknown()))
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."),
18
+ transitions: z
19
+ .array(z.record(z.unknown()))
20
+ .optional()
21
+ .describe("List of transitions. Each has 'from', 'to', 'trigger' ({type: 'event'|'timer', event?, delay?}), optional 'priority'"),
22
+ description: z.string().optional().describe("Journey description"),
23
+ entryConditions: z
24
+ .record(z.unknown())
25
+ .optional()
26
+ .describe("Filter on the entry event payload, e.g. {provider: 'credentials'}"),
27
+ reEntryPolicy: z
28
+ .enum(["skip", "restart"])
29
+ .optional()
30
+ .describe("What happens if a user triggers the entry event again: 'skip' (ignore) or 'restart' (re-enroll)"),
31
+ }, async (params) => {
32
+ const data = await journeys.createJourney(client, params);
33
+ const formatted = formatJourneyWithGuidance(data);
34
+ return { content: [{ type: "text", text: formatted }] };
35
+ });
36
+ server.tool("journey_list", "List all journeys for the current project. Optionally filter by status (draft, active, paused, archived).", {
37
+ status: z
38
+ .enum(["draft", "active", "paused", "archived"])
39
+ .optional()
40
+ .describe("Filter by journey status"),
41
+ limit: z.number().optional().describe("Max results (default 50)"),
42
+ offset: z.number().optional().describe("Pagination offset"),
43
+ }, async (params) => {
44
+ const data = await journeys.listJourneys(client, params);
45
+ const formatted = formatJourneyList(data);
46
+ return { content: [{ type: "text", text: formatted }] };
47
+ });
48
+ server.tool("journey_get", "Get a journey by ID with full definition (nodes, transitions) and current stats (enrolled, active, completed).", {
49
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
50
+ }, async (params) => {
51
+ const data = await journeys.getJourney(client, params.journeyId);
52
+ const formatted = formatJourneyWithGuidance(data);
53
+ return { content: [{ type: "text", text: formatted }] };
54
+ });
55
+ server.tool("journey_update", "Update a journey definition. Only provided fields are changed. Re-validates the full definition after merge. Cannot update archived journeys.", {
56
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
57
+ name: z.string().optional().describe("New journey name"),
58
+ description: z.string().optional().describe("New description"),
59
+ entryEvent: z.string().optional().describe("New entry event"),
60
+ entryNode: z.string().optional().describe("New entry node name"),
61
+ entryConditions: z
62
+ .record(z.unknown())
63
+ .optional()
64
+ .describe("New entry conditions filter"),
65
+ reEntryPolicy: z
66
+ .enum(["skip", "restart"])
67
+ .optional()
68
+ .describe("New re-entry policy"),
69
+ nodes: z
70
+ .record(z.record(z.unknown()))
71
+ .optional()
72
+ .describe("Updated node definitions"),
73
+ transitions: z
74
+ .array(z.record(z.unknown()))
75
+ .optional()
76
+ .describe("Updated transitions"),
77
+ }, async (params) => {
78
+ const { journeyId, ...rest } = params;
79
+ const data = await journeys.updateJourney(client, journeyId, rest);
80
+ const formatted = formatJourneyWithGuidance(data);
81
+ return { content: [{ type: "text", text: formatted }] };
82
+ });
83
+ server.tool("journey_delete", "Delete a journey and all associated user states. This is permanent.", {
84
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
85
+ }, async (params) => {
86
+ await journeys.deleteJourney(client, params.journeyId);
87
+ return {
88
+ content: [{ type: "text", text: `Deleted journey ${params.journeyId}.` }],
89
+ };
90
+ });
91
+ // --- Lifecycle tools ---
92
+ server.tool("journey_activate", "Activate a journey (draft → active). Once active, users are automatically enrolled when the entry event fires.", {
93
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
94
+ }, async (params) => {
95
+ const data = await journeys.activateJourney(client, params.journeyId);
96
+ const formatted = formatJourneyWithGuidance(data);
97
+ return { content: [{ type: "text", text: formatted }] };
98
+ });
99
+ 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
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
101
+ }, async (params) => {
102
+ const data = await journeys.pauseJourney(client, params.journeyId);
103
+ const formatted = formatJourneyWithGuidance(data);
104
+ return { content: [{ type: "text", text: formatted }] };
105
+ });
106
+ server.tool("journey_archive", "Archive a journey. All active users are exited. No further enrollment or transitions. This cannot be undone.", {
107
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
108
+ }, async (params) => {
109
+ const data = await journeys.archiveJourney(client, params.journeyId);
110
+ const formatted = formatJourneyWithGuidance(data);
111
+ return { content: [{ type: "text", text: formatted }] };
112
+ });
113
+ // --- Analytics tools ---
114
+ server.tool("journey_analytics", "Get journey analytics: user counts per node, conversion rates per transition, time-in-node metrics, terminal distribution, and totals (enrolled/active/completed/failed).", {
115
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
116
+ }, async (params) => {
117
+ const data = await journeys.getJourneyAnalytics(client, params.journeyId);
118
+ const formatted = formatJourneyAnalytics(data);
119
+ return { content: [{ type: "text", text: formatted }] };
120
+ });
121
+ // --- 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.", {
123
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
124
+ }, async (params) => {
125
+ const data = await journeys.listNodes(client, params.journeyId);
126
+ const formatted = formatNodeList(data);
127
+ return { content: [{ type: "text", text: formatted }] };
128
+ });
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.", {
130
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
131
+ name: z
132
+ .string()
133
+ .describe("Node name (stable identifier, e.g. 'welcome_email', 'nudge_1')"),
134
+ action: z
135
+ .object({
136
+ type: z.enum(["send_email", "tag_user", "advance_stage", "none"]).describe("Action type"),
137
+ templateSlug: z.string().optional().describe("Email template slug (for send_email)"),
138
+ surveySlug: z.string().optional().describe("Survey slug to include in email (for send_email)"),
139
+ tagName: z.string().optional().describe("Tag to add (for tag_user)"),
140
+ stageName: z.string().optional().describe("Stage to set (for advance_stage)"),
141
+ })
142
+ .optional()
143
+ .describe("Action to execute when a user enters this node. Defaults to {type: 'none'}."),
144
+ terminal: z
145
+ .boolean()
146
+ .default(false)
147
+ .describe("Whether this is a terminal node (journey ends here for the user)"),
148
+ }, async (params) => {
149
+ const { journeyId, ...rest } = params;
150
+ const data = await journeys.addNode(client, journeyId, rest);
151
+ const formatted = formatSingleNode(data, "Added", params.name);
152
+ return { content: [{ type: "text", text: formatted }] };
153
+ });
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).", {
155
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
156
+ nodeName: z.string().describe("Name of the node to edit"),
157
+ action: z
158
+ .object({
159
+ type: z.enum(["send_email", "tag_user", "advance_stage", "none"]).describe("Action type"),
160
+ templateSlug: z.string().optional().describe("Email template slug"),
161
+ surveySlug: z.string().optional().describe("Survey slug to include in email"),
162
+ tagName: z.string().optional().describe("Tag to add"),
163
+ stageName: z.string().optional().describe("Stage to set"),
164
+ })
165
+ .optional()
166
+ .describe("New action definition (replaces the entire action)"),
167
+ terminal: z
168
+ .boolean()
169
+ .optional()
170
+ .describe("Whether this is a terminal node"),
171
+ }, async (params) => {
172
+ const { journeyId, nodeName, ...rest } = params;
173
+ const data = await journeys.editNode(client, journeyId, nodeName, rest);
174
+ const formatted = formatSingleNode(data, "Updated", nodeName);
175
+ return { content: [{ type: "text", text: formatted }] };
176
+ });
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.", {
178
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
179
+ nodeName: z.string().describe("Name of the node to remove"),
180
+ }, async (params) => {
181
+ const data = await journeys.removeNode(client, params.journeyId, params.nodeName);
182
+ const formatted = formatSingleNode(data, "Removed", params.nodeName);
183
+ return { content: [{ type: "text", text: formatted }] };
184
+ });
185
+ // --- 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.", {
187
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
188
+ from_node: z.string().optional().describe("Filter by source node name"),
189
+ to_node: z.string().optional().describe("Filter by target node name"),
190
+ }, async (params) => {
191
+ const { journeyId, ...opts } = params;
192
+ const data = await journeys.listTransitions(client, journeyId, opts);
193
+ const formatted = formatTransitionList(data);
194
+ return { content: [{ type: "text", text: formatted }] };
195
+ });
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.", {
197
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
198
+ from: z.string().describe("Source node name"),
199
+ to: z.string().describe("Target node name"),
200
+ on: z
201
+ .string()
202
+ .optional()
203
+ .describe("Event that triggers this transition, e.g. 'user.login'. Shorthand for trigger: {type: 'event', event: ...}"),
204
+ after: z
205
+ .string()
206
+ .optional()
207
+ .describe("Delay before transition fires, e.g. '3 day', '12 hr', '30 m', '2 week'. Bare number = minutes. Shorthand for trigger: {type: 'timer', delay: ...}"),
208
+ trigger: z
209
+ .object({
210
+ type: z.enum(["event", "timer"]).describe("Trigger type: 'event' for on-event, 'timer' for after-delay"),
211
+ event: z.string().optional().describe("Event name (for type 'event'), e.g. 'user.login'"),
212
+ delay: z.string().optional().describe("Delay duration (for type 'timer'), e.g. '3d', '12h'"),
213
+ })
214
+ .optional()
215
+ .describe("Structured trigger object (alternative to on/after shorthand)"),
216
+ priority: z
217
+ .number()
218
+ .optional()
219
+ .describe("Priority for ambiguous transitions (lower = higher priority)"),
220
+ name: z
221
+ .string()
222
+ .optional()
223
+ .describe("Transition name (auto-generated from '{from}-to-{to}' if omitted)"),
224
+ }, async (params) => {
225
+ const { journeyId, on: onEvent, after, trigger: rawTrigger, ...rest } = params;
226
+ // Resolve trigger: on/after shorthand takes precedence over structured trigger
227
+ let trigger;
228
+ if (onEvent) {
229
+ trigger = { type: "event", event: onEvent };
230
+ }
231
+ else if (after) {
232
+ trigger = { type: "timer", delay: parseDelay(after) };
233
+ }
234
+ else if (rawTrigger) {
235
+ trigger = rawTrigger;
236
+ if (trigger.type === "timer" && trigger.delay) {
237
+ trigger.delay = parseDelay(trigger.delay);
238
+ }
239
+ }
240
+ else {
241
+ return {
242
+ content: [{ type: "text", text: "Error: Provide 'on' (event name), 'after' (delay), or 'trigger' object." }],
243
+ isError: true,
244
+ };
245
+ }
246
+ const data = await journeys.addTransition(client, journeyId, { ...rest, trigger });
247
+ const transitionName = rest.name || `${rest.from}-to-${rest.to}`;
248
+ const formatted = formatSingleTransition(data, "Added", transitionName);
249
+ return { content: [{ type: "text", text: formatted }] };
250
+ });
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.", {
252
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
253
+ transitionName: z.string().describe("Name of the transition to edit"),
254
+ on: z
255
+ .string()
256
+ .optional()
257
+ .describe("New event trigger, e.g. 'user.login'. Shorthand for trigger: {type: 'event', event: ...}"),
258
+ after: z
259
+ .string()
260
+ .optional()
261
+ .describe("New delay, e.g. '3 day', '12 hr'. Shorthand for trigger: {type: 'timer', delay: ...}"),
262
+ trigger: z
263
+ .object({
264
+ type: z.enum(["event", "timer"]).describe("Trigger type"),
265
+ event: z.string().optional().describe("Event name"),
266
+ delay: z.string().optional().describe("Delay duration"),
267
+ })
268
+ .optional()
269
+ .describe("Structured trigger object (alternative to on/after shorthand)"),
270
+ priority: z
271
+ .number()
272
+ .optional()
273
+ .describe("New priority value"),
274
+ }, async (params) => {
275
+ const { journeyId, transitionName, on: onEvent, after, trigger: rawTrigger, ...rest } = params;
276
+ // Resolve trigger from on/after shorthand or structured trigger
277
+ let trigger;
278
+ if (onEvent) {
279
+ trigger = { type: "event", event: onEvent };
280
+ }
281
+ else if (after) {
282
+ trigger = { type: "timer", delay: parseDelay(after) };
283
+ }
284
+ else if (rawTrigger) {
285
+ trigger = rawTrigger;
286
+ if (trigger.type === "timer" && trigger.delay) {
287
+ trigger.delay = parseDelay(trigger.delay);
288
+ }
289
+ }
290
+ const data = await journeys.editTransition(client, journeyId, transitionName, { ...rest, trigger });
291
+ const formatted = formatSingleTransition(data, "Updated", transitionName);
292
+ return { content: [{ type: "text", text: formatted }] };
293
+ });
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.", {
295
+ journeyId: z.string().describe("Journey ID (jrn_ prefixed)"),
296
+ transitionName: z.string().describe("Name of the transition to remove"),
297
+ }, async (params) => {
298
+ const data = await journeys.removeTransition(client, params.journeyId, params.transitionName);
299
+ const formatted = formatSingleTransition(data, "Removed", params.transitionName);
300
+ return { content: [{ type: "text", text: formatted }] };
301
+ });
302
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AscendKitClient } from "../api/client.js";
3
+ export declare function registerPlatformTools(server: McpServer, client: AscendKitClient): void;