@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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bind experiment target_url to hosts the ACTIVE workspace actually owns.
|
|
3
|
+
*
|
|
4
|
+
* A merchant with two workspaces (e.g. apex-staging vs the stealth apex.inc
|
|
5
|
+
* property) must not silently land a draft on the sibling's site. Agents
|
|
6
|
+
* guess brand domains; the catalog is the source of truth.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hostnameOf(raw: string): string | null;
|
|
9
|
+
export declare function collectHosts(input: {
|
|
10
|
+
analyticsHosts?: Array<{
|
|
11
|
+
hostname?: string;
|
|
12
|
+
} | string>;
|
|
13
|
+
namedSites?: string[];
|
|
14
|
+
workspaceUrl?: string | null;
|
|
15
|
+
apexUrl?: string | null;
|
|
16
|
+
}): string[];
|
|
17
|
+
export declare function evaluateTargetUrl(targetUrl: string, allowedHosts: string[]): {
|
|
18
|
+
ok: true;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=workspace-target-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-target-url.d.ts","sourceRoot":"","sources":["../src/workspace-target-url.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOrD;AAUD,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,cAAc,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,CAAC;IACvD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GAAG,MAAM,EAAE,CASX;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EAAE,GACrB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAwB/C"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bind experiment target_url to hosts the ACTIVE workspace actually owns.
|
|
3
|
+
*
|
|
4
|
+
* A merchant with two workspaces (e.g. apex-staging vs the stealth apex.inc
|
|
5
|
+
* property) must not silently land a draft on the sibling's site. Agents
|
|
6
|
+
* guess brand domains; the catalog is the source of truth.
|
|
7
|
+
*/
|
|
8
|
+
export function hostnameOf(raw) {
|
|
9
|
+
try {
|
|
10
|
+
const withScheme = /:\/\//.test(raw) ? raw : `https://${raw}`;
|
|
11
|
+
return new URL(withScheme).hostname.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function addHost(hosts, raw) {
|
|
18
|
+
if (!raw)
|
|
19
|
+
return;
|
|
20
|
+
const host = hostnameOf(raw);
|
|
21
|
+
if (!host)
|
|
22
|
+
return;
|
|
23
|
+
if (host === "localhost" || host === "127.0.0.1")
|
|
24
|
+
return;
|
|
25
|
+
hosts.add(host);
|
|
26
|
+
}
|
|
27
|
+
export function collectHosts(input) {
|
|
28
|
+
const hosts = new Set();
|
|
29
|
+
for (const item of input.analyticsHosts ?? []) {
|
|
30
|
+
addHost(hosts, typeof item === "string" ? item : item.hostname);
|
|
31
|
+
}
|
|
32
|
+
for (const site of input.namedSites ?? [])
|
|
33
|
+
addHost(hosts, site);
|
|
34
|
+
addHost(hosts, input.workspaceUrl);
|
|
35
|
+
addHost(hosts, input.apexUrl);
|
|
36
|
+
return [...hosts];
|
|
37
|
+
}
|
|
38
|
+
export function evaluateTargetUrl(targetUrl, allowedHosts) {
|
|
39
|
+
const host = hostnameOf(targetUrl);
|
|
40
|
+
if (!host) {
|
|
41
|
+
return { ok: false, message: `target_url is not a valid URL: ${targetUrl}` };
|
|
42
|
+
}
|
|
43
|
+
if (host === "localhost" || host === "127.0.0.1") {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
message: "target_url must be a named site on the active workspace, not localhost. Capture localhost screenshots with attach_experiment_asset; the experiment itself runs on a hosted site.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (allowedHosts.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
message: "No sites are named on the active workspace yet. Call list_workspace_environments, then set_workspace_environments with the real host (or use the workspace's own site). Do not guess a brand domain or a sibling workspace's site.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (allowedHosts.includes(host))
|
|
56
|
+
return { ok: true };
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: `target_url host "${host}" is not a site on the active workspace. Known hosts: ${allowedHosts.join(", ")}. Do not guess a brand domain or a sibling workspace's site.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=workspace-target-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-target-url.js","sourceRoot":"","sources":["../src/workspace-target-url.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC;QAC9D,OAAO,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,KAAkB,EAAE,GAAmB;IACtD,IAAI,CAAC,GAAG;QAAE,OAAO;IACjB,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,IAAI;QAAE,OAAO;IAClB,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO;IACzD,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAK5B;IACC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,cAAc,IAAI,EAAE,EAAE,CAAC;QAC9C,OAAO,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,UAAU,IAAI,EAAE;QAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAChE,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IACnC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,SAAiB,EACjB,YAAsB;IAEtB,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,kCAAkC,SAAS,EAAE,EAAE,CAAC;IAC/E,CAAC;IACD,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;QACjD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EACL,kLAAkL;SACrL,CAAC;IACJ,CAAC;IACD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EACL,oOAAoO;SACvO,CAAC;IACJ,CAAC;IACD,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACrD,OAAO;QACL,EAAE,EAAE,KAAK;QACT,OAAO,EAAE,oBAAoB,IAAI,yDAAyD,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,8DAA8D;KAChL,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -45,14 +45,16 @@ experience, so Apex won't let you do it by accident.
|
|
|
45
45
|
|
|
46
46
|
| `controlState` | What it means | Editing it |
|
|
47
47
|
|---|---|---|
|
|
48
|
-
| `none` | Never experimented on |
|
|
49
|
-
| `no_winner` | An experiment ran and settled without a winner |
|
|
50
|
-
| `winner` | Won an experiment, unchanged since |
|
|
51
|
-
| `winner_edited` | Won, then was changed |
|
|
48
|
+
| `none` | Never experimented on | Editable. If a journey is sending it, Publish still asks replace vs compete |
|
|
49
|
+
| `no_winner` | An experiment ran and settled without a winner | Editable. Same publish question if it's sending |
|
|
50
|
+
| `winner` | Won an experiment, unchanged since | Champion. Control is read-only. Add a variant to compete; Replace overwrites |
|
|
51
|
+
| `winner_edited` | Won, then was changed | Still a champion. Same as winner |
|
|
52
52
|
| `in_experiment` | Frozen — an experiment is measuring it right now | Blocked; see below |
|
|
53
53
|
|
|
54
54
|
A comm that's live in a published journey also needs `intent`, whatever its
|
|
55
|
-
`controlState`.
|
|
55
|
+
`controlState`. Do not rewrite a champion to start a test. Use
|
|
56
|
+
`add_communication_variant`, then `publish_communication` with `intent: "compete"`.
|
|
57
|
+
Rewriting what's sending is `replace`.
|
|
56
58
|
|
|
57
59
|
### The two intents
|
|
58
60
|
|
|
@@ -135,7 +137,7 @@ Read these resources before suggesting communications-related actions to underst
|
|
|
135
137
|
1. list_communications(pipeline="transactional", channel="email")
|
|
136
138
|
→ narrow to the pipeline + channel the step needs. 'transactional'
|
|
137
139
|
bypasses opt-out and is ONLY valid after a recipient-initiated trigger
|
|
138
|
-
(
|
|
140
|
+
(user_signed_up, password_reset_requested). 'marketing' is consent-gated.
|
|
139
141
|
2. Read each row's subject + one-line body preview to disambiguate similar
|
|
140
142
|
titles — you do NOT need to preview_communication every candidate.
|
|
141
143
|
3. Pick by `id` (+ its version) and attach it to the send step.
|
|
@@ -151,7 +153,7 @@ hard-block**. For those journeys, filter to `pipeline="marketing"`.
|
|
|
151
153
|
2. generate_communications with the welcome entry ID
|
|
152
154
|
3. edit_communication to customize copy
|
|
153
155
|
4. send_test_communication to preview
|
|
154
|
-
5. Show user the apex.track("
|
|
156
|
+
5. Show user the apex.track("user_signed_up") call to add
|
|
155
157
|
```
|
|
156
158
|
|
|
157
159
|
### "My churn rate is high"
|
|
@@ -69,10 +69,12 @@ Interactive flows: MCP prompts `new-experiment` and `experiment-review` orchestr
|
|
|
69
69
|
|
|
70
70
|
## Screenshots, exposure & the wiring gate
|
|
71
71
|
|
|
72
|
-
- **Screenshots are first-class.**
|
|
72
|
+
- **Screenshots are first-class.** SDK / Cursor experiments author the variant in local code. Apex does **not** auto-capture a public URL on create — that page does not have the unpublished variant, and a guessed host (a sibling workspace's brand domain) shows the wrong site on both arms. Capture both arms on localhost with `?_apex_preview=control|variant_a&_apex_exp=<id>`, then `attach_experiment_asset({ experimentId, variantKey, imageBase64 })`. After deploy, `recapture_experiment_screenshots` refreshes the live host. Snippet/DOM experiments created in the dashboard still auto-capture a public URL whose variant already lives there.
|
|
73
|
+
- **Target URL is this workspace.** `target_url` must be a host named on the **active** workspace (`list_workspace_environments` + the workspace site). If the merchant already named a workspace, switch to it — do not ask which workspace again. If they have multiple hosts on that workspace, ask which URL. Never guess `apex.inc` or any other workspace's domain.
|
|
73
74
|
- **Mobile/Capacitor experiments capture on-device.** When the experiment runs on an authed in-app screen (servers can't reach it), tell the dev to call `Apex.captureVariantScreenshot({ experimentId, variantKey })` on the variant's screen, keyed to the resolved variant in a `useEffect`, in a debug build (`Apex.initialize({ ..., debug: true })`). It no-ops in production and lands the shot on the dashboard card + gallery like web/agent captures.
|
|
74
75
|
- **Exposure auto-fires.** When a variant resolves via `useApexVariant` (web) or `Apex.getVariant()` (mobile), the SDK fires the canonical `experiment_exposure` event — the denominator for results. You don't fire it manually.
|
|
75
|
-
- **
|
|
76
|
+
- **Preview on Next.js.** `useApexVariant` cannot read `window` during SSR. Pass the request search string (`useApexVariant(id, { search })` from `searchParams`, or `<ApexProvider search>`) so the first HTML matches `_apex_preview`. Without it the server renders control and the arm only appears after hydrate.
|
|
77
|
+
- **Don't launch a dead experiment.** `activate_experiment` is gated on `verify_experiment_wiring`. Code-wired (SDK-hook) web experiments are safe to activate before their code ships: the server holds them in `pending_deployment` ("Waiting on deploy"). The 14-day clock starts at the first **recorded** visit (a tester on a named Beta site, or a live visit) — not merge, not activate. Testers on a named Beta site count in the winner. Localhost/dev is recorded as Dev and does not start the window or decide the winner. Assign with no Origin is Unclassified (no enrollment). Backend assign must send `environment` or `x-apex-web-environment`. Non-SDK experiments with unwired arms are refused (pass `force: true` to override). Run `verify_experiment_wiring({ experimentId })` after the deploy lands to confirm both arms are live.
|
|
76
78
|
|
|
77
79
|
### Journey-arm experiments (any surface)
|
|
78
80
|
|
|
@@ -146,6 +148,8 @@ Match Apex UI terminology in user-facing copy: e.g. **Adaptive traffic allocatio
|
|
|
146
148
|
|
|
147
149
|
## Common mistakes
|
|
148
150
|
|
|
151
|
+
- Guessing `target_url` from the brand name or a sibling workspace (e.g. `apex.inc` while the active workspace is `apex-staging`). Use `list_workspace_environments` + the workspace site.
|
|
152
|
+
- Declaring an SDK / Cursor draft ready from hosted screenshots of a public URL. The variant is local until deploy — attach localhost shots with `attach_experiment_asset`.
|
|
149
153
|
- Starting an experiment without a **clear primary metric** tied to the belief.
|
|
150
154
|
- Using different visitor ids for the same user across channels (breaks consistent assignment).
|
|
151
155
|
- Changing variant implementation mid-flight without versioning (invalidates analysis).
|
|
@@ -27,7 +27,7 @@ Events are batched and sent to `POST /api/events` automatically. Call `flush()`
|
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
29
|
// On signup
|
|
30
|
-
track("
|
|
30
|
+
track("user_signed_up", { method: "email", plan: "free" });
|
|
31
31
|
identify(user.id, {
|
|
32
32
|
email: user.email,
|
|
33
33
|
name: user.name,
|
|
@@ -117,7 +117,7 @@ For agent-driven testing through MCP, use the **`send_server_event`** tool — s
|
|
|
117
117
|
|
|
118
118
|
## Event naming conventions
|
|
119
119
|
|
|
120
|
-
- **snake_case**: `
|
|
120
|
+
- **snake_case**: `user_signed_up`, `feature_used`, `invoice_paid`
|
|
121
121
|
- Context in **properties**: `track("ui_action", { action: "click", name: "Start trial", location: "pricing" })`
|
|
122
122
|
- For experiments, include `experimentId` and `variant` in properties
|
|
123
123
|
- One primary metric per experiment; secondary metrics as properties
|