@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-inc/mcp-server",
3
- "version": "0.27.0",
3
+ "version": "0.27.2",
4
4
  "description": "MCP server for Apex \u2014 currently in private beta.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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 | Edit freely |
49
- | `no_winner` | An experiment ran and settled without a winner | Edit freely; worth re-testing |
50
- | `winner` | Won an experiment, unchanged since | Needs `intent` |
51
- | `winner_edited` | Won, then was changed | Needs `intent` |
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
- (user_signup, password_reset_requested). 'marketing' is consent-gated.
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("user_signup") call to add
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.** For public web URLs, `create_experiment` auto-captures control + variant screenshots and shows them on the dashboard experiment card + detail gallery. For localhost / auth-gated / mobile screens, capture the image yourself and call `attach_experiment_asset({ experimentId, variantKey, imageBase64 })`.
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
- - **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, not localhost. Testers on a named Beta site count in the winner. Localhost does not. 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
+ - **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("signup_completed", { method: "email", plan: "free" });
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**: `signup_completed`, `feature_used`, `invoice_paid`
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