@apex-inc/mcp-server 0.18.0 → 0.20.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.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "MCP server for Apex — currently in private beta.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,9 +29,44 @@ The Adoption Engine tracks whether each end-user has adopted each feature, nudge
29
29
  | `update_adoption_milestone` | Change priority / gap window / turn on/off |
30
30
  | `delete_adoption_milestone` | Remove a milestone |
31
31
  | `get_adoption_metrics` | Per-milestone funnel + lift-vs-holdout + workspace health |
32
+ | `list_adoption_features` / `create_adoption_feature` | Tenant-owned feature groupings (group your own events) |
33
+ | `list_segments` / `get_segment` / `create_segment` | Saved groups of customers — same grammar as milestones |
34
+ | `define_event` | Register an event's NAME before it fires (define ≠ fire) |
32
35
 
33
36
  Read-only summary resource: `apex://adoption`.
34
37
 
38
+ ## One grammar: define → wire → verify
39
+
40
+ Milestones and segments both describe "people who…" over the workspace's own
41
+ events + traits. The methodology is the same everywhere (dashboard, SDK, MCP):
42
+
43
+ 1. **Define** — name the signal. If the event doesn't exist yet, `define_event`
44
+ registers it. Defining does NOT make it fire.
45
+ 2. **Wire** — a developer calls `apex.track("your_event")` from the app (or
46
+ sends the trait via `apex.identify`). Until then it shows a "not seen yet"
47
+ dot.
48
+ 3. **Verify** — once real events arrive, milestone lift / segment counts fill in.
49
+
50
+ ### Segments — `create_segment`
51
+
52
+ Author with the friendly `conditions` grammar (snake_case sub-keys), or pass a
53
+ raw `predicate`:
54
+
55
+ ```json
56
+ {
57
+ "name": "Power users who haven't upgraded",
58
+ "conditions": {
59
+ "match": "all",
60
+ "groups": [
61
+ { "match": "all", "conditions": [
62
+ { "verb": "has", "event": "report_run", "window_days": 30 },
63
+ { "attribute": "plan", "op": "is_not", "value": "pro" }
64
+ ] }
65
+ ]
66
+ }
67
+ }
68
+ ```
69
+
35
70
  ## Tool Invocation Order
36
71
 
37
72
  ### "Nudge users who haven't run their first report"
@@ -39,6 +39,33 @@ When a user was anonymous and then becomes known (signup, login, checkout email)
39
39
  - Call **`identify`** with a stable `userId` and pass **`traits.email`** when applicable.
40
40
  - **Server-side** `identify` (webhooks, API routes): pass **`traits.visitorId`** = value of the browser **`apex_vid`** cookie so the stitch links to the real marketing session. Without it, Apex may create a synthetic visitor disconnected from prior events.
41
41
 
42
+ ## B2B accounts + groups (model the company, not just the seat)
43
+
44
+ For B2B, revenue belongs to the **account** (company), not the individual. Add a nested `account` object to the SAME `identify` you already call at login/signup:
45
+
46
+ ```ts
47
+ identify(user.id, {
48
+ email: user.email,
49
+ account: { id: "acct_acme", name: "Acme Inc", plan: "enterprise", seat_count: 25 },
50
+ });
51
+ ```
52
+
53
+ Apex upserts the Account, links the user as a member, and rolls every member's revenue into one account LTV (no double counting). `account.id` is required — Apex never infers the company from a shared email domain.
54
+
55
+ If your product has an intermediate org-unit layer (workspaces / teams / projects), assert it with `groups[]` on the same identify — the "model a team/workspace layer" recipe:
56
+
57
+ ```ts
58
+ identify(user.id, {
59
+ account: { id: "acct_acme", name: "Acme Inc" },
60
+ groups: [
61
+ { id: "ws_prod", name: "Production", kind: "workspace", role: "admin" },
62
+ { id: "team_growth", name: "Growth Team", kind: "team", parent_id: "acct_acme" },
63
+ ],
64
+ });
65
+ ```
66
+
67
+ Each group becomes a sub-account of `parent_id` (defaults to `account.id`) and the user is linked to it. Read it back with `apex.accounts.list()` / `.subaccounts(id)` / `.people(id)` / `.contactGroups(contactId)` (SDK) or `list_accounts` / `list_sub_accounts` / `get_contact_groups` (MCP). Revenue lands on the billed account; a parent's LTV is its own counters, not the sum of children.
68
+
42
69
  ## What to measure by loop phase
43
70
 
44
71
  - **Acquisition**: landing views, campaign params (UTM already captured by snippet where configured), signup started.
@@ -105,6 +105,16 @@ Scan-first additions per vertical — wire where the truth lives:
105
105
  (monthly|annual|quarterly|weekly) — quarantined for MRR otherwise.
106
106
  `seat_count` unlocks seat growth. Account traits: `account_id`,
107
107
  `plan`, `mrr`, `arr`, `seat_count` (typed via ApexAccountTraits).
108
+ For account-level rollup (B2B): send a nested `account` object on
109
+ `identify` — `identify(user.id, { account: { id, name, plan,
110
+ seat_count } })` — so revenue rolls up to the company, not the seat.
111
+ For an intermediate org-unit layer (workspaces / teams / projects),
112
+ add `groups[]` on the SAME identify: `groups: [{ id, name, kind,
113
+ parent_id?, role? }]`. Each group becomes a sub-account of `parent_id`
114
+ (defaults to `account.id`) and the user is linked to it. snake_case
115
+ sub-keys; a group only attaches under an account the user is already
116
+ anchored to. Read back via MCP `list_accounts` / `list_sub_accounts`
117
+ / `get_contact_groups`.
108
118
  - **E-commerce**: returns pipeline → `return_requested` / `return_completed`
109
119
  (goods back) alongside `purchase_refunded` (money back — now carries
110
120
  optional `product_id` for per-SKU attribution). Never conflate the two.