@apex-inc/mcp-server 0.8.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,5 +1,5 @@
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
4
  /**
5
5
  * MOBX-006 — stable synthetic visitor id for agent-fired events.
@@ -942,7 +942,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
942
942
  // under `untracked`). Stamp the per-process synthetic visitor
943
943
  // id + timestamp so agent-fired events actually count.
944
944
  const result = await apiPost("/api/events", {
945
- projectKey: process.env.APEX_PROJECT_KEY || "default",
945
+ workspaceKey: (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY) || "default",
946
946
  userId: "mcp-agent",
947
947
  events: [
948
948
  {
@@ -1012,7 +1012,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1012
1012
  await apiPost("/api/identity/stitch", {
1013
1013
  visitorId: `mcp-${email}`,
1014
1014
  email,
1015
- projectKey: process.env.APEX_PROJECT_KEY || "default",
1015
+ workspaceKey: (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY) || "default",
1016
1016
  metadata: { name, company, ...metadata, source: "mcp" },
1017
1017
  });
1018
1018
  return {
@@ -1027,7 +1027,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1027
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.`,
1028
1028
  schema: z.object({}),
1029
1029
  handler: async () => {
1030
- const dict = await apiGet("/api/data-dictionary");
1030
+ const dict = await apiGet("/api/schema");
1031
1031
  const lines = [];
1032
1032
  for (const entity of Object.values(dict.entities)) {
1033
1033
  lines.push(`\n${entity.label} {{${entity.key}.<field>}}`);
@@ -1058,7 +1058,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1058
1058
  .describe("Canonical snake_case name (e.g. 'first_name'), or null to clear"),
1059
1059
  }),
1060
1060
  handler: async ({ entityKey, fieldName, canonicalName, }) => {
1061
- 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 });
1062
1062
  const mapped = res.data?.canonicalName;
1063
1063
  return {
1064
1064
  content: [
@@ -1124,48 +1124,51 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1124
1124
  },
1125
1125
  },
1126
1126
  list_projects: {
1127
- 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.",
1128
1128
  schema: z.object({}),
1129
1129
  handler: async () => {
1130
- const projects = await apiGet("/api/projects");
1131
- const current = getActiveProject();
1132
- 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) {
1133
1136
  return {
1134
1137
  content: [{
1135
1138
  type: "text",
1136
- text: "No projects found for this API key.",
1139
+ text: "No workspaces found for this API key.",
1137
1140
  }],
1138
1141
  };
1139
1142
  }
1140
- const lines = projects.map((p) => {
1141
- const active = p.projectKey === current ? " ← active" : "";
1142
- 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}`;
1143
1146
  });
1144
1147
  return {
1145
1148
  content: [{
1146
1149
  type: "text",
1147
- text: [`Projects (${projects.length}):`, ...lines].join("\n"),
1150
+ text: [`Workspaces (${workspaces.length}):`, ...lines].join("\n"),
1148
1151
  }],
1149
1152
  };
1150
1153
  },
1151
1154
  },
1152
1155
  switch_project: {
1153
- 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.",
1154
1157
  schema: z.object({
1155
- projectKey: z.string().describe("The project key to switch to"),
1158
+ workspaceKey: z.string().describe("The workspace key to switch to"),
1156
1159
  }),
1157
1160
  handler: async (args) => {
1158
- setActiveProject(args.projectKey);
1161
+ setActiveWorkspace(args.workspaceKey);
1159
1162
  return {
1160
1163
  content: [{
1161
1164
  type: "text",
1162
- 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.`,
1163
1166
  }],
1164
1167
  };
1165
1168
  },
1166
1169
  },
1167
1170
  list_orgs: {
1168
- 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.",
1169
1172
  schema: z.object({}),
1170
1173
  handler: async () => {
1171
1174
  const ctx = getUserContext();
@@ -1180,8 +1183,8 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1180
1183
  const activeOrg = getActiveOrg();
1181
1184
  const lines = ctx.orgs.map((org) => {
1182
1185
  const active = org.orgId === activeOrg ? " ← active" : "";
1183
- const projects = org.projects.map((p) => ` ${p.projectKey} — ${p.name}`).join("\n");
1184
- 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)"}`;
1185
1188
  });
1186
1189
  return {
1187
1190
  content: [{
@@ -1192,7 +1195,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1192
1195
  },
1193
1196
  },
1194
1197
  switch_org: {
1195
- 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.",
1196
1199
  schema: z.object({
1197
1200
  orgId: z.string().describe("The organization ID to switch to (from list_orgs)"),
1198
1201
  }),
@@ -1219,12 +1222,12 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1219
1222
  };
1220
1223
  }
1221
1224
  setActiveOrg(args.orgId);
1222
- const project = getActiveProject();
1223
- const projectInfo = project ? `, active project: ${project}` : "";
1225
+ const workspace = getActiveWorkspace();
1226
+ const projectInfo = workspace ? `, active workspace: ${workspace}` : "";
1224
1227
  return {
1225
1228
  content: [{
1226
1229
  type: "text",
1227
- 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}.`,
1228
1231
  }],
1229
1232
  };
1230
1233
  },
@@ -1375,7 +1378,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1375
1378
  //
1376
1379
  // Merchant-scoped tools for managing an affiliate program through the
1377
1380
  // Apex Partner Network. Every tool authenticates via the caller's
1378
- // active project + API key (same pattern as experiments / comms).
1381
+ // active workspace + API key (same pattern as experiments / comms).
1379
1382
  // Partner-facing flows (self-signup, portal) + admin Fraud Ops are
1380
1383
  // intentionally not wrapped — they require different auth.
1381
1384
  list_partner_programs: {
@@ -1550,7 +1553,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1550
1553
  const tag = f.isCustom ? " (custom)" : "";
1551
1554
  return `• ${kind}: ${display}${tag} — source: ${f.source}`;
1552
1555
  }).join("\n");
1553
- 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)}` }] };
1554
1557
  },
1555
1558
  },
1556
1559
  vouch_for_partner: {
@@ -1570,7 +1573,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1570
1573
  // Pair with the AudienceActivationWizard (Layer 2) to give agents
1571
1574
  // a way to wire up audience instrumentation without leaving the
1572
1575
  // editor. Both tools are PROJECT-SCOPED via the api-client's
1573
- // active-project context — the merchant's CLI run pins which
1576
+ // active-workspace context — the merchant's CLI run pins which
1574
1577
  // workspace they're acting on.
1575
1578
  wire_audience_seed: {
1576
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.`,
@@ -1588,13 +1591,13 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1588
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."),
1589
1592
  }),
1590
1593
  handler: async ({ seedId, framework = "typescript", fireTestEvent = true, }) => {
1591
- const projectKey = getActiveProject() || process.env.APEX_PROJECT_KEY;
1592
- if (!projectKey) {
1594
+ const workspaceKey = getActiveWorkspace() || (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY);
1595
+ if (!workspaceKey) {
1593
1596
  return {
1594
1597
  content: [
1595
1598
  {
1596
1599
  type: "text",
1597
- 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.",
1598
1601
  },
1599
1602
  ],
1600
1603
  };
@@ -1656,7 +1659,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1656
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.`
1657
1660
  : `## Test events\nNo test events fired (per fireTestEvent=false). Run the wizard's "Send a test event" button manually after wiring.`,
1658
1661
  ``,
1659
- `Project: ${projectKey}`,
1662
+ `Workspace: ${workspaceKey}`,
1660
1663
  ].join("\n");
1661
1664
  return { content: [{ type: "text", text: summary }] };
1662
1665
  },
@@ -1677,13 +1680,13 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1677
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."),
1678
1681
  }),
1679
1682
  handler: async ({ audienceId, framework = "typescript", fireTestEvent = true, }) => {
1680
- const projectKey = getActiveProject() || process.env.APEX_PROJECT_KEY;
1681
- if (!projectKey) {
1683
+ const workspaceKey = getActiveWorkspace() || (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY);
1684
+ if (!workspaceKey) {
1682
1685
  return {
1683
1686
  content: [
1684
1687
  {
1685
1688
  type: "text",
1686
- 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.",
1687
1690
  },
1688
1691
  ],
1689
1692
  };
@@ -1752,7 +1755,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1752
1755
  else {
1753
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.`);
1754
1757
  }
1755
- sections.push(`\nProject: ${projectKey}`);
1758
+ sections.push(`\nWorkspace: ${workspaceKey}`);
1756
1759
  return { content: [{ type: "text", text: sections.join("\n") }] };
1757
1760
  },
1758
1761
  },
@@ -1764,13 +1767,13 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1764
1767
  .describe("Audience id (the workspace's audience record, not the seed). Use list_audiences first to find it."),
1765
1768
  }),
1766
1769
  handler: async ({ audienceId }) => {
1767
- const projectKey = getActiveProject() || process.env.APEX_PROJECT_KEY;
1768
- if (!projectKey) {
1770
+ const workspaceKey = getActiveWorkspace() || (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY);
1771
+ if (!workspaceKey) {
1769
1772
  return {
1770
1773
  content: [
1771
1774
  {
1772
1775
  type: "text",
1773
- 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.",
1774
1777
  },
1775
1778
  ],
1776
1779
  };
@@ -1799,7 +1802,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1799
1802
  preview && preview.matchingCount === 0
1800
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`
1801
1804
  : "",
1802
- `\nProject: ${projectKey}`,
1805
+ `\nWorkspace: ${workspaceKey}`,
1803
1806
  ].join("\n");
1804
1807
  return { content: [{ type: "text", text: summary }] };
1805
1808
  },
@@ -1968,7 +1971,7 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
1968
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.`,
1969
1972
  schema: z.object({}),
1970
1973
  handler: async () => {
1971
- // 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
1972
1975
  // that apiGet sends automatically.
1973
1976
  const data = await apiGet("/api/setup-state");
1974
1977
  const { overrides, signal, workspaceKey } = data.data;
@@ -2007,7 +2010,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
2007
2010
  .describe("Include archived sources. Defaults to false."),
2008
2011
  }),
2009
2012
  handler: async ({ includeArchived }) => {
2010
- const proj = getActiveProject();
2013
+ const proj = getActiveWorkspace();
2011
2014
  const qs = includeArchived ? "?includeArchived=true" : "";
2012
2015
  const json = await apiGet(`/api/workspaces/${proj}/data-sources${qs}`);
2013
2016
  const sources = json.data ?? [];
@@ -2027,7 +2030,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
2027
2030
  name: z.string().describe("Display name, e.g. 'Marketing site' or 'Backend API'."),
2028
2031
  }),
2029
2032
  handler: async ({ kind, name }) => {
2030
- const proj = getActiveProject();
2033
+ const proj = getActiveWorkspace();
2031
2034
  const json = await apiPost(`/api/workspaces/${proj}/data-sources`, { kind, name });
2032
2035
  const s = json.data;
2033
2036
  return { content: [{ type: "text", text: `✓ Created data source **${s.name}** (${s.kind}) — ${s.status} · \`${s.id}\`` }] };
@@ -2039,7 +2042,7 @@ _Suggest the next step the user should tackle based on what's incomplete in the
2039
2042
  id: z.string().describe("The data source id (ds_...)."),
2040
2043
  }),
2041
2044
  handler: async ({ id }) => {
2042
- const proj = getActiveProject();
2045
+ const proj = getActiveWorkspace();
2043
2046
  const json = await apiGet(`/api/workspaces/${proj}/data-sources/${encodeURIComponent(id)}/health`);
2044
2047
  const h = json.data;
2045
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"}` }] };
@@ -2105,13 +2108,13 @@ _Suggest the next step the user should tackle based on what's incomplete in the
2105
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.`,
2106
2109
  schema: z.object({}),
2107
2110
  handler: async () => {
2108
- const projectKey = getActiveProject();
2109
- if (!projectKey) {
2111
+ const workspaceKey = getActiveWorkspace();
2112
+ if (!workspaceKey) {
2110
2113
  return {
2111
- content: [{ type: "text", text: "No active project. Use switch_project first (or set APEX_PROJECT_KEY)." }],
2114
+ content: [{ type: "text", text: "No active workspace. Use switch_project first (or set APEX_PROJECT_KEY)." }],
2112
2115
  };
2113
2116
  }
2114
- const json = await apiGet(`/api/workspaces/${encodeURIComponent(projectKey)}/readiness`);
2117
+ const json = await apiGet(`/api/workspaces/${encodeURIComponent(workspaceKey)}/readiness`);
2115
2118
  const d = json.data;
2116
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(", ")}` : ""}`);
2117
2120
  const traitLines = d.traits.map((t) => `- ${t.name}: ${t.present ? "arriving" : "not seen"}${t.inferredType ? ` (${t.inferredType})` : ""}`);
@@ -2129,7 +2132,52 @@ _Suggest the next step the user should tackle based on what's incomplete in the
2129
2132
  return {
2130
2133
  content: [{
2131
2134
  type: "text",
2132
- text: `# Wiring status — ${projectKey}\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_"}`,
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".`,
2133
2181
  }],
2134
2182
  };
2135
2183
  },