@apex-inc/mcp-server 0.25.0 → 0.27.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.
- 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.js +2 -2
- package/dist/control-gate-guidance.js.map +1 -1
- 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 +73 -5
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +114 -42
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/skills/apex-adoption/SKILL.md +2 -0
- package/skills/apex-communications/SKILL.md +28 -16
- package/skills/apex-experimentation/SKILL.md +15 -5
- package/skills/apex-growth-tracking/SKILL.md +10 -10
- package/skills/apex-integration-cookbook/SKILL.md +8 -7
- package/skills/apex-spec/SKILL.md +6 -0
package/dist/tools.js
CHANGED
|
@@ -5,6 +5,8 @@ import { apiGet, apiPost, apiPatch, apiPut, apiDelete, postWithIdempotency, setA
|
|
|
5
5
|
import { resolveExperimentHypothesis } from "./experiment-copy.js";
|
|
6
6
|
import { controlGateGuidanceFor } from "./control-gate-guidance.js";
|
|
7
7
|
import { getPersonLabel, lowerPerson } from "./person-label.js";
|
|
8
|
+
import { notStartedExplanation, publishedExperimentLine, } from "./publish-communication-copy.js";
|
|
9
|
+
import { activationLiveLine, shouldRefuseActivationWiring, } from "./activation-wiring.js";
|
|
8
10
|
const APEX = "∧ Apex";
|
|
9
11
|
/**
|
|
10
12
|
* MOBX-006 — stable synthetic visitor id for agent-fired events.
|
|
@@ -81,28 +83,6 @@ function appUrl(path) {
|
|
|
81
83
|
const base = process.env.APEX_URL || process.env.APEX_API_URL || "http://localhost:3001";
|
|
82
84
|
return `${base.replace(/\/$/, "")}${path}`;
|
|
83
85
|
}
|
|
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
86
|
function journeyLink(id) {
|
|
107
87
|
return appUrl(`/dashboard/communications/journeys/${id}`);
|
|
108
88
|
}
|
|
@@ -1066,7 +1046,7 @@ export const toolDefinitions = {
|
|
|
1066
1046
|
},
|
|
1067
1047
|
},
|
|
1068
1048
|
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
|
|
1049
|
+
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
1050
|
schema: z.object({
|
|
1071
1051
|
experimentId: z.string().describe("The experiment ID to activate"),
|
|
1072
1052
|
force: z
|
|
@@ -1091,11 +1071,7 @@ export const toolDefinitions = {
|
|
|
1091
1071
|
// the experiment ONCE IT'S RUNNING, so it can't have pre-activation
|
|
1092
1072
|
// exposures — blocking on them is a chicken-and-egg. Both are safe to
|
|
1093
1073
|
// 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) {
|
|
1074
|
+
if (wiring && shouldRefuseActivationWiring(wiring, force)) {
|
|
1099
1075
|
const armLines = wiring.perArm.map(formatArmLine);
|
|
1100
1076
|
const zeroArms = wiring.perArm
|
|
1101
1077
|
.filter((a) => a.exposures === 0)
|
|
@@ -1194,9 +1170,11 @@ export const toolDefinitions = {
|
|
|
1194
1170
|
// PATCH returns a minimal/legacy body — re-read the unified record to
|
|
1195
1171
|
// derive the live traffic split from the allocation weights.
|
|
1196
1172
|
let controlPct = 50;
|
|
1173
|
+
let liveSurface = exp.surface;
|
|
1197
1174
|
try {
|
|
1198
1175
|
const unified = await apiGet(`/api/experiments/${experimentId}?unified=true`);
|
|
1199
1176
|
controlPct = controlPctOf(unified);
|
|
1177
|
+
liveSurface = unified.surface ?? liveSurface;
|
|
1200
1178
|
}
|
|
1201
1179
|
catch { /* fall back to 50/50 in the message */ }
|
|
1202
1180
|
// Surface why the wiring gate didn't block: either it couldn't be
|
|
@@ -1235,7 +1213,7 @@ export const toolDefinitions = {
|
|
|
1235
1213
|
` Traffic is being split ${controlPct}% / ${100 - controlPct}%`,
|
|
1236
1214
|
``,
|
|
1237
1215
|
...notes,
|
|
1238
|
-
`
|
|
1216
|
+
` ${activationLiveLine(liveSurface)}`,
|
|
1239
1217
|
` Results will appear in the dashboard and via get_results.`,
|
|
1240
1218
|
``,
|
|
1241
1219
|
`${"═".repeat(40)}`,
|
|
@@ -1746,7 +1724,15 @@ export const toolDefinitions = {
|
|
|
1746
1724
|
content: [
|
|
1747
1725
|
{
|
|
1748
1726
|
type: "text",
|
|
1749
|
-
text:
|
|
1727
|
+
text: [
|
|
1728
|
+
`${APEX} Forked into a fresh draft "${forked.name}" (id: ${forked.id}).`,
|
|
1729
|
+
``,
|
|
1730
|
+
`A copy gets a new id. The page still asks the old one until you replace it.`,
|
|
1731
|
+
` Find: const variant = useApexVariant("${experimentId}");`,
|
|
1732
|
+
` Replace: const variant = useApexVariant("${forked.id}");`,
|
|
1733
|
+
`Deploy that change, then activate.`,
|
|
1734
|
+
`Preview still works via _apex_exp. A normal visit shows control until the new id is shipped.`,
|
|
1735
|
+
].join("\n"),
|
|
1750
1736
|
},
|
|
1751
1737
|
],
|
|
1752
1738
|
};
|
|
@@ -2742,9 +2728,9 @@ Identical to the SDK's track() and the snippet's apex.track(). Use this to instr
|
|
|
2742
2728
|
|
|
2743
2729
|
The Apex Spec ships two peer registries:
|
|
2744
2730
|
|
|
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\`).
|
|
2731
|
+
\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
2732
|
|
|
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.
|
|
2733
|
+
\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
2734
|
|
|
2749
2735
|
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
2736
|
|
|
@@ -2791,9 +2777,17 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
2791
2777
|
visitorId: z.string().optional().describe("Visitor ID to stitch back to a web/mobile session (apex_vid)"),
|
|
2792
2778
|
email: z.string().optional().describe("Email to stitch by identity when no visitorId is known"),
|
|
2793
2779
|
data: z.record(z.unknown()).optional().describe("Event payload (value, currency, order_id, …)"),
|
|
2780
|
+
environment: z
|
|
2781
|
+
.enum(["production", "preview", "localhost"])
|
|
2782
|
+
.optional()
|
|
2783
|
+
.describe("Dashboard slice only. Omit to leave Unclassified. Never use this to force Production dollars."),
|
|
2784
|
+
release_channel: z
|
|
2785
|
+
.string()
|
|
2786
|
+
.optional()
|
|
2787
|
+
.describe("Optional OS channel (testflight, app-store, xcode-debug, …). Dashboard slice only."),
|
|
2794
2788
|
idempotencyKey: z.string().optional().describe("Stable key to prevent double-counting on retry (defaults to a fresh UUID)"),
|
|
2795
2789
|
}),
|
|
2796
|
-
handler: async ({ type, visitorId, email, data, idempotencyKey, }) => {
|
|
2790
|
+
handler: async ({ type, visitorId, email, data, environment, release_channel, idempotencyKey, }) => {
|
|
2797
2791
|
const key = idempotencyKey ??
|
|
2798
2792
|
(typeof globalThis.crypto?.randomUUID === "function"
|
|
2799
2793
|
? globalThis.crypto.randomUUID()
|
|
@@ -2805,6 +2799,8 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
2805
2799
|
visitorId,
|
|
2806
2800
|
email,
|
|
2807
2801
|
data,
|
|
2802
|
+
environment,
|
|
2803
|
+
release_channel,
|
|
2808
2804
|
timestamp: new Date().toISOString(),
|
|
2809
2805
|
},
|
|
2810
2806
|
],
|
|
@@ -3271,6 +3267,10 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3271
3267
|
slots: z.record(z.string()).optional().describe("Content overrides: headline, body, ctaLabel, ctaUrl. These now sync into the rendered email body (not just metadata)."),
|
|
3272
3268
|
channels: z.array(z.string()).optional().describe("Channels: email, in_app_push, mobile_push"),
|
|
3273
3269
|
status: z.enum(["draft", "active", "paused"]).optional().describe("Communication status"),
|
|
3270
|
+
audience_bucket: z
|
|
3271
|
+
.enum(["production", "beta", "dev"])
|
|
3272
|
+
.optional()
|
|
3273
|
+
.describe("Who this send is for. Default production. Never All or Unclassified."),
|
|
3274
3274
|
intent: z
|
|
3275
3275
|
.enum(["compete", "replace"])
|
|
3276
3276
|
.optional()
|
|
@@ -3286,6 +3286,8 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3286
3286
|
body.status = args.status;
|
|
3287
3287
|
if (args.channels)
|
|
3288
3288
|
body.channels = args.channels;
|
|
3289
|
+
if (args.audience_bucket)
|
|
3290
|
+
body.audienceBucket = args.audience_bucket;
|
|
3289
3291
|
if (args.intent)
|
|
3290
3292
|
body.intent = args.intent;
|
|
3291
3293
|
if (args.confirmLiveExperiment)
|
|
@@ -3325,13 +3327,13 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3325
3327
|
},
|
|
3326
3328
|
},
|
|
3327
3329
|
publish_communication: {
|
|
3328
|
-
description: "Publish a communication's draft as the live version — this is what makes an edit reach recipients. Editing only saves a draft.\n\
|
|
3330
|
+
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.",
|
|
3329
3331
|
schema: z.object({
|
|
3330
3332
|
communicationId: z.string().describe("The communication ID to publish"),
|
|
3331
3333
|
intent: z
|
|
3332
3334
|
.enum(["compete", "replace"])
|
|
3333
3335
|
.optional()
|
|
3334
|
-
.describe('How to apply the draft when the Control is protected. "compete" = the current content keeps sending and your draft
|
|
3336
|
+
.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.'),
|
|
3335
3337
|
confirmLiveExperiment: z
|
|
3336
3338
|
.boolean()
|
|
3337
3339
|
.optional()
|
|
@@ -3340,6 +3342,22 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3340
3342
|
.string()
|
|
3341
3343
|
.optional()
|
|
3342
3344
|
.describe("Which published journey hosts the experiment. Only needed after a 409 ambiguous_experiment_host, using one of the returned candidate ids."),
|
|
3345
|
+
hypothesis: z
|
|
3346
|
+
.string()
|
|
3347
|
+
.optional()
|
|
3348
|
+
.describe("Override the Suggested hypothesis on the draft experiment."),
|
|
3349
|
+
beliefId: z
|
|
3350
|
+
.string()
|
|
3351
|
+
.optional()
|
|
3352
|
+
.describe("Link the draft experiment to an existing belief."),
|
|
3353
|
+
confidence: z
|
|
3354
|
+
.number()
|
|
3355
|
+
.optional()
|
|
3356
|
+
.describe("Override the prefilled confidence (percent)."),
|
|
3357
|
+
durationDays: z
|
|
3358
|
+
.number()
|
|
3359
|
+
.optional()
|
|
3360
|
+
.describe("Override the prefilled decision window in days."),
|
|
3343
3361
|
}),
|
|
3344
3362
|
handler: async (args) => {
|
|
3345
3363
|
const body = {};
|
|
@@ -3349,6 +3367,14 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3349
3367
|
body.confirmLiveExperiment = true;
|
|
3350
3368
|
if (args.hostJourneyId)
|
|
3351
3369
|
body.hostJourneyId = args.hostJourneyId;
|
|
3370
|
+
if (args.hypothesis)
|
|
3371
|
+
body.hypothesis = args.hypothesis;
|
|
3372
|
+
if (args.beliefId)
|
|
3373
|
+
body.beliefId = args.beliefId;
|
|
3374
|
+
if (typeof args.confidence === "number")
|
|
3375
|
+
body.confidence = args.confidence;
|
|
3376
|
+
if (typeof args.durationDays === "number")
|
|
3377
|
+
body.durationDays = args.durationDays;
|
|
3352
3378
|
try {
|
|
3353
3379
|
const data = await apiPost(`/api/communications/${args.communicationId}/publish`, body);
|
|
3354
3380
|
const lines = [];
|
|
@@ -3362,7 +3388,11 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3362
3388
|
lines.push(`${data.repinned.length} send step(s) moved to the new version.`);
|
|
3363
3389
|
}
|
|
3364
3390
|
if (data.experiment) {
|
|
3365
|
-
lines.push(
|
|
3391
|
+
lines.push(publishedExperimentLine({
|
|
3392
|
+
id: data.experiment.id,
|
|
3393
|
+
status: data.experiment.status ?? "draft",
|
|
3394
|
+
url: appUrl(`/dashboard/experiments/${data.experiment.id}`),
|
|
3395
|
+
}));
|
|
3366
3396
|
}
|
|
3367
3397
|
else if (data.experimentNotStarted) {
|
|
3368
3398
|
lines.push(notStartedExplanation(data.experimentNotStarted));
|
|
@@ -3480,7 +3510,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3480
3510
|
},
|
|
3481
3511
|
},
|
|
3482
3512
|
create_comm_experiment: {
|
|
3483
|
-
description: "
|
|
3513
|
+
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.",
|
|
3484
3514
|
schema: z.object({
|
|
3485
3515
|
communicationId: z.string().describe("The communication to experiment on"),
|
|
3486
3516
|
splitLevel: z.enum(["subject", "body", "cta", "full"]).describe("What to vary between variants"),
|
|
@@ -3963,11 +3993,20 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3963
3993
|
// Agent-friendly arg names (snake_case) are mapped to the REST
|
|
3964
3994
|
// routes inside each handler.
|
|
3965
3995
|
create_journey: {
|
|
3966
|
-
description: `${APEX} — Create a blank draft journey (an unconfigured event trigger wired to an exit). Then set_journey_trigger and add_journey_step to build it, and publish_journey to launch. For e-commerce flows like cart recovery, prefer create_journey_from_template.`,
|
|
3967
|
-
schema: z.object({
|
|
3968
|
-
|
|
3996
|
+
description: `${APEX} — Create a blank draft journey (an unconfigured event trigger wired to an exit). Then set_journey_trigger and add_journey_step to build it, and publish_journey to launch. For e-commerce flows like cart recovery, prefer create_journey_from_template. audience_bucket defaults to production (live customers). Same email on TestFlight and the store is two people — do not create a staging copy of the journey.`,
|
|
3997
|
+
schema: z.object({
|
|
3998
|
+
name: z.string().optional().describe("Journey name (default 'Untitled journey')."),
|
|
3999
|
+
audience_bucket: z
|
|
4000
|
+
.enum(["production", "beta", "dev"])
|
|
4001
|
+
.optional()
|
|
4002
|
+
.describe("Who this journey enrolls. Default production. Never All or Unclassified."),
|
|
4003
|
+
}),
|
|
4004
|
+
handler: async ({ name, audience_bucket }) => {
|
|
3969
4005
|
try {
|
|
3970
4006
|
const j = await apiPost("/api/journeys/blank", { name });
|
|
4007
|
+
if (audience_bucket && audience_bucket !== "production") {
|
|
4008
|
+
await apiPatch(`/api/journeys/${j.id}`, { audienceBucket: audience_bucket });
|
|
4009
|
+
}
|
|
3971
4010
|
if (!tenantOk(j.workspaceKey))
|
|
3972
4011
|
return tenantMismatch();
|
|
3973
4012
|
return { content: [{ type: "text", text: `${APEX} Created draft journey "${j.name}" (id: ${j.id}). Open it in the browser: ${journeyLink(j.id)}\nNext: set_journey_trigger, then add_journey_step.` }] };
|
|
@@ -4223,14 +4262,21 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
4223
4262
|
},
|
|
4224
4263
|
},
|
|
4225
4264
|
create_communication: {
|
|
4226
|
-
description: `${APEX} — Create a blank custom communication (draft, no send path) you can reference from a journey send step. Distinct from generate_communications, which generates pre-written messages from catalog templates; use this when you need a net-new message (e.g. a cart-recovery email not in the catalog).`,
|
|
4265
|
+
description: `${APEX} — Create a blank custom communication (draft, no send path) you can reference from a journey send step. Distinct from generate_communications, which generates pre-written messages from catalog templates; use this when you need a net-new message (e.g. a cart-recovery email not in the catalog). audience_bucket defaults to production.`,
|
|
4227
4266
|
schema: z.object({
|
|
4228
4267
|
title: z.string().optional(),
|
|
4229
4268
|
channels: z.array(z.enum(["email", "in_app_push", "mobile_push"])).optional(),
|
|
4269
|
+
audience_bucket: z
|
|
4270
|
+
.enum(["production", "beta", "dev"])
|
|
4271
|
+
.optional()
|
|
4272
|
+
.describe("Who this send is for. Default production. Never All or Unclassified."),
|
|
4230
4273
|
}),
|
|
4231
|
-
handler: async ({ title, channels }) => {
|
|
4274
|
+
handler: async ({ title, channels, audience_bucket }) => {
|
|
4232
4275
|
try {
|
|
4233
4276
|
const comm = await apiPost("/api/communications/blank", { title, channels });
|
|
4277
|
+
if (audience_bucket && audience_bucket !== "production") {
|
|
4278
|
+
await apiPatch(`/api/communications/${comm.id}`, { audienceBucket: audience_bucket });
|
|
4279
|
+
}
|
|
4234
4280
|
if (!tenantOk(comm.workspaceKey))
|
|
4235
4281
|
return tenantMismatch();
|
|
4236
4282
|
return { content: [{ type: "text", text: `${APEX} Created draft communication "${comm.title}" (id: ${comm.id}). Open it in the browser: ${appUrl(`/dashboard/communications/${comm.id}`)}\nEdit it (or edit_communication), then reference it from a journey send step.` }] };
|
|
@@ -4521,6 +4567,32 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
4521
4567
|
return { content: [{ type: "text", text }] };
|
|
4522
4568
|
},
|
|
4523
4569
|
},
|
|
4570
|
+
list_workspace_environments: {
|
|
4571
|
+
description: `${APEX} — Read the sites this workspace named as Production and Beta, plus unclassified sites Apex has seen. Name sites before you trust the Production slice. Do not invent a second workspace.`,
|
|
4572
|
+
schema: z.object({}),
|
|
4573
|
+
handler: async () => {
|
|
4574
|
+
const json = await apiGet("/api/workspace/environments");
|
|
4575
|
+
return {
|
|
4576
|
+
content: [{ type: "text", text: JSON.stringify(json.data, null, 2) }],
|
|
4577
|
+
};
|
|
4578
|
+
},
|
|
4579
|
+
},
|
|
4580
|
+
set_workspace_environments: {
|
|
4581
|
+
description: `${APEX} — Name Production sites (and optional Beta sites). Apex will not guess staging. / qa. / dev. Naming reclassifies events from those sites at read time.`,
|
|
4582
|
+
schema: z.object({
|
|
4583
|
+
production_hosts: z.array(z.string()).describe("Hostnames that are live (shop.example.com)."),
|
|
4584
|
+
beta_hosts: z.array(z.string()).optional().describe("Hostnames that are testers. Never guessed from staging."),
|
|
4585
|
+
}),
|
|
4586
|
+
handler: async ({ production_hosts, beta_hosts, }) => {
|
|
4587
|
+
const json = await apiPut("/api/workspace/environments", {
|
|
4588
|
+
production_hosts,
|
|
4589
|
+
beta_hosts: beta_hosts ?? [],
|
|
4590
|
+
});
|
|
4591
|
+
return {
|
|
4592
|
+
content: [{ type: "text", text: JSON.stringify(json.data, null, 2) }],
|
|
4593
|
+
};
|
|
4594
|
+
},
|
|
4595
|
+
},
|
|
4524
4596
|
// ─── Data Sources + unified analytics ──────────────────────────────────
|
|
4525
4597
|
list_data_sources: {
|
|
4526
4598
|
description: `${APEX} — List the workspace's Data Sources (the producers that send events: Website, iOS/Android apps, Backend). Shows each source's live status (live / listening / stalled / not-started). Call when the user asks "where is my data coming from?", "which sources are live?", or to audit collection coverage.`,
|