@apex-inc/mcp-server 0.22.1 → 0.23.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.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "MCP server for Apex — currently in private beta.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,11 +25,72 @@ Use these tools to help users build a communication program that sends the right
25
25
 
26
26
  ### Editing and testing
27
27
 
28
- 1. **`list_communications`** — see what exists.
29
- 2. **`edit_communication`** — update subject, body, CTA, channels, or status.
28
+ 1. **`list_communications`** — see what exists, and check `controlState` before you edit.
29
+ 2. **`edit_communication`** — update subject, body, CTA, channels, or status. Pass `intent` when the control is protected (see below).
30
30
  3. **`preview_communication`** — render the email with the user's brand.
31
31
  4. **`send_test_communication`** — send a test to verify it looks right.
32
32
 
33
+ ## Replace versus compete — the one rule that protects results
34
+
35
+ A communication's **Control** is what everyone receives. It is often not just
36
+ "the default copy": it can be the **winner** of an experiment Apex already ran,
37
+ and it can be what a published journey is sending to real people right now.
38
+ Overwriting it silently throws away either a measured result or a live
39
+ experience, so Apex won't let you do it by accident.
40
+
41
+ `list_communications` returns `controlState` for every comm:
42
+
43
+ | `controlState` | What it means | Editing it |
44
+ |---|---|---|
45
+ | `none` | Never experimented on | Edit freely |
46
+ | `no_winner` | An experiment ran and settled without a winner | Edit freely; worth re-testing |
47
+ | `winner` | Won an experiment, unchanged since | Needs `intent` |
48
+ | `winner_edited` | Won, then was changed | Needs `intent` |
49
+ | `in_experiment` | Frozen — an experiment is measuring it right now | Blocked; see below |
50
+
51
+ A comm that's live in a published journey also needs `intent`, whatever its
52
+ `controlState`.
53
+
54
+ ### The two intents
55
+
56
+ - **`intent: "compete"`** — *Add as a variant.* Your edit runs as an experiment
57
+ against the current content. Nothing changes for recipients yet; Apex measures
58
+ which one is better and tells you.
59
+ - **`intent: "replace"`** — *Update for everyone.* Your edit becomes what
60
+ everyone receives. Any prior win is superseded.
61
+
62
+ ### When in doubt, compete
63
+
64
+ This is the default, and it isn't a coin flip. The two mistakes are not
65
+ symmetric:
66
+
67
+ - Guessing `compete` when the user wanted `replace` costs **time** — they see
68
+ an experiment start and can promote the winner immediately.
69
+ - Guessing `replace` when the user wanted `compete` **destroys a measured
70
+ result permanently**. There's no undo for evidence.
71
+
72
+ So: if the user explicitly said "just change it for everyone" / "replace the
73
+ copy" / "roll this out," send `replace`. Otherwise send `compete`, and say what
74
+ you did:
75
+
76
+ > "That comm's control won an experiment, so I added your new copy as a variant
77
+ > and started an experiment against it rather than overwriting the winner. Want
78
+ > me to make it the control for everyone instead?"
79
+
80
+ ### Don't force through a running experiment
81
+
82
+ `in_experiment` is a hard stop, not an intent question — the content is a
83
+ treatment mid-measurement. `edit_communication` accepts
84
+ `confirm_live_experiment: true` to override, and that **marks the experiment
85
+ invalid and excludes its result from the belief graph**. Never pass it on your
86
+ own initiative. Offer the alternatives first: wait for the experiment to
87
+ finish, or end it with `conclude_experiment` / `promote_winner` so the learning
88
+ is captured.
89
+
90
+ If you omit `intent` when it's needed, the server returns a 409 that names both
91
+ options. That's a fork in the road, not a failure — read it, ask the user, and
92
+ re-issue. Do not retry the identical call.
93
+
33
94
  ### Running experiments
34
95
 
35
96
  1. **`list_communications`** — find the communication to test.
@@ -81,7 +81,34 @@ For experiments that run inside an Adaptive Journey (control vs variant email/co
81
81
  - `get_experiment_results` — computed results + the readiness verdict (`collecting | decisive | inconclusive | guardrail`). Check this before promoting: only `decisive` means a confident winner.
82
82
  - `promote_experiment_winner` — re-verifies the readiness gate server-side, writes the winning variant's content into the canonical communication as a new version, and archives the losers. Pass `force: true` only to override the gate deliberately.
83
83
 
84
- Journey-arm experiments are authored in the journey canvas (Experiment step) and launched on journey publish; the SDK/MCP surface is for reading results and promoting.
84
+ Journey-arm experiments are authored in the journey canvas (Experiment step) and launched on journey publish. They can also start from the communication side — see below.
85
+
86
+ ## The winner is the control
87
+
88
+ There is one control per communication, and it is the last thing that won. When you promote a winner, its content is merged into the communication itself; there is no separate "winning variant" living alongside the control afterwards. This matters for how you reason about a chain of experiments:
89
+
90
+ - **An experiment always compares the current control against new variants**, never version 3 against version 5. Whatever won last time IS the control this time, so a chain of experiments reads as a chain of improvements against a moving, always-current baseline.
91
+ - **`get_experiment_results` reports the previous baseline** alongside control and variant. For a chained experiment that number is the rate the predecessor's winner achieved — which is what stops "variant beat control by 8%" from hiding the fact that the control was already three rounds of improvement deep.
92
+ - **A communication hosts one experiment at a time.** Starting a second returns 409 `comm_already_in_experiment`. That constraint is what lets "what won?" have exactly one answer.
93
+
94
+ Do NOT describe this as champion versus challenger, and do not try to run "the old version versus the new version" as two concurrent experiments. Both were earlier models; neither is how Apex works now.
95
+
96
+ ### Starting an experiment from the communication side
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:
99
+
100
+ - **A host** — some published journey must send the communication. `no_host` means there is no traffic to measure.
101
+ - **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
+ - **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
+
104
+ ### Editing a control that won
105
+
106
+ `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
+
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.
110
+
111
+ Omitting `intent` when it's required returns 409 `intent_required` naming both. Do not pick `replace` to clear the error.
85
112
 
86
113
  ## Server-side variant assignment (integrator apps)
87
114