@apex-inc/mcp-server 0.26.0 → 0.27.1
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.
- package/dist/activation-wiring.d.ts +12 -0
- package/dist/activation-wiring.d.ts.map +1 -0
- package/dist/activation-wiring.js +18 -0
- package/dist/activation-wiring.js.map +1 -0
- package/dist/control-gate-guidance.d.ts +1 -0
- package/dist/control-gate-guidance.d.ts.map +1 -1
- package/dist/control-gate-guidance.js +13 -5
- package/dist/control-gate-guidance.js.map +1 -1
- package/dist/prompts.js +3 -3
- package/dist/publish-communication-copy.d.ts +11 -0
- package/dist/publish-communication-copy.d.ts.map +1 -0
- package/dist/publish-communication-copy.js +29 -0
- package/dist/publish-communication-copy.js.map +1 -0
- package/dist/tools.d.ts +30 -6
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +135 -45
- package/dist/tools.js.map +1 -1
- package/dist/workspace-target-url.d.ts +23 -0
- package/dist/workspace-target-url.d.ts.map +1 -0
- package/dist/workspace-target-url.js +62 -0
- package/dist/workspace-target-url.js.map +1 -0
- package/package.json +1 -1
- package/skills/apex-communications/SKILL.md +25 -21
- package/skills/apex-experimentation/SKILL.md +20 -6
- package/skills/apex-growth-tracking/SKILL.md +1 -1
- package/skills/apex-integration-cookbook/SKILL.md +1 -1
- package/skills/apex-spec/SKILL.md +6 -0
package/dist/tools.js
CHANGED
|
@@ -2,10 +2,13 @@ import { z } from "zod";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { apiGet, apiPost, apiPatch, apiPut, apiDelete, postWithIdempotency, setActiveWorkspace, getActiveWorkspace, setActiveOrg, getActiveOrg, getUserContext } from "./api-client.js";
|
|
5
|
+
import { collectHosts, evaluateTargetUrl } from "./workspace-target-url.js";
|
|
5
6
|
import { resolveExperimentHypothesis } from "./experiment-copy.js";
|
|
6
7
|
import { controlGateGuidanceFor } from "./control-gate-guidance.js";
|
|
7
8
|
import { getPersonLabel, lowerPerson } from "./person-label.js";
|
|
8
|
-
|
|
9
|
+
import { notStartedExplanation, publishedExperimentLine, } from "./publish-communication-copy.js";
|
|
10
|
+
import { activationLiveLine, shouldRefuseActivationWiring, } from "./activation-wiring.js";
|
|
11
|
+
const APEX = "Apex";
|
|
9
12
|
/**
|
|
10
13
|
* MOBX-006 — stable synthetic visitor id for agent-fired events.
|
|
11
14
|
*
|
|
@@ -55,6 +58,55 @@ function titleCaseEvent(event) {
|
|
|
55
58
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
56
59
|
.join(" ");
|
|
57
60
|
}
|
|
61
|
+
function activeWorkspaceSiteUrl() {
|
|
62
|
+
const ctx = getUserContext();
|
|
63
|
+
const key = getActiveWorkspace();
|
|
64
|
+
if (!ctx || !key)
|
|
65
|
+
return null;
|
|
66
|
+
for (const org of ctx.orgs) {
|
|
67
|
+
const match = org.workspaces.find((w) => w.workspaceKey === key);
|
|
68
|
+
if (match?.url)
|
|
69
|
+
return match.url;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
async function assertTargetUrlForActiveWorkspace(targetUrl) {
|
|
74
|
+
const namedSites = [];
|
|
75
|
+
let workspaceUrl = activeWorkspaceSiteUrl();
|
|
76
|
+
let catalogUnreadable = false;
|
|
77
|
+
try {
|
|
78
|
+
const env = await apiGet("/api/workspace/environments");
|
|
79
|
+
namedSites.push(...(env.data.production_hosts ?? []), ...(env.data.beta_hosts ?? []));
|
|
80
|
+
for (const row of env.data.known_hosts ?? []) {
|
|
81
|
+
namedSites.push(typeof row === "string" ? row : row.hostname ?? "");
|
|
82
|
+
}
|
|
83
|
+
if (env.data.workspace_url)
|
|
84
|
+
workspaceUrl = env.data.workspace_url;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
catalogUnreadable = true;
|
|
88
|
+
}
|
|
89
|
+
let analyticsHosts = [];
|
|
90
|
+
try {
|
|
91
|
+
const json = await apiGet("/api/analytics/hostnames?limit=100");
|
|
92
|
+
analyticsHosts = json.data ?? [];
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* analytics is a supplement, not the source of truth */
|
|
96
|
+
}
|
|
97
|
+
const allowed = collectHosts({
|
|
98
|
+
analyticsHosts,
|
|
99
|
+
namedSites,
|
|
100
|
+
workspaceUrl,
|
|
101
|
+
});
|
|
102
|
+
if (allowed.length === 0 && catalogUnreadable) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
message: "Could not load this workspace's sites (list_workspace_environments failed). Name the site with set_workspace_environments before setting target_url. Do not guess a brand domain or a sibling workspace's site.",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return evaluateTargetUrl(targetUrl, allowed);
|
|
109
|
+
}
|
|
58
110
|
/**
|
|
59
111
|
* Query string for the pack report windows: relative (`days` / `months`)
|
|
60
112
|
* or explicit `start` + `end` (YYYY-MM-DD, both required together —
|
|
@@ -81,28 +133,6 @@ function appUrl(path) {
|
|
|
81
133
|
const base = process.env.APEX_URL || process.env.APEX_API_URL || "http://localhost:3001";
|
|
82
134
|
return `${base.replace(/\/$/, "")}${path}`;
|
|
83
135
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Why publishing a comm with variants didn't start an experiment.
|
|
86
|
-
*
|
|
87
|
-
* Each of these is a missing precondition the user can actually fix, so the
|
|
88
|
-
* agent is told what to do rather than that "nothing happened."
|
|
89
|
-
*/
|
|
90
|
-
function notStartedExplanation(reason) {
|
|
91
|
-
switch (reason) {
|
|
92
|
-
case "no_host":
|
|
93
|
-
return "The variants were published but no experiment started: no published journey sends this communication, so there's no traffic to measure. Add it to a journey and publish that journey.";
|
|
94
|
-
case "no_goal":
|
|
95
|
-
return "The variants were published but no experiment started: the journey sending this communication has no goal event, so there's nothing to optimize toward. Set the journey's goal event first.";
|
|
96
|
-
case "ambiguous_host":
|
|
97
|
-
return "The variants were published but no experiment started: more than one published journey sends this communication. Re-issue publish_communication with host_journey_id.";
|
|
98
|
-
case "conflict":
|
|
99
|
-
return "The variants were published but no experiment started: another experiment already holds this communication. End it, then publish again.";
|
|
100
|
-
case "no_variants":
|
|
101
|
-
return "";
|
|
102
|
-
default:
|
|
103
|
-
return "The variants were published but the experiment could not be started. Check the communication in the dashboard.";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
136
|
function journeyLink(id) {
|
|
107
137
|
return appUrl(`/dashboard/communications/journeys/${id}`);
|
|
108
138
|
}
|
|
@@ -460,7 +490,7 @@ export const toolDefinitions = {
|
|
|
460
490
|
description: `${APEX} — Create an experiment record. Returns a structured recipe for the AI agent to implement. The agent makes the actual code changes using useApexVariant hook — Apex never generates code. Always call with preview=true first so the user sees a summary.`,
|
|
461
491
|
schema: z.object({
|
|
462
492
|
name: z.string().describe("Experiment name"),
|
|
463
|
-
targetUrl: z.string().describe("URL of the page to test (include #anchor for section targeting)"),
|
|
493
|
+
targetUrl: z.string().describe("URL of the page to test on the ACTIVE workspace (include #anchor for section targeting). Must be a host this workspace has actually sent events from — never guess a brand domain or a sibling workspace's site."),
|
|
464
494
|
controlContent: z.string().describe("The current content that the control shows"),
|
|
465
495
|
variantContent: z.string().describe("The new content for the variant"),
|
|
466
496
|
targetComponent: z.string().optional().describe("Component or file path where the change should be made"),
|
|
@@ -487,6 +517,13 @@ export const toolDefinitions = {
|
|
|
487
517
|
preview: z.boolean().optional().describe("If true, returns a preview without creating. Default: true."),
|
|
488
518
|
}),
|
|
489
519
|
handler: async (args) => {
|
|
520
|
+
const targetCheck = await assertTargetUrlForActiveWorkspace(args.targetUrl);
|
|
521
|
+
if (!targetCheck.ok) {
|
|
522
|
+
return {
|
|
523
|
+
content: [{ type: "text", text: `${APEX} ${targetCheck.message}` }],
|
|
524
|
+
isError: true,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
490
527
|
const split = args.trafficSplit ?? 50;
|
|
491
528
|
// MCP experiments are ALWAYS code-native (SDK). Client-side snippet /
|
|
492
529
|
// runtime-DOM experiments are intentionally NOT offered here (founder
|
|
@@ -811,12 +848,16 @@ export const toolDefinitions = {
|
|
|
811
848
|
`2. Install the hook package: npm i @apex-inc/react`,
|
|
812
849
|
`3. Add: import { useApexVariant } from "@apex-inc/react"`,
|
|
813
850
|
`4. In the component, add: const variant = useApexVariant("${exp.id}") // fires experiment_exposure automatically`,
|
|
851
|
+
` Next.js App Router: pass the request search string so the first HTML matches the preview URL: useApexVariant("${exp.id}", { search }). Without it the server renders control and the arm only appears after hydrate.`,
|
|
814
852
|
`5. Wrap the target content in a conditional (fall back to control on null/error):`,
|
|
815
853
|
` {variant === "variant_a" || variant === "variant_b" ? <VARIANT_CONTENT> : <CONTROL_CONTENT>}`,
|
|
816
|
-
`6. Screenshots for the dashboard:`,
|
|
817
|
-
` -
|
|
818
|
-
` -
|
|
819
|
-
`
|
|
854
|
+
`6. Screenshots for the dashboard — REQUIRED before you tell the user the draft is ready:`,
|
|
855
|
+
` - This is an SDK experiment. The variant exists only in local code until it ships.`,
|
|
856
|
+
` - Apex does NOT auto-capture a public URL on create (that page does not have the variant yet, and a guessed host shows the wrong site on both arms).`,
|
|
857
|
+
` - Capture BOTH arms on localhost with ?_apex_preview=control|variant_a&_apex_exp=${exp.id}, then call attach_experiment_asset({experimentId, variantKey, imageBase64}) for each arm.`,
|
|
858
|
+
` - After the code is deployed, recapture_experiment_screenshots refreshes the live host.`,
|
|
859
|
+
`7. Show the user the diff. Dashboard "View live" links are built from target_url (not a hardcoded host). Preview the variant at: ${previewUrl}`,
|
|
860
|
+
` If the merchant is QA'ing on localhost, the dashboard rewrites Apex app hosts to this origin so the unpublished variant is visible.`,
|
|
820
861
|
`8. After preview approval, commit and push`,
|
|
821
862
|
`9. Call track_deployment with the experiment ID and commit SHA`,
|
|
822
863
|
`10. Call activate_experiment (with the user's go-ahead). If the code isn't live yet, the`,
|
|
@@ -960,7 +1001,7 @@ export const toolDefinitions = {
|
|
|
960
1001
|
},
|
|
961
1002
|
},
|
|
962
1003
|
recapture_experiment_screenshots: {
|
|
963
|
-
description: `${APEX} — Re-run the hosted screenshot capture for a WEB experiment's arms (renders the target URL for control + variant and stores the images shown on the dashboard). Use
|
|
1004
|
+
description: `${APEX} — Re-run the hosted screenshot capture for a WEB experiment's arms (renders the target URL for control + variant and stores the images shown on the dashboard). Use after the variant is deployed. For unpublished SDK / Cursor drafts, attach localhost shots with attach_experiment_asset instead — hosted recapture of a public URL will be refused.`,
|
|
964
1005
|
schema: z.object({
|
|
965
1006
|
experimentId: z.string().describe("The experiment ID to re-capture screenshots for."),
|
|
966
1007
|
}),
|
|
@@ -1066,7 +1107,7 @@ export const toolDefinitions = {
|
|
|
1066
1107
|
},
|
|
1067
1108
|
},
|
|
1068
1109
|
activate_experiment: {
|
|
1069
|
-
description: `${APEX} — Launch an experiment. Changes its status from draft to running. Traffic will be split immediately. Before flipping to running, this verifies BOTH arms are producing \`experiment_exposure\` events (wiring check). Code-wired (SDK-hook) experiments may be activated before their code ships: the server holds them in \`pending_deployment\` ("Waiting on deploy") and starts on the first exposure. Snippet experiments
|
|
1110
|
+
description: `${APEX} — Launch an experiment. Changes its status from draft to running. Traffic will be split immediately. Before flipping to running, this verifies BOTH arms are producing \`experiment_exposure\` events (wiring check). Code-wired (SDK-hook) experiments may be activated before their code ships: the server holds them in \`pending_deployment\` ("Waiting on deploy") and starts on the first exposure. Snippet experiments and journey_arm message tests do not need pre-activation exposures — journey traffic starts on the next send once running. Only wired web experiments that genuinely can't self-start are refused unless force is true. Ask the user to confirm before calling.`,
|
|
1070
1111
|
schema: z.object({
|
|
1071
1112
|
experimentId: z.string().describe("The experiment ID to activate"),
|
|
1072
1113
|
force: z
|
|
@@ -1091,11 +1132,7 @@ export const toolDefinitions = {
|
|
|
1091
1132
|
// the experiment ONCE IT'S RUNNING, so it can't have pre-activation
|
|
1092
1133
|
// exposures — blocking on them is a chicken-and-egg. Both are safe to
|
|
1093
1134
|
// launch "early"; wiring is verified AFTER via verify_experiment_wiring.
|
|
1094
|
-
if (wiring &&
|
|
1095
|
-
!wiring.bothArmsLive &&
|
|
1096
|
-
force !== true &&
|
|
1097
|
-
!wiring.sdkMode &&
|
|
1098
|
-
!wiring.snippetMode) {
|
|
1135
|
+
if (wiring && shouldRefuseActivationWiring(wiring, force)) {
|
|
1099
1136
|
const armLines = wiring.perArm.map(formatArmLine);
|
|
1100
1137
|
const zeroArms = wiring.perArm
|
|
1101
1138
|
.filter((a) => a.exposures === 0)
|
|
@@ -1194,9 +1231,11 @@ export const toolDefinitions = {
|
|
|
1194
1231
|
// PATCH returns a minimal/legacy body — re-read the unified record to
|
|
1195
1232
|
// derive the live traffic split from the allocation weights.
|
|
1196
1233
|
let controlPct = 50;
|
|
1234
|
+
let liveSurface = exp.surface;
|
|
1197
1235
|
try {
|
|
1198
1236
|
const unified = await apiGet(`/api/experiments/${experimentId}?unified=true`);
|
|
1199
1237
|
controlPct = controlPctOf(unified);
|
|
1238
|
+
liveSurface = unified.surface ?? liveSurface;
|
|
1200
1239
|
}
|
|
1201
1240
|
catch { /* fall back to 50/50 in the message */ }
|
|
1202
1241
|
// Surface why the wiring gate didn't block: either it couldn't be
|
|
@@ -1235,7 +1274,7 @@ export const toolDefinitions = {
|
|
|
1235
1274
|
` Traffic is being split ${controlPct}% / ${100 - controlPct}%`,
|
|
1236
1275
|
``,
|
|
1237
1276
|
...notes,
|
|
1238
|
-
`
|
|
1277
|
+
` ${activationLiveLine(liveSurface)}`,
|
|
1239
1278
|
` Results will appear in the dashboard and via get_results.`,
|
|
1240
1279
|
``,
|
|
1241
1280
|
`${"═".repeat(40)}`,
|
|
@@ -1646,6 +1685,8 @@ export const toolDefinitions = {
|
|
|
1646
1685
|
name: z.string().optional().describe("Rename (cosmetic; editable anytime)."),
|
|
1647
1686
|
surface: z.enum(["web", "mobile"]).optional().describe("Correct the surface — editable only on a draft with 0 exposures (web↔mobile)."),
|
|
1648
1687
|
hypothesis: z.string().optional().describe("Edit the hypothesis — draft only (pre-registration)."),
|
|
1688
|
+
targetUrl: z.string().optional().describe("Retarget a DRAFT to a URL on the ACTIVE workspace. Same host rules as create_experiment."),
|
|
1689
|
+
predictionId: z.string().optional().describe("Attach a minted prediction id (from log_prediction) to this draft. Required before activation."),
|
|
1649
1690
|
primaryMetricEvent: z.string().optional().describe("Change the primary metric's canonical event — draft only."),
|
|
1650
1691
|
primaryMetricType: z.enum(["rate", "count", "revenue", "duration"]).optional(),
|
|
1651
1692
|
guardrailEvent: z.string().optional().describe("Replace the guardrail with this canonical protective event — draft only."),
|
|
@@ -1653,6 +1694,15 @@ export const toolDefinitions = {
|
|
|
1653
1694
|
runtimeDays: z.number().int().min(1).max(365).optional().describe("Set how long to run the experiment, in days (the decision window Apex force-concludes at). Draft only — part of the pre-registration; freezes at launch (fork to change it)."),
|
|
1654
1695
|
}),
|
|
1655
1696
|
handler: async (args) => {
|
|
1697
|
+
if (args.targetUrl) {
|
|
1698
|
+
const targetCheck = await assertTargetUrlForActiveWorkspace(args.targetUrl);
|
|
1699
|
+
if (!targetCheck.ok) {
|
|
1700
|
+
return {
|
|
1701
|
+
content: [{ type: "text", text: `${APEX} ${targetCheck.message}` }],
|
|
1702
|
+
isError: true,
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1656
1706
|
const body = { id: args.experimentId };
|
|
1657
1707
|
if (args.name !== undefined)
|
|
1658
1708
|
body.name = args.name;
|
|
@@ -1660,6 +1710,10 @@ export const toolDefinitions = {
|
|
|
1660
1710
|
body.surface = args.surface;
|
|
1661
1711
|
if (args.hypothesis !== undefined)
|
|
1662
1712
|
body.hypothesis = args.hypothesis;
|
|
1713
|
+
if (args.targetUrl !== undefined)
|
|
1714
|
+
body.targetUrl = args.targetUrl;
|
|
1715
|
+
if (args.predictionId !== undefined)
|
|
1716
|
+
body.predictionId = args.predictionId;
|
|
1663
1717
|
// evaluationWindow patch — the API merges it, so setting one of
|
|
1664
1718
|
// minPerVariant / durationDays never clobbers the other.
|
|
1665
1719
|
if (args.minSamplePerArm !== undefined || args.runtimeDays !== undefined) {
|
|
@@ -1746,7 +1800,15 @@ export const toolDefinitions = {
|
|
|
1746
1800
|
content: [
|
|
1747
1801
|
{
|
|
1748
1802
|
type: "text",
|
|
1749
|
-
text:
|
|
1803
|
+
text: [
|
|
1804
|
+
`${APEX} Forked into a fresh draft "${forked.name}" (id: ${forked.id}).`,
|
|
1805
|
+
``,
|
|
1806
|
+
`A copy gets a new id. The page still asks the old one until you replace it.`,
|
|
1807
|
+
` Find: const variant = useApexVariant("${experimentId}");`,
|
|
1808
|
+
` Replace: const variant = useApexVariant("${forked.id}");`,
|
|
1809
|
+
`Deploy that change, then activate.`,
|
|
1810
|
+
`Preview still works via _apex_exp. A normal visit shows control until the new id is shipped.`,
|
|
1811
|
+
].join("\n"),
|
|
1750
1812
|
},
|
|
1751
1813
|
],
|
|
1752
1814
|
};
|
|
@@ -2742,9 +2804,9 @@ Identical to the SDK's track() and the snippet's apex.track(). Use this to instr
|
|
|
2742
2804
|
|
|
2743
2805
|
The Apex Spec ships two peer registries:
|
|
2744
2806
|
|
|
2745
|
-
\u2022 **App events** (\`APEX_EVENTS\`) — the canonical SDK vocabulary the customer's app fires. Names you'll commonly use here: \`page_view\`, \`product_view\`, \`add_to_cart\`, \`checkout_started\`, \`in_app_purchase\`, \`purchase_refunded\`, \`subscription_event\`, \`user_signed_up\`, \`user_signed_in\`, \`user_identified\`, \`app_open\`, \`session_start\`, \`form_submit\`, \`click\`, \`search\`, \`share\`, \`content_view\`, \`goal_conversion\`, \`email_opened\`, \`email_clicked\`, \`push_opened\`, \`in_app_message_seen\`, \`deep_link_open\`. Field names are flat snake_case (e.g. \`product_id\`, \`order_id\`, \`value\`, \`currency\`).
|
|
2807
|
+
\u2022 **App events** (\`APEX_EVENTS\`) — the canonical SDK vocabulary the customer's app fires. Names you'll commonly use here: \`page_view\`, \`product_view\`, \`add_to_cart\`, \`checkout_started\`, \`in_app_purchase\`, \`purchase_refunded\`, \`subscription_event\`, \`user_signed_up\`, \`user_signed_in\`, \`user_identified\`, \`app_open\`, \`session_start\`, \`form_submit\`, \`click\`, \`ui_action\`, \`search\`, \`share\`, \`content_view\`, \`goal_conversion\`, \`email_opened\`, \`email_clicked\`, \`push_opened\`, \`in_app_message_seen\`, \`deep_link_open\`. Field names are flat snake_case (e.g. \`product_id\`, \`order_id\`, \`value\`, \`currency\`). Named in-product controls (buttons, tabs, widget chrome) are \`ui_action\` with \`action\` + \`name\` + optional \`location\` — do not invent one event per button. SDK helper: \`trackUiAction({ action, name, location })\`.
|
|
2746
2808
|
|
|
2747
|
-
\u2022 **Platform events** (\`APEX_PLATFORM_EVENTS\`) — events Apex's own dashboard fires as merchants use it. Names: \`user_invitation_created\`, \`user_invitation_accepted\`, \`developer_invite_sent\`, \`integration_connected\`, \`integration_sync_completed\`, \`integration_sync_failed\`, \`experiment_launched\`, \`experiment_significant\`, \`experiment_concluded\`, \`experiment_promoted\`, \`portfolio_access_requested\`, \`portfolio_access_approved\`, \`portfolio_access_rejected\`, \`portfolio_share_accepted\`, \`subscription_upgraded\`, \`subscription_downgraded\`, \`tier_limit_approaching\`, \`anomaly_detected\`, \`weekly_digest_scheduled\`, \`cognito_verification_code\`, \`user_signup\`. Use these when an agent is acting *as* the Apex platform (admin tooling, dashboard automation), not as the customer's product.
|
|
2809
|
+
\u2022 **Platform events** (\`APEX_PLATFORM_EVENTS\`) — events Apex's own dashboard fires as merchants use it. Names: \`user_invitation_created\`, \`user_invitation_accepted\`, \`developer_invite_sent\`, \`integration_selected\`, \`integration_connected\`, \`setup_step_continued\`, \`integration_sync_completed\`, \`integration_sync_failed\`, \`experiment_launched\`, \`experiment_significant\`, \`experiment_concluded\`, \`experiment_promoted\`, \`portfolio_access_requested\`, \`portfolio_access_approved\`, \`portfolio_access_rejected\`, \`portfolio_share_accepted\`, \`subscription_upgraded\`, \`subscription_downgraded\`, \`tier_limit_approaching\`, \`anomaly_detected\`, \`weekly_digest_scheduled\`, \`cognito_verification_code\`, \`user_signup\`. Use these when an agent is acting *as* the Apex platform (admin tooling, dashboard automation), not as the customer's product.
|
|
2748
2810
|
|
|
2749
2811
|
Custom event names are accepted (the spec is open by default), but agents should prefer canonical names from one of the two registries so the event flows directly into the catalog templates, the journey trigger system, and the conversion-goal helpers without manual wiring. The full spec lives at https://docs.apex.inc/spec.
|
|
2750
2812
|
|
|
@@ -3114,7 +3176,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3114
3176
|
},
|
|
3115
3177
|
},
|
|
3116
3178
|
list_communications: {
|
|
3117
|
-
description: "List the workspace's communication templates (email / inbox / web push / mobile push) with the fields you need to CHOOSE one — for a journey send step, a broadcast, or a transactional send. Returns per template: title, version, status, pipeline, channels, subject, a one-line body preview so you can disambiguate similar titles without opening each one, and controlState. Pipeline is 'transactional' (bypasses opt-out — needs a recipient-initiated trigger) or 'marketing' (consent-gated). Filter by pipeline, channel, or a search string. This is the programmatic equivalent of the dashboard's communication picker.\n\ncontrolState tells you what the Control currently IS, and therefore what editing it will cost: 'winner' (won an experiment, unedited — editing needs an explicit intent), 'winner_edited' (won, then changed since), 'in_experiment' (frozen, an experiment is measuring it right now), 'no_winner' (an experiment ran and settled without one), 'none' (never experimented on
|
|
3179
|
+
description: "List the workspace's communication templates (email / inbox / web push / mobile push) with the fields you need to CHOOSE one — for a journey send step, a broadcast, or a transactional send. Returns per template: title, version, status, pipeline, channels, subject, a one-line body preview so you can disambiguate similar titles without opening each one, and controlState. Pipeline is 'transactional' (bypasses opt-out — needs a recipient-initiated trigger) or 'marketing' (consent-gated). Filter by pipeline, channel, or a search string. This is the programmatic equivalent of the dashboard's communication picker.\n\ncontrolState tells you what the Control currently IS, and therefore what editing it will cost: 'winner' (won an experiment, unedited — editing needs an explicit intent), 'winner_edited' (won, then changed since), 'in_experiment' (frozen, an experiment is measuring it right now), 'no_winner' (an experiment ran and settled without one), 'none' (never experimented on). A live letter with no champion is still editable; Publish asks replace vs compete. A champion or a running test: do not edit Control — add a variant, then compete.",
|
|
3118
3180
|
schema: z.object({
|
|
3119
3181
|
pipeline: z
|
|
3120
3182
|
.enum(["transactional", "marketing"])
|
|
@@ -3341,13 +3403,13 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3341
3403
|
},
|
|
3342
3404
|
},
|
|
3343
3405
|
publish_communication: {
|
|
3344
|
-
description: "Publish a communication's draft as the live version — this is what makes an edit reach recipients. Editing only saves a draft.\n\
|
|
3406
|
+
description: "Publish a communication's draft as the live version — this is what makes an edit reach recipients. Editing only saves a draft.\n\nCompete (or publishing a letter that already has variants) creates a prefilled DRAFT experiment. It does not start the test. After the user reviews the draft, call activate_experiment to begin it. Replace publishes the letter for everyone and never creates an experiment.\n\nOptional overrides: hypothesis, belief_id, confidence, duration_days.\n\nTwo conflicts need a decision rather than a retry. `ambiguous_experiment_host` means several published journeys send this communication — the candidates are returned; ask the user and re-issue with host_journey_id. `comm_already_in_experiment` means one is already running; end it first.",
|
|
3345
3407
|
schema: z.object({
|
|
3346
3408
|
communicationId: z.string().describe("The communication ID to publish"),
|
|
3347
3409
|
intent: z
|
|
3348
3410
|
.enum(["compete", "replace"])
|
|
3349
3411
|
.optional()
|
|
3350
|
-
.describe('How to apply the draft when the Control is protected. "compete" = the current content keeps sending and your draft
|
|
3412
|
+
.describe('How to apply the draft when the Control is protected. "compete" = the current content keeps sending and your draft becomes a filled-in experiment draft (then activate_experiment). "replace" = your draft becomes what everyone receives. Required only when the comm won an experiment or is live in a published journey.'),
|
|
3351
3413
|
confirmLiveExperiment: z
|
|
3352
3414
|
.boolean()
|
|
3353
3415
|
.optional()
|
|
@@ -3356,6 +3418,22 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3356
3418
|
.string()
|
|
3357
3419
|
.optional()
|
|
3358
3420
|
.describe("Which published journey hosts the experiment. Only needed after a 409 ambiguous_experiment_host, using one of the returned candidate ids."),
|
|
3421
|
+
hypothesis: z
|
|
3422
|
+
.string()
|
|
3423
|
+
.optional()
|
|
3424
|
+
.describe("Override the Suggested hypothesis on the draft experiment."),
|
|
3425
|
+
beliefId: z
|
|
3426
|
+
.string()
|
|
3427
|
+
.optional()
|
|
3428
|
+
.describe("Link the draft experiment to an existing belief."),
|
|
3429
|
+
confidence: z
|
|
3430
|
+
.number()
|
|
3431
|
+
.optional()
|
|
3432
|
+
.describe("Override the prefilled confidence (percent)."),
|
|
3433
|
+
durationDays: z
|
|
3434
|
+
.number()
|
|
3435
|
+
.optional()
|
|
3436
|
+
.describe("Override the prefilled decision window in days."),
|
|
3359
3437
|
}),
|
|
3360
3438
|
handler: async (args) => {
|
|
3361
3439
|
const body = {};
|
|
@@ -3365,6 +3443,14 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3365
3443
|
body.confirmLiveExperiment = true;
|
|
3366
3444
|
if (args.hostJourneyId)
|
|
3367
3445
|
body.hostJourneyId = args.hostJourneyId;
|
|
3446
|
+
if (args.hypothesis)
|
|
3447
|
+
body.hypothesis = args.hypothesis;
|
|
3448
|
+
if (args.beliefId)
|
|
3449
|
+
body.beliefId = args.beliefId;
|
|
3450
|
+
if (typeof args.confidence === "number")
|
|
3451
|
+
body.confidence = args.confidence;
|
|
3452
|
+
if (typeof args.durationDays === "number")
|
|
3453
|
+
body.durationDays = args.durationDays;
|
|
3368
3454
|
try {
|
|
3369
3455
|
const data = await apiPost(`/api/communications/${args.communicationId}/publish`, body);
|
|
3370
3456
|
const lines = [];
|
|
@@ -3378,7 +3464,11 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3378
3464
|
lines.push(`${data.repinned.length} send step(s) moved to the new version.`);
|
|
3379
3465
|
}
|
|
3380
3466
|
if (data.experiment) {
|
|
3381
|
-
lines.push(
|
|
3467
|
+
lines.push(publishedExperimentLine({
|
|
3468
|
+
id: data.experiment.id,
|
|
3469
|
+
status: data.experiment.status ?? "draft",
|
|
3470
|
+
url: appUrl(`/dashboard/experiments/${data.experiment.id}`),
|
|
3471
|
+
}));
|
|
3382
3472
|
}
|
|
3383
3473
|
else if (data.experimentNotStarted) {
|
|
3384
3474
|
lines.push(notStartedExplanation(data.experimentNotStarted));
|
|
@@ -3453,7 +3543,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3453
3543
|
},
|
|
3454
3544
|
},
|
|
3455
3545
|
add_communication_variant: {
|
|
3456
|
-
description: "Add a content variant (Variant A, B, …) to a communication. Clones the current Control into a new column so you can run a content experiment. Does not change what recipients get until you publish. Theme look is inherited; do not set per-block align here.",
|
|
3546
|
+
description: "Add a content variant (Variant A, B, …) to a communication. Clones the current Control into a new column so you can run a content experiment. When the letter is a champion, this is how you author a challenger — do not edit Control. Does not change what recipients get until you publish. Theme look is inherited; do not set per-block align here.",
|
|
3457
3547
|
schema: z.object({
|
|
3458
3548
|
communicationId: z.string().describe("The communication to add a variant to"),
|
|
3459
3549
|
label: z.string().optional().describe("Optional column label (e.g. Variant A)"),
|
|
@@ -3496,7 +3586,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3496
3586
|
},
|
|
3497
3587
|
},
|
|
3498
3588
|
create_comm_experiment: {
|
|
3499
|
-
description: "
|
|
3589
|
+
description: "Legacy helper. Prefer the composer loop: edit_communication or add_communication_variant, then publish_communication (compete creates a prefilled draft), then activate_experiment to start the test. Do not teach this as the default.",
|
|
3500
3590
|
schema: z.object({
|
|
3501
3591
|
communicationId: z.string().describe("The communication to experiment on"),
|
|
3502
3592
|
splitLevel: z.enum(["subject", "body", "cta", "full"]).describe("What to vary between variants"),
|