@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/README.md +2 -2
- package/dist/api-client.d.ts +5 -5
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +18 -18
- package/dist/api-client.js.map +1 -1
- package/dist/resources.js +1 -1
- package/dist/resources.js.map +1 -1
- package/dist/tools.d.ts +56 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +97 -49
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/skills/apex-experimentation/SKILL.md +2 -2
- package/skills/apex-growth-intelligence/SKILL.md +2 -2
- package/skills/apex-growth-tracking/SKILL.md +4 -4
- package/skills/apex-integration-cookbook/SKILL.md +7 -7
- package/skills/apex-partner-network/SKILL.md +4 -4
- package/skills/apex-spec/SKILL.md +77 -0
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { apiGet, apiPost, apiPatch, apiDelete, postWithIdempotency,
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
|
1139
|
+
text: "No workspaces found for this API key.",
|
|
1137
1140
|
}],
|
|
1138
1141
|
};
|
|
1139
1142
|
}
|
|
1140
|
-
const lines =
|
|
1141
|
-
const active = p.
|
|
1142
|
-
return ` ${p.
|
|
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: [`
|
|
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
|
|
1156
|
+
description: "Switch the active workspace for this session. All subsequent API calls will use this workspace.",
|
|
1154
1157
|
schema: z.object({
|
|
1155
|
-
|
|
1158
|
+
workspaceKey: z.string().describe("The workspace key to switch to"),
|
|
1156
1159
|
}),
|
|
1157
1160
|
handler: async (args) => {
|
|
1158
|
-
|
|
1161
|
+
setActiveWorkspace(args.workspaceKey);
|
|
1159
1162
|
return {
|
|
1160
1163
|
content: [{
|
|
1161
1164
|
type: "text",
|
|
1162
|
-
text: `Switched to
|
|
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
|
|
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
|
|
1184
|
-
return ` ${org.orgName} (${org.orgId}, ${org.role})${active}\n${
|
|
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
|
|
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
|
|
1223
|
-
const projectInfo =
|
|
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.
|
|
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
|
|
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.
|
|
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-
|
|
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
|
|
1592
|
-
if (!
|
|
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
|
|
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
|
-
`
|
|
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
|
|
1681
|
-
if (!
|
|
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
|
|
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(`\
|
|
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
|
|
1768
|
-
if (!
|
|
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
|
|
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
|
-
`\
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
2109
|
-
if (!
|
|
2111
|
+
const workspaceKey = getActiveWorkspace();
|
|
2112
|
+
if (!workspaceKey) {
|
|
2110
2113
|
return {
|
|
2111
|
-
content: [{ type: "text", text: "No active
|
|
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(
|
|
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 — ${
|
|
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
|
},
|