@apex-inc/mcp-server 0.7.0 → 0.9.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/dist/tools.js CHANGED
@@ -1,6 +1,16 @@
1
1
  import { z } from "zod";
2
- import { apiGet, apiPost, apiPatch, apiDelete, postWithIdempotency, setActiveProject, getActiveProject, setActiveOrg, getActiveOrg, getUserContext } from "./api-client.js";
2
+ import { apiGet, apiPost, apiPatch, apiDelete, postWithIdempotency, setActiveWorkspace, getActiveWorkspace, setActiveOrg, getActiveOrg, getUserContext } from "./api-client.js";
3
3
  const APEX = "∧ Apex";
4
+ /**
5
+ * MOBX-006 — stable synthetic visitor id for agent-fired events.
6
+ *
7
+ * `/api/events` only counts an event toward wiring detection when it
8
+ * carries a top-level `visitorId`. A per-process random suffix keeps
9
+ * the id stable for the MCP server's lifetime (so repeated track_event
10
+ * calls roll up to one synthetic "visitor") while distinguishing
11
+ * separate agent sessions from each other.
12
+ */
13
+ const MCP_AGENT_VISITOR_ID = `mcp-agent-${Math.random().toString(36).slice(2, 10)}`;
4
14
  export const toolDefinitions = {
5
15
  plan_experiment: {
6
16
  description: `${APEX} — PLANNING ONLY, does not create anything. Analyzes a goal against existing beliefs and experiments, then returns structured options for the user to choose from. After calling, you MUST present the returned options as a numbered list or poll and wait for the user to pick before calling any other Apex tools.`,
@@ -916,27 +926,40 @@ The Apex Spec ships two peer registries:
916
926
 
917
927
  \u2022 **Platform events** (\`APEX_PLATFORM_EVENTS\`) — events Apex's own dashboard fires as merchants use it. Names: \`user_invitation_created\`, \`user_invitation_accepted\`, \`developer_invite_sent\`, \`integration_connected\`, \`integration_sync_completed\`, \`integration_sync_failed\`, \`badge_awarded\`, \`user_level_up\`, \`nudge_scheduled\`, \`drip_step_scheduled\`, \`experiment_launched\`, \`experiment_significant\`, \`experiment_concluded\`, \`experiment_promoted\`, \`portfolio_access_requested\`, \`portfolio_access_approved\`, \`portfolio_access_rejected\`, \`portfolio_share_accepted\`, \`subscription_upgraded\`, \`subscription_downgraded\`, \`tier_limit_approaching\`, \`anomaly_detected\`, \`weekly_digest_scheduled\`, \`cognito_verification_code\`, \`user_signup\`. Use these when an agent is acting *as* the Apex platform (admin tooling, dashboard automation), not as the customer's product.
918
928
 
919
- Custom event names are accepted (the spec is open by default), but agents should prefer canonical names from one of the two registries so the event flows directly into the catalog templates, the journey trigger system, and the conversion-goal helpers without manual wiring. The full spec lives at https://docs.apex.inc/spec.`,
929
+ Custom event names are accepted (the spec is open by default), but agents should prefer canonical names from one of the two registries so the event flows directly into the catalog templates, the journey trigger system, and the conversion-goal helpers without manual wiring. The full spec lives at https://docs.apex.inc/spec.
930
+
931
+ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*, fixed for this MCP process) and a timestamp, so they WILL count toward wiring detection (the same signal the Set up Apex page and get_wiring_status read). They are flagged as agent-fired (source: "mcp" in the event data + the x-apex-source header), so dashboards can distinguish them from real app traffic — but they are NOT test-mode events; fire them deliberately.`,
920
932
  schema: z.object({
921
933
  event: z.string().describe("Event name — snake_case from APEX_EVENTS or APEX_PLATFORM_EVENTS (e.g. 'page_view', 'in_app_purchase', 'user_invitation_created'). Custom names are accepted but prefer canonical ones."),
922
934
  properties: z.record(z.unknown()).optional().describe("Flat snake_case properties matching the spec entry for this event (e.g. { product_id: 'prod_abc', value: 29.99, currency: 'USD' })."),
923
935
  }),
924
936
  handler: async ({ event, properties }) => {
937
+ // MOBX-006 — the batch path of /api/events reads `visitorId`,
938
+ // `type`, and the event bag (`data`) at the TOP LEVEL of each
939
+ // event. Without a visitorId the event lands in the raw SDK
940
+ // firehose but never reaches the tracking-event store that
941
+ // powers wiring detection / readiness (the route reports it
942
+ // under `untracked`). Stamp the per-process synthetic visitor
943
+ // id + timestamp so agent-fired events actually count.
925
944
  const result = await apiPost("/api/events", {
926
- projectKey: process.env.APEX_PROJECT_KEY || "default",
945
+ workspaceKey: (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY) || "default",
927
946
  userId: "mcp-agent",
928
947
  events: [
929
948
  {
930
949
  type: event,
931
- payload: { ...properties, source: "mcp" },
950
+ visitorId: MCP_AGENT_VISITOR_ID,
932
951
  timestamp: new Date().toISOString(),
952
+ data: { ...properties, source: "mcp" },
933
953
  },
934
954
  ],
935
955
  });
956
+ const wiringNote = result.untracked && result.untracked.length > 0
957
+ ? `\nNOT counted toward wiring detection: ${result.untracked[0].reason}`
958
+ : `\nCounts toward wiring detection (visitorId: ${MCP_AGENT_VISITOR_ID}, flagged agent-fired via source: "mcp").`;
936
959
  return {
937
960
  content: [{
938
961
  type: "text",
939
- text: `Event tracked: "${event}" — ${result.received} event(s) stored.${properties ? `\nProperties: ${JSON.stringify(properties)}` : ""}`,
962
+ text: `Event tracked: "${event}" — ${result.received} event(s) stored.${wiringNote}${properties ? `\nProperties: ${JSON.stringify(properties)}` : ""}`,
940
963
  }],
941
964
  };
942
965
  },
@@ -989,7 +1012,7 @@ Custom event names are accepted (the spec is open by default), but agents should
989
1012
  await apiPost("/api/identity/stitch", {
990
1013
  visitorId: `mcp-${email}`,
991
1014
  email,
992
- projectKey: process.env.APEX_PROJECT_KEY || "default",
1015
+ workspaceKey: (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY) || "default",
993
1016
  metadata: { name, company, ...metadata, source: "mcp" },
994
1017
  });
995
1018
  return {
@@ -1004,7 +1027,7 @@ Custom event names are accepted (the spec is open by default), but agents should
1004
1027
  description: `${APEX} — Read the workspace's canonical Schema (data dictionary): entities, their fields, which fields are wired (observed in live data) vs not, surface coverage, and any canonical mappings. Use to see what variables/attributes a workspace has and what's still unwired before authoring comms or segments.`,
1005
1028
  schema: z.object({}),
1006
1029
  handler: async () => {
1007
- const dict = await apiGet("/api/data-dictionary");
1030
+ const dict = await apiGet("/api/schema");
1008
1031
  const lines = [];
1009
1032
  for (const entity of Object.values(dict.entities)) {
1010
1033
  lines.push(`\n${entity.label} {{${entity.key}.<field>}}`);
@@ -1035,7 +1058,7 @@ Custom event names are accepted (the spec is open by default), but agents should
1035
1058
  .describe("Canonical snake_case name (e.g. 'first_name'), or null to clear"),
1036
1059
  }),
1037
1060
  handler: async ({ entityKey, fieldName, canonicalName, }) => {
1038
- const res = await apiPost("/api/data-dictionary/map", { entityKey, fieldName, canonicalName: canonicalName ?? null });
1061
+ const res = await apiPost("/api/schema/map", { entityKey, fieldName, canonicalName: canonicalName ?? null });
1039
1062
  const mapped = res.data?.canonicalName;
1040
1063
  return {
1041
1064
  content: [
@@ -1101,48 +1124,51 @@ Custom event names are accepted (the spec is open by default), but agents should
1101
1124
  },
1102
1125
  },
1103
1126
  list_projects: {
1104
- description: "List all projects available to your API key. Use this to see which projects you can work with.",
1127
+ description: "List all workspaces available to your API key. Use this to see which workspaces you can work with.",
1105
1128
  schema: z.object({}),
1106
1129
  handler: async () => {
1107
- const projects = await apiGet("/api/projects");
1108
- const current = getActiveProject();
1109
- if (!projects.length) {
1130
+ // NOTE: `/api/projects` is the PERMANENT alias path this tool has
1131
+ // shipped against — it returns the flat row shape below. Do not
1132
+ // point at `/api/workspaces` (different response shape).
1133
+ const workspaces = await apiGet("/api/projects");
1134
+ const current = getActiveWorkspace();
1135
+ if (!workspaces.length) {
1110
1136
  return {
1111
1137
  content: [{
1112
1138
  type: "text",
1113
- text: "No projects found for this API key.",
1139
+ text: "No workspaces found for this API key.",
1114
1140
  }],
1115
1141
  };
1116
1142
  }
1117
- const lines = projects.map((p) => {
1118
- const active = p.projectKey === current ? " ← active" : "";
1119
- return ` ${p.projectKey} — ${p.name} (${p.url})${active}`;
1143
+ const lines = workspaces.map((p) => {
1144
+ const active = p.workspaceKey === current ? " ← active" : "";
1145
+ return ` ${p.workspaceKey} — ${p.name} (${p.url})${active}`;
1120
1146
  });
1121
1147
  return {
1122
1148
  content: [{
1123
1149
  type: "text",
1124
- text: [`Projects (${projects.length}):`, ...lines].join("\n"),
1150
+ text: [`Workspaces (${workspaces.length}):`, ...lines].join("\n"),
1125
1151
  }],
1126
1152
  };
1127
1153
  },
1128
1154
  },
1129
1155
  switch_project: {
1130
- description: "Switch the active project for this session. All subsequent API calls will use this project.",
1156
+ description: "Switch the active workspace for this session. All subsequent API calls will use this workspace.",
1131
1157
  schema: z.object({
1132
- projectKey: z.string().describe("The project key to switch to"),
1158
+ workspaceKey: z.string().describe("The workspace key to switch to"),
1133
1159
  }),
1134
1160
  handler: async (args) => {
1135
- setActiveProject(args.projectKey);
1161
+ setActiveWorkspace(args.workspaceKey);
1136
1162
  return {
1137
1163
  content: [{
1138
1164
  type: "text",
1139
- text: `Switched to project: ${args.projectKey}. All subsequent calls will use this project.`,
1165
+ text: `Switched to workspace: ${args.workspaceKey}. All subsequent calls will use this workspace.`,
1140
1166
  }],
1141
1167
  };
1142
1168
  },
1143
1169
  },
1144
1170
  list_orgs: {
1145
- description: "List all organizations you have access to, with their projects. Use this to see which orgs and projects are available.",
1171
+ description: "List all organizations you have access to, with their workspaces. Use this to see which orgs and workspaces are available.",
1146
1172
  schema: z.object({}),
1147
1173
  handler: async () => {
1148
1174
  const ctx = getUserContext();
@@ -1157,8 +1183,8 @@ Custom event names are accepted (the spec is open by default), but agents should
1157
1183
  const activeOrg = getActiveOrg();
1158
1184
  const lines = ctx.orgs.map((org) => {
1159
1185
  const active = org.orgId === activeOrg ? " ← active" : "";
1160
- const projects = org.projects.map((p) => ` ${p.projectKey} — ${p.name}`).join("\n");
1161
- return ` ${org.orgName} (${org.orgId}, ${org.role})${active}\n${projects || " (no projects)"}`;
1186
+ const workspaces = org.workspaces.map((p) => ` ${p.workspaceKey} — ${p.name}`).join("\n");
1187
+ return ` ${org.orgName} (${org.orgId}, ${org.role})${active}\n${workspaces || " (no workspaces)"}`;
1162
1188
  });
1163
1189
  return {
1164
1190
  content: [{
@@ -1169,7 +1195,7 @@ Custom event names are accepted (the spec is open by default), but agents should
1169
1195
  },
1170
1196
  },
1171
1197
  switch_org: {
1172
- description: "Switch the active organization. This also switches to the first project in that org.",
1198
+ description: "Switch the active organization. This also switches to the first workspace in that org.",
1173
1199
  schema: z.object({
1174
1200
  orgId: z.string().describe("The organization ID to switch to (from list_orgs)"),
1175
1201
  }),
@@ -1196,12 +1222,12 @@ Custom event names are accepted (the spec is open by default), but agents should
1196
1222
  };
1197
1223
  }
1198
1224
  setActiveOrg(args.orgId);
1199
- const project = getActiveProject();
1200
- const projectInfo = project ? `, active project: ${project}` : "";
1225
+ const workspace = getActiveWorkspace();
1226
+ const projectInfo = workspace ? `, active workspace: ${workspace}` : "";
1201
1227
  return {
1202
1228
  content: [{
1203
1229
  type: "text",
1204
- text: `Switched to org: ${org.orgName} (${org.projects.length} project${org.projects.length !== 1 ? "s" : ""})${projectInfo}.`,
1230
+ text: `Switched to org: ${org.orgName} (${org.workspaces.length} workspace${org.workspaces.length !== 1 ? "s" : ""})${projectInfo}.`,
1205
1231
  }],
1206
1232
  };
1207
1233
  },
@@ -1322,26 +1348,37 @@ Custom event names are accepted (the spec is open by default), but agents should
1322
1348
  },
1323
1349
  },
1324
1350
  get_event_taxonomy: {
1325
- description: "Get the standard events your app should track for this vertical. Shows event names, descriptions, and apex.track() examples.",
1351
+ // MOBX-002 (post-mortem #9) this tool used to read the legacy
1352
+ // comms registry (`/api/communications/events`), which disagreed
1353
+ // with the Apex Spec on names AND casing (purchase_completed vs
1354
+ // in_app_purchase, userId vs order_id) and rendered "undefined"
1355
+ // descriptions. There is ONE event vocabulary: the Apex Spec.
1356
+ // This tool now serves it and points agents at get_event_spec.
1357
+ description: "DEPRECATED alias of get_event_spec — the canonical Apex Spec event registry (one vocabulary; the legacy comms taxonomy this used to serve disagreed with it). Prefer get_event_spec.",
1326
1358
  schema: z.object({}),
1327
1359
  handler: async () => {
1328
- const data = await apiGet("/api/communications/events");
1329
- const events = Array.isArray(data) ? data : [];
1330
- if (events.length === 0) {
1331
- return { content: [{ type: "text", text: "No event taxonomy configured. Set up your project's vertical first." }] };
1360
+ const res = await apiGet("/api/spec/events");
1361
+ const events = res.data?.events ?? {};
1362
+ const names = Object.keys(events);
1363
+ if (names.length === 0) {
1364
+ return { content: [{ type: "text", text: "Spec bundle unavailable. Use get_event_spec." }] };
1332
1365
  }
1333
- const summary = events.map((e) => {
1334
- const evt = e;
1335
- return `• ${evt.id}: ${evt.name} — ${evt.description || ""}`;
1336
- }).join("\n");
1337
- return { content: [{ type: "text", text: `${events.length} events in taxonomy:\n\n${summary}\n\nFull data:\n${JSON.stringify(data, null, 2)}` }] };
1366
+ const summary = names
1367
+ .map((n) => `• ${n} — ${events[n]?.description ?? ""}`)
1368
+ .join("\n");
1369
+ return {
1370
+ content: [{
1371
+ type: "text",
1372
+ text: `${names.length} canonical events (Apex Spec ${res.data?.specVersion ?? ""}). Use get_event_spec for full schemas.\n\n${summary}`,
1373
+ }],
1374
+ };
1338
1375
  },
1339
1376
  },
1340
1377
  // ─── Partner Network (MMP-161) ──────────────────────────────────────────
1341
1378
  //
1342
1379
  // Merchant-scoped tools for managing an affiliate program through the
1343
1380
  // Apex Partner Network. Every tool authenticates via the caller's
1344
- // active project + API key (same pattern as experiments / comms).
1381
+ // active workspace + API key (same pattern as experiments / comms).
1345
1382
  // Partner-facing flows (self-signup, portal) + admin Fraud Ops are
1346
1383
  // intentionally not wrapped — they require different auth.
1347
1384
  list_partner_programs: {
@@ -1516,7 +1553,7 @@ Custom event names are accepted (the spec is open by default), but agents should
1516
1553
  const tag = f.isCustom ? " (custom)" : "";
1517
1554
  return `• ${kind}: ${display}${tag} — source: ${f.source}`;
1518
1555
  }).join("\n");
1519
- return { content: [{ type: "text", text: `Fees for ${data.projectKey}:\n\n${lines}\n\nFull data:\n${JSON.stringify(data, null, 2)}` }] };
1556
+ return { content: [{ type: "text", text: `Fees for ${data.workspaceKey}:\n\n${lines}\n\nFull data:\n${JSON.stringify(data, null, 2)}` }] };
1520
1557
  },
1521
1558
  },
1522
1559
  vouch_for_partner: {
@@ -1536,7 +1573,7 @@ Custom event names are accepted (the spec is open by default), but agents should
1536
1573
  // Pair with the AudienceActivationWizard (Layer 2) to give agents
1537
1574
  // a way to wire up audience instrumentation without leaving the
1538
1575
  // editor. Both tools are PROJECT-SCOPED via the api-client's
1539
- // active-project context — the merchant's CLI run pins which
1576
+ // active-workspace context — the merchant's CLI run pins which
1540
1577
  // workspace they're acting on.
1541
1578
  wire_audience_seed: {
1542
1579
  description: `${APEX} — Wire up the events and traits a campaign audience seed depends on. Use this when the merchant says "wire up the active customers audience" or after the AudienceActivationWizard's Screen 1 hands off to Cursor. Returns the per-event/trait code snippets, the file recommendations, and fires a test event with is_test=1 so the wizard's polling loop confirms the wiring landed end-to-end. Idempotent: calling twice does NOT double-instrument; the agent should still review existing code before adding the snippet.`,
@@ -1554,13 +1591,13 @@ Custom event names are accepted (the spec is open by default), but agents should
1554
1591
  .describe("When true (default), fires a test event with is_test=1 for each event the seed depends on. The wizard's poll loop sees these and advances. Set to false if the agent only wants to read the snippets without writing test data."),
1555
1592
  }),
1556
1593
  handler: async ({ seedId, framework = "typescript", fireTestEvent = true, }) => {
1557
- const projectKey = getActiveProject() || process.env.APEX_PROJECT_KEY;
1558
- if (!projectKey) {
1594
+ const workspaceKey = getActiveWorkspace() || (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY);
1595
+ if (!workspaceKey) {
1559
1596
  return {
1560
1597
  content: [
1561
1598
  {
1562
1599
  type: "text",
1563
- text: "No active project. Call set_active_project first or set APEX_PROJECT_KEY.",
1600
+ text: "No active workspace. Call set_active_project first or set APEX_PROJECT_KEY.",
1564
1601
  },
1565
1602
  ],
1566
1603
  };
@@ -1622,7 +1659,7 @@ Custom event names are accepted (the spec is open by default), but agents should
1622
1659
  ? `## Test events fired\nFired test events for: ${firedTests.join(", ")}. The wizard's polling loop will see these and advance. Confirm the merchant fires REAL events from production code paths before relying on the audience.`
1623
1660
  : `## Test events\nNo test events fired (per fireTestEvent=false). Run the wizard's "Send a test event" button manually after wiring.`,
1624
1661
  ``,
1625
- `Project: ${projectKey}`,
1662
+ `Workspace: ${workspaceKey}`,
1626
1663
  ].join("\n");
1627
1664
  return { content: [{ type: "text", text: summary }] };
1628
1665
  },
@@ -1643,13 +1680,13 @@ Custom event names are accepted (the spec is open by default), but agents should
1643
1680
  .describe("When true (default), fires a test event with is_test=1 for each NON-AUTO-FIRED event the audience depends on. Auto-fired events (pageview, click, etc.) are skipped — they're already firing automatically and don't need a synthetic test."),
1644
1681
  }),
1645
1682
  handler: async ({ audienceId, framework = "typescript", fireTestEvent = true, }) => {
1646
- const projectKey = getActiveProject() || process.env.APEX_PROJECT_KEY;
1647
- if (!projectKey) {
1683
+ const workspaceKey = getActiveWorkspace() || (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY);
1684
+ if (!workspaceKey) {
1648
1685
  return {
1649
1686
  content: [
1650
1687
  {
1651
1688
  type: "text",
1652
- text: "No active project. Call set_active_project first or set APEX_PROJECT_KEY.",
1689
+ text: "No active workspace. Call set_active_project first or set APEX_PROJECT_KEY.",
1653
1690
  },
1654
1691
  ],
1655
1692
  };
@@ -1718,7 +1755,7 @@ Custom event names are accepted (the spec is open by default), but agents should
1718
1755
  else {
1719
1756
  sections.push(`\n## Test events\nNo test events fired (per fireTestEvent=false). Run the wizard's "Send a test event" button manually after wiring.`);
1720
1757
  }
1721
- sections.push(`\nProject: ${projectKey}`);
1758
+ sections.push(`\nWorkspace: ${workspaceKey}`);
1722
1759
  return { content: [{ type: "text", text: sections.join("\n") }] };
1723
1760
  },
1724
1761
  },
@@ -1730,13 +1767,13 @@ Custom event names are accepted (the spec is open by default), but agents should
1730
1767
  .describe("Audience id (the workspace's audience record, not the seed). Use list_audiences first to find it."),
1731
1768
  }),
1732
1769
  handler: async ({ audienceId }) => {
1733
- const projectKey = getActiveProject() || process.env.APEX_PROJECT_KEY;
1734
- if (!projectKey) {
1770
+ const workspaceKey = getActiveWorkspace() || (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY);
1771
+ if (!workspaceKey) {
1735
1772
  return {
1736
1773
  content: [
1737
1774
  {
1738
1775
  type: "text",
1739
- text: "No active project. Call set_active_project first or set APEX_PROJECT_KEY.",
1776
+ text: "No active workspace. Call set_active_project first or set APEX_PROJECT_KEY.",
1740
1777
  },
1741
1778
  ],
1742
1779
  };
@@ -1765,7 +1802,7 @@ Custom event names are accepted (the spec is open by default), but agents should
1765
1802
  preview && preview.matchingCount === 0
1766
1803
  ? `\n## Diagnosis\nZero matches. Most likely causes:\n 1. Events the predicate references aren't firing yet from the merchant's codebase. Run \`wire_audience_seed\` if this is a starter audience, or check the predicate's event names against your codebase.\n 2. The predicate's window (\`last_n_days\`, \`ever\`) is too narrow.\n 3. End-user identity isn't being stitched. Each \`apex.track()\` call must include \`endUserId\` in its data payload, or be preceded by an \`identify()\` call.\n`
1767
1804
  : "",
1768
- `\nProject: ${projectKey}`,
1805
+ `\nWorkspace: ${workspaceKey}`,
1769
1806
  ].join("\n");
1770
1807
  return { content: [{ type: "text", text: summary }] };
1771
1808
  },
@@ -1934,7 +1971,7 @@ Custom event names are accepted (the spec is open by default), but agents should
1934
1971
  description: `${APEX} — READ-ONLY. Returns the current Set up Apex state for the active workspace: which steps the merchant has marked complete or skipped, plus the live signal flags (snippet installed, mobile live, sender domain verified, etc.). Use to answer "where am I in onboarding?" or to surface "your next step is X" recommendations. WRITE tools (complete_setup_step, skip_setup_step) are deliberately NOT exposed pending a server-enforced confirmation nonce.`,
1935
1972
  schema: z.object({}),
1936
1973
  handler: async () => {
1937
- // The active project key is encoded in the x-apex-project header
1974
+ // The active workspace key is encoded in the x-apex-workspace header
1938
1975
  // that apiGet sends automatically.
1939
1976
  const data = await apiGet("/api/setup-state");
1940
1977
  const { overrides, signal, workspaceKey } = data.data;
@@ -1973,7 +2010,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
1973
2010
  .describe("Include archived sources. Defaults to false."),
1974
2011
  }),
1975
2012
  handler: async ({ includeArchived }) => {
1976
- const proj = getActiveProject();
2013
+ const proj = getActiveWorkspace();
1977
2014
  const qs = includeArchived ? "?includeArchived=true" : "";
1978
2015
  const json = await apiGet(`/api/workspaces/${proj}/data-sources${qs}`);
1979
2016
  const sources = json.data ?? [];
@@ -1993,7 +2030,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
1993
2030
  name: z.string().describe("Display name, e.g. 'Marketing site' or 'Backend API'."),
1994
2031
  }),
1995
2032
  handler: async ({ kind, name }) => {
1996
- const proj = getActiveProject();
2033
+ const proj = getActiveWorkspace();
1997
2034
  const json = await apiPost(`/api/workspaces/${proj}/data-sources`, { kind, name });
1998
2035
  const s = json.data;
1999
2036
  return { content: [{ type: "text", text: `✓ Created data source **${s.name}** (${s.kind}) — ${s.status} · \`${s.id}\`` }] };
@@ -2005,7 +2042,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
2005
2042
  id: z.string().describe("The data source id (ds_...)."),
2006
2043
  }),
2007
2044
  handler: async ({ id }) => {
2008
- const proj = getActiveProject();
2045
+ const proj = getActiveWorkspace();
2009
2046
  const json = await apiGet(`/api/workspaces/${proj}/data-sources/${encodeURIComponent(id)}/health`);
2010
2047
  const h = json.data;
2011
2048
  return { content: [{ type: "text", text: `# ${h.kind} source ${h.id}\n\n- status: **${h.status}**\n- first seen: ${h.firstSeenAt ?? "never"}\n- last seen: ${h.lastSeenAt ?? "never"}` }] };
@@ -2041,5 +2078,109 @@ _Suggest the next step the user should tackle based on what's incomplete in the
2041
2078
  return { content: [{ type: "text", text: hosts.length ? `# Hostnames (${hosts.length})\n\n${lines.join("\n")}` : "No hostnames seen yet." }] };
2042
2079
  },
2043
2080
  },
2081
+ get_event_spec: {
2082
+ description: `${APEX} — The canonical event reference for instrumenting Apex tracking: every event name, its fields (with required/optional markers), and example payloads, as a JSON Schema bundle. Call this BEFORE wiring apex.track()/Apex.track() calls or Server Events API posts into a codebase so you use canonical names and field shapes. Pair with get_wiring_status to verify your instrumentation afterwards.`,
2083
+ schema: z.object({
2084
+ event: z.string().optional().describe("Return only this event's schema (canonical name, e.g. 'add_to_cart'). Omit for the full bundle."),
2085
+ }),
2086
+ handler: async ({ event }) => {
2087
+ const res = await apiGet("/api/spec/events");
2088
+ const bundle = res.data ?? {};
2089
+ if (event) {
2090
+ const all = (bundle.events ?? {});
2091
+ const match = all[event];
2092
+ if (!match) {
2093
+ const names = Object.keys(all).slice(0, 50).join(", ");
2094
+ return {
2095
+ content: [{ type: "text", text: `Unknown event "${event}". Known events include: ${names}` }],
2096
+ };
2097
+ }
2098
+ return {
2099
+ content: [{ type: "text", text: `# ${event}\n\n\`\`\`json\n${JSON.stringify(match, null, 2)}\n\`\`\`` }],
2100
+ };
2101
+ }
2102
+ return {
2103
+ content: [{ type: "text", text: `# Apex event reference\n\n\`\`\`json\n${JSON.stringify(bundle, null, 2)}\n\`\`\`` }],
2104
+ };
2105
+ },
2106
+ },
2107
+ get_wiring_status: {
2108
+ description: `${APEX} — Live wiring verification for the active workspace: which canonical events have been received (tier: waiting → detected → verified), which identify traits (first_name, email, …) are arriving, per-data-source status, and which journey/communication templates are Ready to run. Call this AFTER instrumenting tracking code to verify your own work — iterate until each wired event reports "detected". Presence-only: never returns end-user values.`,
2109
+ schema: z.object({}),
2110
+ handler: async () => {
2111
+ const workspaceKey = getActiveWorkspace();
2112
+ if (!workspaceKey) {
2113
+ return {
2114
+ content: [{ type: "text", text: "No active workspace. Use switch_project first (or set APEX_PROJECT_KEY)." }],
2115
+ };
2116
+ }
2117
+ const json = await apiGet(`/api/workspaces/${encodeURIComponent(workspaceKey)}/readiness`);
2118
+ const d = json.data;
2119
+ const eventLines = d.events.map((e) => `- ${e.name}: ${e.tier} (${e.count.toLocaleString()} events, platforms: ${e.platforms.join("/") || "—"})${e.observedFields?.length ? ` — fields seen: ${e.observedFields.join(", ")}` : ""}`);
2120
+ const traitLines = d.traits.map((t) => `- ${t.name}: ${t.present ? "arriving" : "not seen"}${t.inferredType ? ` (${t.inferredType})` : ""}`);
2121
+ const sourceLines = d.sources.map((s) => `- ${s.name} (${s.kind}): ${s.status}${s.lastSeenAt ? ` — last event ${s.lastSeenAt}` : ""}`);
2122
+ const readyTemplates = d.templates.filter((t) => t.ready);
2123
+ const blockedTemplates = d.templates.filter((t) => !t.ready);
2124
+ const templateLines = [
2125
+ ...readyTemplates.map((t) => `- ✅ ${t.title} (${t.templateKind}) — Ready to run`),
2126
+ ...blockedTemplates.slice(0, 10).map((t) => `- ⏳ ${t.title} (${t.templateKind}) — needs ${[...t.missingEvents, ...t.missingTraits].join(", ") || "?"}`),
2127
+ ];
2128
+ const milestones = [
2129
+ d.milestones.firstEventAt ? `First event: ${d.milestones.firstEventAt}` : "First event: not yet",
2130
+ d.milestones.firstIdentifiedAt ? `First identified customer: ${d.milestones.firstIdentifiedAt}` : "First identified customer: not yet",
2131
+ ];
2132
+ return {
2133
+ content: [{
2134
+ type: "text",
2135
+ text: `# Wiring status — ${workspaceKey}\n\n## Milestones\n${milestones.map((m) => `- ${m}`).join("\n")}\n\n## Events received\n${eventLines.join("\n") || "_none yet_"}\n\n## Identify traits\n${traitLines.join("\n") || "_none yet_"}\n\n## Data sources\n${sourceLines.join("\n") || "_none yet_"}\n\n## Templates\n${templateLines.join("\n") || "_none derivable yet_"}`,
2136
+ }],
2137
+ };
2138
+ },
2139
+ },
2140
+ propose_wiring_plan: {
2141
+ description: `${APEX} — Report the Apex Spec instrumentation opportunities you found by SCANNING the merchant's codebase (auth flows → identify + user_signed_up, checkout → purchase events, forms → form_submit, app purchases → in_app_purchase, server order paths → order_placed, …). Call this BEFORE wiring: the plan appears on the merchant's setup page as pending lights, each flipping green when its first real event arrives. Structure your scan report in three sections: (1) ALREADY WIRED — events the repo already instruments (include them here with wired: true; they're awaiting their first heartbeat), (2) GAPS TO WIRE — the work you propose, (3) WON'T WIRE + WHY — opportunities you're deliberately skipping (e.g. a server event with no backend, or a form that already fires a richer dedicated event). Then instrument each gap where the truth lives and verify with get_wiring_status. Scan first; never ask the merchant to enumerate events for you.`,
2142
+ schema: z.object({
2143
+ events: z
2144
+ .array(z.object({
2145
+ name: z
2146
+ .string()
2147
+ .describe("Canonical snake_case event name from the Apex Spec (use get_event_spec), or a custom snake_case name."),
2148
+ surface: z
2149
+ .enum(["web", "mobile", "server"])
2150
+ .describe("Where this event should fire FROM — wire each event where the truth lives."),
2151
+ note: z
2152
+ .string()
2153
+ .optional()
2154
+ .describe("Short pointer to where you found the opportunity, e.g. 'checkout/confirm.ts handlePayment()'."),
2155
+ wired: z
2156
+ .boolean()
2157
+ .optional()
2158
+ .describe("True when the repo ALREADY instruments this event (awaiting its first heartbeat) — the merchant's board labels it 'wired' instead of 'agent plan'."),
2159
+ }))
2160
+ .min(1)
2161
+ .describe("Every instrumentation opportunity found in the scan — both already-wired events (wired: true) and the gaps you propose to wire."),
2162
+ summary: z.string().optional().describe("One-paragraph scan summary for the merchant — include what you're deliberately NOT wiring and why."),
2163
+ }),
2164
+ handler: async (args) => {
2165
+ const workspaceKey = getActiveWorkspace();
2166
+ if (!workspaceKey) {
2167
+ return {
2168
+ content: [{ type: "text", text: "No active workspace. Use switch_project first (or set APEX_PROJECT_KEY)." }],
2169
+ };
2170
+ }
2171
+ const json = await apiPost(`/api/workspaces/${encodeURIComponent(workspaceKey)}/wiring-plan`, {
2172
+ events: args.events,
2173
+ summary: args.summary,
2174
+ source: "mcp",
2175
+ });
2176
+ const lines = json.data.events.map((e) => `- ${e.name} (${e.surface})${e.wired ? " — already wired, awaiting first heartbeat" : ""}`);
2177
+ return {
2178
+ content: [{
2179
+ type: "text",
2180
+ text: `# Wiring plan proposed — ${workspaceKey}\n\nThe merchant's setup page now shows ${json.data.events.length} pending event light${json.data.events.length === 1 ? "" : "s"}:\n${lines.join("\n")}\n\nNext: instrument each gap where it happens in the code (smallest change, never commit), then call get_wiring_status until every light reports "detected".`,
2181
+ }],
2182
+ };
2183
+ },
2184
+ },
2044
2185
  };
2045
2186
  //# sourceMappingURL=tools.js.map