@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-inc/mcp-server",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "MCP server for Apex \u2014 currently in private beta.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,6 +17,8 @@ The Adoption Engine tracks whether each end-user has adopted each feature, nudge
17
17
  - **Celebration** (fires once on adoption): congratulates the user the moment they *do* reach the milestone, and tells them what's next. Scaffold both legs in one call with `compose_adoption_milestone` + `scaffold: "both"` (this mirrors the dashboard sentence on-ramp).
18
18
  - Milestones can be given a sequence **order** (which one accounts reach first). Ordering the full set unlocks the account-view "Path" funnel (a step-by-step drop-off chart). Unordered milestones are treated as independent (order-agnostic coverage).
19
19
 
20
+ Name live hosts (`list_workspace_environments` / `set_workspace_environments` or Foundations) before you trust Production adoption numbers. Unmatched hosts stay Unclassified.
21
+
20
22
  ## When to Activate
21
23
 
22
24
  - "Nudge users who signed up but never ran their first report."
@@ -7,6 +7,8 @@ description: Help users set up and manage lifecycle communications through Apex
7
7
 
8
8
  Use these tools to help users build a communication program that sends the right message at the right time — without building email infrastructure from scratch.
9
9
 
10
+ One communication object. `audience_bucket` on create/edit is who gets it (`production` default, or `beta` / `dev`). Never All. Never Unclassified. Same email on TestFlight and the store is two contacts — a live send uses the live row only. Do not create a staging copy.
11
+
10
12
  ## When to Activate
11
13
 
12
14
  - User asks about setting up emails, notifications, or onboarding messages
@@ -54,19 +56,19 @@ A comm that's live in a published journey also needs `intent`, whatever its
54
56
 
55
57
  ### The two intents
56
58
 
57
- - **`intent: "compete"`** — *Add as a variant.* Your edit runs as an experiment
58
- against the current content. Nothing changes for recipients yet; Apex measures
59
- which one is better and tells you.
60
- - **`intent: "replace"`** — *Update for everyone.* Your edit becomes what
61
- everyone receives. Any prior win is superseded.
59
+ - **`intent: "compete"`** — *Start an experiment.* What's sending now stays
60
+ live. Publish creates a filled-in draft. Then `activate_experiment` after
61
+ the user confirms.
62
+ - **`intent: "replace"`** — *Replace what's sending.* Everyone gets this
63
+ version on the next send. No experiment row.
62
64
 
63
65
  ### When in doubt, compete
64
66
 
65
67
  This is the default, and it isn't a coin flip. The two mistakes are not
66
68
  symmetric:
67
69
 
68
- - Guessing `compete` when the user wanted `replace` costs **time** — they see
69
- an experiment start and can promote the winner immediately.
70
+ - Guessing `compete` when the user wanted `replace` costs **time** — they have
71
+ a draft to activate, or they can publish again with replace.
70
72
  - Guessing `replace` when the user wanted `compete` **destroys a measured
71
73
  result permanently**. There's no undo for evidence.
72
74
 
@@ -74,9 +76,9 @@ So: if the user explicitly said "just change it for everyone" / "replace the
74
76
  copy" / "roll this out," send `replace`. Otherwise send `compete`, and say what
75
77
  you did:
76
78
 
77
- > "That comm's control won an experiment, so I added your new copy as a variant
78
- > and started an experiment against it rather than overwriting the winner. Want
79
- > me to make it the control for everyone instead?"
79
+ > "That comm's control won an experiment, so I published your copy as a
80
+ > competing version and opened a filled-in draft. Want me to activate it, or
81
+ > make this the letter everyone gets instead?"
80
82
 
81
83
  ### Don't force through a running experiment
82
84
 
@@ -95,9 +97,10 @@ re-issue. Do not retry the identical call.
95
97
  ### Running experiments
96
98
 
97
99
  1. **`list_communications`** — find the communication to test.
98
- 2. **`create_comm_experiment`** — set up the A/B test with variants.
99
- 3. The experiment runs automatically via Thompson Sampling.
100
- 4. Check results via `list_communications` or the dashboard.
100
+ 2. **`edit_communication` / `add_communication_variant`** — write the versions.
101
+ 3. **`publish_communication`** with `intent: "compete"` creates a prefilled draft. Does not start the test.
102
+ 4. **`activate_experiment`** begins the test on the next send.
103
+ 5. Check results via `get_results` / `list_experiments`.
101
104
 
102
105
  ### Manual sends
103
106
 
@@ -155,15 +158,16 @@ hard-block**. For those journeys, filter to `pipeline="marketing"`.
155
158
  ```
156
159
  1. recommend_communications → look for retention/winback entries
157
160
  2. generate_communications for churn-related comms
158
- 3. create_comm_experiment to A/B test the messaging
161
+ 3. add_communication_variant, publish_communication (compete), then activate_experiment
159
162
  4. get_event_taxonomy to see what churn signals to track
160
163
  ```
161
164
 
162
165
  ### "How do I A/B test my emails?"
163
166
  ```
164
167
  1. list_communications → find the target
165
- 2. create_comm_experiment with subject or body split
166
- 3. Explain Thompson Sampling: traffic shifts automatically toward the winner
168
+ 2. add_communication_variant (or edit the second column)
169
+ 3. publish_communication intent=compete filled-in draft
170
+ 4. activate_experiment after the user confirms
167
171
  ```
168
172
 
169
173
  ### "Set up cart abandonment recovery"
@@ -180,6 +184,14 @@ Cart-recovery is a Journey (multi-step sequence with a wait) — not a single co
180
184
 
181
185
  The key is that the **journey** owns the wait, the branching on cart state, and the exit semantics. The **communications** are just the payloads the journey fires.
182
186
 
187
+ ## Writing copy
188
+
189
+ When you draft or edit a letter:
190
+
191
+ - Use contractions (we'll, it's, don't). People talk that way.
192
+ - If the letter has pictures of variants, put them right after the title. Do not bury them under a wall of copy.
193
+ - Write what the recipient needs: their experiment, their metric, their workspace. Do not write how the composer or a block works. The button label is enough.
194
+
183
195
  ## Theme vs Template
184
196
 
185
197
  **Template** is the job: event, attributes, slots, copy. Activate from the library. There is no extra theme step.
@@ -72,7 +72,7 @@ Interactive flows: MCP prompts `new-experiment` and `experiment-review` orchestr
72
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 })`.
73
73
  - **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
74
  - **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") and starts them automatically at the first `experiment_exposure`. 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.
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
76
 
77
77
  ### Journey-arm experiments (any surface)
78
78
 
@@ -95,18 +95,28 @@ Do NOT describe this as champion versus challenger, and do not try to run "the o
95
95
 
96
96
  ### Starting an experiment from the communication side
97
97
 
98
- `publish_communication` is the other way an experiment starts. Publishing a communication that has variants starts the experiment measuring them against the control, on the published journey that sends it. Preconditions, each reported rather than guessed:
98
+ Write the letter with `edit_communication` / `add_communication_variant`. Then:
99
+
100
+ 1. `publish_communication` with `intent: "compete"` (or when variants already exist). This creates a **prefilled draft** on the same experiment page as web tests. It does **not** start the test.
101
+ 2. Show the user the draft. They can edit window, hypothesis, belief.
102
+ 3. `activate_experiment` starts it. People on that journey get control or the variant on the next send. Screenshots are captured on activate — do not call `attach_experiment_asset` for a letter unless capture failed.
103
+
104
+ `intent: "replace"` publishes the letter for everyone. Leftover variant columns leave. No experiment row.
105
+
106
+ Preconditions for a draft, each reported rather than guessed:
99
107
 
100
108
  - **A host** — some published journey must send the communication. `no_host` means there is no traffic to measure.
101
109
  - **A goal** — the host step's own goal event, or the journey's. `no_goal` means there is nothing to optimize toward; set the journey's goal event first.
102
110
  - **One host** — 409 `ambiguous_experiment_host` lists the candidates when several journeys send it. Ask the user which, then re-issue with `host_journey_id`.
103
111
 
112
+ When in doubt, compete. The cheap mistake is now "they have a draft to activate," not "they already have a live test."
113
+
104
114
  ### Editing a control that won
105
115
 
106
116
  `edit_communication` and `publish_communication` both take `intent` when the control is protected (it won an experiment, or a published journey is sending it):
107
117
 
108
- - `"compete"` — the current content keeps sending and your change runs against it as an experiment. **This is the default when the user hasn't said otherwise**; it is recoverable.
109
- - `"replace"` — your change becomes what everyone receives, superseding the prior win. This permanently discards a measured result.
118
+ - `"compete"` — the current content keeps sending. Publish opens a filled-in draft. Then `activate_experiment`. **This is the default when the user hasn't said otherwise**; it is recoverable.
119
+ - `"replace"` — your change becomes what everyone receives, superseding the prior win. No experiment row.
110
120
 
111
121
  Omitting `intent` when it's required returns 409 `intent_required` naming both. Do not pick `replace` to clear the error.
112
122
 
@@ -160,7 +170,7 @@ Editability is gated by lifecycle, keyed to DATA:
160
170
 
161
171
  - **Draft, no exposures yet:** fully editable. Use `update_experiment({ experimentId, ... })` to fix a mislabeled surface, rename, or correct the hypothesis/metric/guardrail. This is where typo and surface fixes belong.
162
172
  - **Running, or any exposures recorded:** the pre-registration (hypothesis, metric, guardrails) and bucketing (surface, variants) FREEZE. `update_experiment` returns `experiment_locked` with the reason — that's intentional, so results stay valid. Do not fight it.
163
- - **After freeze:** call `fork_experiment({ experimentId })`. It duplicates the design into a fresh draft (no data carries over, lineage recorded), which you then edit and activate.
173
+ - **After freeze:** call `fork_experiment({ experimentId })`. It duplicates the design into a fresh draft (no data carries over, lineage recorded). A copy gets a new id — replace `useApexVariant("<old id>")` with the new id, deploy, then activate. Preview still works via `_apex_exp`; a normal visit shows control until the new id is shipped.
164
174
 
165
175
  Surface specifically: `create_experiment` auto-detects mobile vs web from repo signals (capacitor.config.* / @apex-inc/capacitor-plugin) and the workspace's registered data sources when you omit `surface`; if a Capacitor app got mislabeled `web`, fix it on the draft with `update_experiment({ experiment_id, surface: "mobile" })` before the first exposure, or fork if it's already running.
166
176
 
@@ -20,7 +20,7 @@ Apex ingests **events** and **identities** so experiments, attribution, and inte
20
20
  ## Event naming
21
21
 
22
22
  - Use **snake_case** or **dot-separated** consistent verbs: `page_viewed`, `signup_started`, `trial_started`, `notification_clicked`.
23
- - Include **context** in properties, not in the event name explosion: `track("button_clicked", { surface: "pricing", label: "start_trial" })` vs dozens of `pricing_start_trial_clicked` events.
23
+ - Include **context** in properties, not in the event name explosion: `track("ui_action", { action: "click", name: "Start trial", location: "pricing" })` vs dozens of `pricing_start_trial_clicked` events.
24
24
  - For experiments, include **`experimentId`** and **`variant`** when the event is relevant to that exposure.
25
25
 
26
26
  ## Canonical attributes (the Schema)
@@ -133,18 +133,18 @@ curl "https://app.apex.inc/api/v1/people/$(printf %s 'user@example.com' | jq -rR
133
133
 
134
134
  ## Environment separation — same key everywhere
135
135
 
136
- Apex auto-detects production / preview / localhost from the request hostname. **Use the same `workspaceKey` on staging and production.** Preview deploys and localhost are billed at $0 and hidden from the default dashboard.
136
+ Use the **same `workspaceKey`** in Xcode, TestFlight, and the store. The phone files the build. Do not guess `staging.` / `qa.` / `dev.` hosts and do not set `environment` from `NODE_ENV`.
137
137
 
138
- Only pass the explicit `environment` override when your staging URL doesn't match the standard preview patterns (`*.vercel.app`, `*.netlify.app`, `staging.*`, `preview.*`, etc.):
138
+ Four buckets:
139
139
 
140
- ```typescript
141
- init({
142
- workspaceKey: "apx_live_...",
143
- environment: process.env.NODE_ENV === "production" ? "production" : "preview",
144
- });
145
- ```
140
+ - **Production** — hosts the merchant named live, plus App Store / Play production.
141
+ - **Beta** — TestFlight, Play closed tracks, vendor preview hosts Apex can prove, plus hosts they named as Beta.
142
+ - **Dev** — Xcode Run, Gradle DEBUG, sideloaded, localhost.
143
+ - **Unclassified** we could not prove a bucket. This is never Production.
144
+
145
+ Every environment bills. Client-sent `environment` is dashboard slicing, never money truth. Do not create a second workspace. Name live hosts with `list_workspace_environments` / `set_workspace_environments` (or Foundations) before you trust the Production slice. Unmatched merchant hosts stay Unclassified. To clear the Unclassified Notice, name the host — do not add a second key and do not set `environment: production` on a server purchase to "fix" dollars. See `/docs/mobile/test-vs-production`.
146
146
 
147
- Do not recommend setting up a second workspace for staging. See `/docs/mobile/test-vs-production` for the full audience model.
147
+ Server events may send `environment` / `release_channel`. Missing stays Unclassified. Required for honesty, not required to ingest.
148
148
 
149
149
  ## Data Sources & unified analytics
150
150
 
@@ -118,7 +118,7 @@ For agent-driven testing through MCP, use the **`send_server_event`** tool — s
118
118
  ## Event naming conventions
119
119
 
120
120
  - **snake_case**: `signup_completed`, `feature_used`, `invoice_paid`
121
- - Context in **properties**: `track("button_clicked", { surface: "pricing", label: "start_trial" })`
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
124
124
 
@@ -221,16 +221,17 @@ Use Gradle `buildConfigField` to inject the channel per build variant so the val
221
221
 
222
222
  ### Billing implications
223
223
 
224
- - **Dev** (Xcode-debug, Gradle-debug, sideloaded): always **free**.
225
- - **Beta** (TestFlight, Play internal/closed): always **free**.
226
- - **Production** (App Store, Play production): **billable**.
227
- - **Unknown** (older SDK without detection): billable, but the dashboard surfaces a fix-it nudge with an SDK upgrade prompt.
224
+ Every environment bills — Dev, Beta, Production, and Unclassified. The monthly-reset free tier is the relief valve, not an environment exemption. `testMode` is a dry-run flag, not a free TestFlight bucket.
228
225
 
229
- No retroactive credits if you misclassify a build, the fix takes effect for future events.
226
+ Naming a site reclassifies events from that hostname immediately. Stored identity is not rewritten. Do not set `environment: production` to "fix" Unclassified money.
227
+
228
+ ### Name live sites before you trust Production
229
+
230
+ Call `list_workspace_environments` (it also returns `unclassified_hosts_seen`) then `set_workspace_environments` with `production_hosts` and optional `beta_hosts`. Apex will not guess `staging.`. Naming reclassifies events from those sites at read time. Stripe / server purchase recipes must not set `environment: production` to "fix" Unclassified money.
230
231
 
231
232
  ### When NOT to recommend a separate workspace
232
233
 
233
- Some MMP vendors require two workspace keys (sandbox + live). Apex does not. If a merchant asks "should I use a different workspace for beta?" or "do I need a sandbox key?", the answer is **no**: point to `/docs/mobile/test-vs-production` and walk them through the **Build environments** card in Settings Mobile apps.
234
+ Some MMP vendors require two workspace keys (sandbox + live). Apex does not. If a merchant asks "should I use a different workspace for beta?" or "do I need a sandbox key?", the answer is **no**: point to `/docs/mobile/test-vs-production` and walk them through Foundations Tell Apex which hosts are live.
234
235
 
235
236
  ## Related
236
237
 
@@ -94,6 +94,12 @@ find where each business truth already lives and instrument it there.
94
94
  - Revenue events MUST carry `value` + `currency`, and a stable
95
95
  external id when one exists (order id, invoice id) — dedupe depends
96
96
  on it.
97
+ - Do **not** invent one event per button. In-product chrome (named
98
+ buttons, widget close/minimize/move, nav picks) is `ui_action` with
99
+ `action` + `name` + optional `location`. Outcomes stay semantic:
100
+ `integration_selected`, `setup_step_continued`, `integration_connected`.
101
+ `click` is the snippet's outbound-link auto-event; do not use it for
102
+ dashboard buttons.
97
103
 
98
104
  ## Vertical event sets (Vertical Widget Packs, 2026-07)
99
105