@apex-inc/mcp-server 0.27.0 → 0.27.2
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/control-gate-guidance.d.ts +1 -0
- package/dist/control-gate-guidance.d.ts.map +1 -1
- package/dist/control-gate-guidance.js +12 -4
- package/dist/control-gate-guidance.js.map +1 -1
- package/dist/prompts.js +3 -3
- package/dist/tools.d.ts +23 -11
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +130 -33
- 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 +9 -7
- package/skills/apex-experimentation/SKILL.md +6 -2
- package/skills/apex-integration-cookbook/SKILL.md +2 -2
package/dist/tools.js
CHANGED
|
@@ -2,12 +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";
|
|
9
10
|
import { activationLiveLine, shouldRefuseActivationWiring, } from "./activation-wiring.js";
|
|
10
|
-
const APEX = "
|
|
11
|
+
const APEX = "Apex";
|
|
11
12
|
/**
|
|
12
13
|
* MOBX-006 — stable synthetic visitor id for agent-fired events.
|
|
13
14
|
*
|
|
@@ -57,6 +58,55 @@ function titleCaseEvent(event) {
|
|
|
57
58
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
58
59
|
.join(" ");
|
|
59
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
|
+
}
|
|
60
110
|
/**
|
|
61
111
|
* Query string for the pack report windows: relative (`days` / `months`)
|
|
62
112
|
* or explicit `start` + `end` (YYYY-MM-DD, both required together —
|
|
@@ -440,7 +490,7 @@ export const toolDefinitions = {
|
|
|
440
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.`,
|
|
441
491
|
schema: z.object({
|
|
442
492
|
name: z.string().describe("Experiment name"),
|
|
443
|
-
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."),
|
|
444
494
|
controlContent: z.string().describe("The current content that the control shows"),
|
|
445
495
|
variantContent: z.string().describe("The new content for the variant"),
|
|
446
496
|
targetComponent: z.string().optional().describe("Component or file path where the change should be made"),
|
|
@@ -467,6 +517,13 @@ export const toolDefinitions = {
|
|
|
467
517
|
preview: z.boolean().optional().describe("If true, returns a preview without creating. Default: true."),
|
|
468
518
|
}),
|
|
469
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
|
+
}
|
|
470
527
|
const split = args.trafficSplit ?? 50;
|
|
471
528
|
// MCP experiments are ALWAYS code-native (SDK). Client-side snippet /
|
|
472
529
|
// runtime-DOM experiments are intentionally NOT offered here (founder
|
|
@@ -791,12 +848,16 @@ export const toolDefinitions = {
|
|
|
791
848
|
`2. Install the hook package: npm i @apex-inc/react`,
|
|
792
849
|
`3. Add: import { useApexVariant } from "@apex-inc/react"`,
|
|
793
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.`,
|
|
794
852
|
`5. Wrap the target content in a conditional (fall back to control on null/error):`,
|
|
795
853
|
` {variant === "variant_a" || variant === "variant_b" ? <VARIANT_CONTENT> : <CONTROL_CONTENT>}`,
|
|
796
|
-
`6. Screenshots for the dashboard:`,
|
|
797
|
-
` -
|
|
798
|
-
` -
|
|
799
|
-
`
|
|
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.`,
|
|
800
861
|
`8. After preview approval, commit and push`,
|
|
801
862
|
`9. Call track_deployment with the experiment ID and commit SHA`,
|
|
802
863
|
`10. Call activate_experiment (with the user's go-ahead). If the code isn't live yet, the`,
|
|
@@ -940,7 +1001,7 @@ export const toolDefinitions = {
|
|
|
940
1001
|
},
|
|
941
1002
|
},
|
|
942
1003
|
recapture_experiment_screenshots: {
|
|
943
|
-
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.`,
|
|
944
1005
|
schema: z.object({
|
|
945
1006
|
experimentId: z.string().describe("The experiment ID to re-capture screenshots for."),
|
|
946
1007
|
}),
|
|
@@ -1624,6 +1685,8 @@ export const toolDefinitions = {
|
|
|
1624
1685
|
name: z.string().optional().describe("Rename (cosmetic; editable anytime)."),
|
|
1625
1686
|
surface: z.enum(["web", "mobile"]).optional().describe("Correct the surface — editable only on a draft with 0 exposures (web↔mobile)."),
|
|
1626
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. Also stamps the prediction onto the experiment's hypothesis when that link is missing. Required before activation."),
|
|
1627
1690
|
primaryMetricEvent: z.string().optional().describe("Change the primary metric's canonical event — draft only."),
|
|
1628
1691
|
primaryMetricType: z.enum(["rate", "count", "revenue", "duration"]).optional(),
|
|
1629
1692
|
guardrailEvent: z.string().optional().describe("Replace the guardrail with this canonical protective event — draft only."),
|
|
@@ -1631,6 +1694,15 @@ export const toolDefinitions = {
|
|
|
1631
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)."),
|
|
1632
1695
|
}),
|
|
1633
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
|
+
}
|
|
1634
1706
|
const body = { id: args.experimentId };
|
|
1635
1707
|
if (args.name !== undefined)
|
|
1636
1708
|
body.name = args.name;
|
|
@@ -1638,6 +1710,10 @@ export const toolDefinitions = {
|
|
|
1638
1710
|
body.surface = args.surface;
|
|
1639
1711
|
if (args.hypothesis !== undefined)
|
|
1640
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;
|
|
1641
1717
|
// evaluationWindow patch — the API merges it, so setting one of
|
|
1642
1718
|
// minPerVariant / durationDays never clobbers the other.
|
|
1643
1719
|
if (args.minSamplePerArm !== undefined || args.runtimeDays !== undefined) {
|
|
@@ -2726,17 +2802,13 @@ export const toolDefinitions = {
|
|
|
2726
2802
|
|
|
2727
2803
|
Identical to the SDK's track() and the snippet's apex.track(). Use this to instrument events from agent workflows, CI pipelines, or IDE actions.
|
|
2728
2804
|
|
|
2729
|
-
The
|
|
2730
|
-
|
|
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 })\`.
|
|
2805
|
+
The public Spec is one registry (\`APEX_EVENTS\`): \`page_view\`, \`product_view\`, \`add_to_cart\`, \`checkout_started\`, \`order_placed\`, \`in_app_purchase\`, \`user_signed_up\`, \`waitlist_joined\`, \`user_invited\`, \`user_invite_accepted\`, \`user_identified\`, \`app_open\`, \`session_start\`, \`form_submit\`, \`feature_used\`, \`ui_action\`. Field names are flat snake_case. Named in-product controls are \`ui_action\` with \`action\` + \`name\` + optional \`location\`. SDK helper: \`trackUiAction({ action, name, location })\`.
|
|
2732
2806
|
|
|
2733
|
-
|
|
2807
|
+
Custom event names are accepted, but prefer canonical names from GET /api/spec/events so templates, journeys, and conversions wire without aliases. Full spec: https://docs.apex.inc/spec.
|
|
2734
2808
|
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*, fixed for this MCP process) and a timestamp, so they WILL count toward wiring detection (the same signal the Set up Apex page and get_wiring_status read). They are flagged as agent-fired (source: "mcp" in the event data + the x-apex-source header), so dashboards can distinguish them from real app traffic — but they are NOT test-mode events; fire them deliberately.`,
|
|
2809
|
+
Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*, fixed for this MCP process) and a timestamp, so they WILL count toward wiring detection. They are flagged as agent-fired (source: "mcp") — not test-mode. Fire them deliberately.`,
|
|
2738
2810
|
schema: z.object({
|
|
2739
|
-
event: z.string().describe("Event name — snake_case from
|
|
2811
|
+
event: z.string().describe("Event name — snake_case from the Apex Spec (e.g. 'page_view', 'user_signed_up', 'order_placed'). Custom names are accepted but prefer canonical ones."),
|
|
2740
2812
|
properties: z.record(z.unknown()).optional().describe("Flat snake_case properties matching the spec entry for this event (e.g. { product_id: 'prod_abc', value: 29.99, currency: 'USD' })."),
|
|
2741
2813
|
}),
|
|
2742
2814
|
handler: async ({ event, properties }) => {
|
|
@@ -2773,7 +2845,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
2773
2845
|
send_server_event: {
|
|
2774
2846
|
description: `${APEX} — Server Events API. Send a server-to-server event via the /api/v1/events endpoint (clientType: "server", batch cap 100, idempotency-key supported). Use this from agent workflows that simulate backend integrations (Stripe webhook flows, CRM-driven conversions, billing events) or to test the Server Events API end-to-end. Differs from track_event in that it stamps clientType: "server" and uses the v1 ingest contract.`,
|
|
2775
2847
|
schema: z.object({
|
|
2776
|
-
type: z.string().describe("Event type (e.g. '
|
|
2848
|
+
type: z.string().describe("Event type (e.g. 'order_placed', 'user_signed_up', or any custom verb)"),
|
|
2777
2849
|
visitorId: z.string().optional().describe("Visitor ID to stitch back to a web/mobile session (apex_vid)"),
|
|
2778
2850
|
email: z.string().optional().describe("Email to stitch by identity when no visitorId is known"),
|
|
2779
2851
|
data: z.record(z.unknown()).optional().describe("Event payload (value, currency, order_id, …)"),
|
|
@@ -3100,7 +3172,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3100
3172
|
},
|
|
3101
3173
|
},
|
|
3102
3174
|
list_communications: {
|
|
3103
|
-
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
|
|
3175
|
+
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.",
|
|
3104
3176
|
schema: z.object({
|
|
3105
3177
|
pipeline: z
|
|
3106
3178
|
.enum(["transactional", "marketing"])
|
|
@@ -3467,7 +3539,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3467
3539
|
},
|
|
3468
3540
|
},
|
|
3469
3541
|
add_communication_variant: {
|
|
3470
|
-
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.",
|
|
3542
|
+
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.",
|
|
3471
3543
|
schema: z.object({
|
|
3472
3544
|
communicationId: z.string().describe("The communication to add a variant to"),
|
|
3473
3545
|
label: z.string().optional().describe("Optional column label (e.g. Variant A)"),
|
|
@@ -3502,8 +3574,10 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
3502
3574
|
mergeData: z.record(z.string()).optional().describe("Template variables to merge (e.g. user_name, company)"),
|
|
3503
3575
|
}),
|
|
3504
3576
|
handler: async (args) => {
|
|
3505
|
-
|
|
3506
|
-
|
|
3577
|
+
// `/send` was removed. The composer test route is the one-off send:
|
|
3578
|
+
// workspace-admin, optional recipient override, mergeData substitution.
|
|
3579
|
+
await apiPost(`/api/communications/${args.communicationId}/test`, {
|
|
3580
|
+
recipient: args.recipientEmail,
|
|
3507
3581
|
mergeData: args.mergeData,
|
|
3508
3582
|
});
|
|
3509
3583
|
return { content: [{ type: "text", text: `Communication sent to ${args.recipientEmail}.` }] };
|
|
@@ -4052,18 +4126,40 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
4052
4126
|
},
|
|
4053
4127
|
},
|
|
4054
4128
|
set_journey_trigger: {
|
|
4055
|
-
description: `${APEX} — Set the entry trigger (the event that enrolls subjects) on a draft journey's trigger step. Pass
|
|
4129
|
+
description: `${APEX} — Set the entry trigger (the event that enrolls subjects) on a draft journey's trigger step. Pass event_name (the Apex Spec event, e.g. user_signed_up). A trig-* contract id is storage-only — prefer the Spec name.`,
|
|
4056
4130
|
schema: z.object({
|
|
4057
4131
|
journeyId: z.string(),
|
|
4058
|
-
|
|
4132
|
+
eventName: z.string().optional().describe("Apex Spec event name that enrolls subjects (e.g. user_signed_up). Preferred."),
|
|
4133
|
+
triggerContractId: z.string().optional().describe("Storage id from list_trigger_contracts. Prefer event_name."),
|
|
4059
4134
|
}),
|
|
4060
|
-
handler: async ({ journeyId, triggerContractId }) => {
|
|
4135
|
+
handler: async ({ journeyId, eventName, triggerContractId, }) => {
|
|
4061
4136
|
try {
|
|
4062
|
-
// Validate the contract id up front so a typo doesn't silently set a
|
|
4063
|
-
// dead trigger that only fails at publish — enumerate valid options.
|
|
4064
4137
|
const cdata = await apiGet("/api/journeys/trigger-contracts");
|
|
4065
4138
|
const contracts = cdata.contracts ?? [];
|
|
4066
|
-
|
|
4139
|
+
let resolvedId = triggerContractId;
|
|
4140
|
+
if (eventName) {
|
|
4141
|
+
const match = contracts.find((c) => c.eventName === eventName);
|
|
4142
|
+
if (match) {
|
|
4143
|
+
resolvedId = match.id;
|
|
4144
|
+
}
|
|
4145
|
+
else {
|
|
4146
|
+
const created = await apiPost("/api/journey-triggers", {
|
|
4147
|
+
eventName,
|
|
4148
|
+
fieldMappings: { end_user_id: "endUserId" },
|
|
4149
|
+
recipientInitiated: true,
|
|
4150
|
+
});
|
|
4151
|
+
resolvedId = created.id;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
if (!resolvedId) {
|
|
4155
|
+
return {
|
|
4156
|
+
content: [{ type: "text", text: `${APEX} Pass event_name (preferred) or triggerContractId.` }],
|
|
4157
|
+
isError: true,
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
if (!eventName &&
|
|
4161
|
+
contracts.length > 0 &&
|
|
4162
|
+
!contracts.some((c) => c.id === resolvedId)) {
|
|
4067
4163
|
const opts = contracts
|
|
4068
4164
|
.map((c) => `${c.id}${c.eventName ? ` (${c.eventName})` : ""}`)
|
|
4069
4165
|
.join(", ");
|
|
@@ -4071,18 +4167,19 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
4071
4167
|
content: [
|
|
4072
4168
|
{
|
|
4073
4169
|
type: "text",
|
|
4074
|
-
text: `${APEX} Unknown triggerContractId "${
|
|
4170
|
+
text: `${APEX} Unknown triggerContractId "${resolvedId}". Valid options: ${opts}`,
|
|
4075
4171
|
},
|
|
4076
4172
|
],
|
|
4077
4173
|
isError: true,
|
|
4078
4174
|
};
|
|
4079
4175
|
}
|
|
4080
4176
|
const j = await apiGet(`/api/journeys/${encodeURIComponent(journeyId)}`);
|
|
4081
|
-
const steps = (j.steps ?? []).map((s) => s.type === "trigger" ? { ...s, trigger: { type: "event", triggerContractId } } : s);
|
|
4177
|
+
const steps = (j.steps ?? []).map((s) => s.type === "trigger" ? { ...s, trigger: { type: "event", triggerContractId: resolvedId } } : s);
|
|
4082
4178
|
const updated = await apiPatch(`/api/journeys/${encodeURIComponent(journeyId)}`, { steps });
|
|
4083
4179
|
if (!tenantOk(updated.workspaceKey))
|
|
4084
4180
|
return tenantMismatch();
|
|
4085
|
-
|
|
4181
|
+
const label = eventName ? `${eventName} (${resolvedId})` : resolvedId;
|
|
4182
|
+
return { content: [{ type: "text", text: `${APEX} Trigger set to ${label} on journey ${journeyId}. View it: ${journeyLink(journeyId)}` }] };
|
|
4086
4183
|
}
|
|
4087
4184
|
catch (err) {
|
|
4088
4185
|
return { content: [{ type: "text", text: `${APEX} ${errMsg(err)}` }], isError: true };
|
|
@@ -4831,7 +4928,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
4831
4928
|
apex_test_event: {
|
|
4832
4929
|
description: `${APEX} — Send a test event through the public ingestion endpoint and confirm it was accepted. The fastest way to verify event tracking works after installing the SDK.`,
|
|
4833
4930
|
schema: z.object({
|
|
4834
|
-
eventName: z.string().describe("Event name, e.g.
|
|
4931
|
+
eventName: z.string().describe("Event name, e.g. user_signed_up"),
|
|
4835
4932
|
attributes: z.record(z.unknown()).optional().describe("Event attributes"),
|
|
4836
4933
|
visitorId: z.string().optional().describe("Visitor id (default a test id)"),
|
|
4837
4934
|
}),
|
|
@@ -5180,7 +5277,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5180
5277
|
// owned-event matcher managed on /dashboard/conversions). A Target is an
|
|
5181
5278
|
// aggregate KPI number to hit by a date. "Goal" is retired as a noun.
|
|
5182
5279
|
list_conversions: {
|
|
5183
|
-
description: `${APEX} — List the workspace's Conversions (
|
|
5280
|
+
description: `${APEX} — List the workspace's Conversions (owned-event definitions of valued actions, e.g. "Purchase", "Signup"). Call this when the user asks what conversions are defined. The starred primary is a label for that Conversion — it does not pick the funnel stages and it is not the experiment default. Experiments name their own metric; the funnel follows the workspace conversion model.`,
|
|
5184
5281
|
schema: z.object({}),
|
|
5185
5282
|
handler: async () => {
|
|
5186
5283
|
const conversions = await apiGet("/api/conversion-goals");
|
|
@@ -5207,7 +5304,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5207
5304
|
},
|
|
5208
5305
|
},
|
|
5209
5306
|
create_conversion: {
|
|
5210
|
-
description: `${APEX} — Define a new Conversion (an owned-event definition of a valued action). Most conversions are a custom_event with an eventName (e.g. "
|
|
5307
|
+
description: `${APEX} — Define a new Conversion (an owned-event definition of a valued action). Most conversions are a custom_event with an eventName (e.g. "order_placed", "user_signed_up") matching an Apex Spec event you send via track. isPrimary stars this Conversion as a label — it does not pick experiment defaults or drive the funnel.`,
|
|
5211
5308
|
schema: z.object({
|
|
5212
5309
|
name: z.string().describe('Human label, e.g. "Purchase" or "Signup"'),
|
|
5213
5310
|
type: z
|
|
@@ -5259,7 +5356,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
5259
5356
|
},
|
|
5260
5357
|
},
|
|
5261
5358
|
set_primary_conversion: {
|
|
5262
|
-
description: `${APEX} — Mark a Conversion as the primary
|
|
5359
|
+
description: `${APEX} — Mark a Conversion as the starred primary. That star labels the Conversion — it does not set experiment defaults or drive the funnel. Experiments name their own metric. Pass the conversion id from list_conversions.`,
|
|
5263
5360
|
schema: z.object({
|
|
5264
5361
|
conversionId: z.string().describe("The conversion id to promote (from list_conversions)."),
|
|
5265
5362
|
}),
|